diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts index ccd5346679..1387e88705 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { Observable } from "rxjs"; +import { TaskIdentifier } from "@bitwarden/common/platform/abstractions/task-scheduler.service"; import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; @@ -251,4 +252,70 @@ describe("BrowserTaskSchedulerService", () => { ); }); }); + + describe("clearScheduledTask", () => { + afterEach(() => { + chrome.alarms.clear = jest.fn().mockImplementation((_name, callback) => callback(true)); + }); + + it("skips clearing the alarm if the alarm name is not provided", async () => { + await browserTaskSchedulerService.clearScheduledTask({ + timeoutId: 1, + intervalId: 2, + }); + + expect(chrome.alarms.clear).not.toHaveBeenCalled(); + }); + + it("skips deleting the active alarm if the alarm was not cleared", async () => { + const taskIdentifier: TaskIdentifier = { taskName: ScheduledTaskNames.eventUploadsInterval }; + chrome.alarms.clear = jest.fn().mockImplementation((_name, callback) => callback(false)); + jest.spyOn(browserTaskSchedulerService as any, "deleteActiveAlarm"); + + await browserTaskSchedulerService.clearScheduledTask(taskIdentifier); + + expect(browserTaskSchedulerService["deleteActiveAlarm"]).not.toHaveBeenCalled(); + }); + + it("clears a named alarm", async () => { + const taskIdentifier: TaskIdentifier = { taskName: ScheduledTaskNames.eventUploadsInterval }; + jest.spyOn(browserTaskSchedulerService as any, "deleteActiveAlarm"); + + await browserTaskSchedulerService.clearScheduledTask(taskIdentifier); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.eventUploadsInterval, + expect.any(Function), + ); + expect(browserTaskSchedulerService["deleteActiveAlarm"]).toHaveBeenCalledWith( + ScheduledTaskNames.eventUploadsInterval, + ); + }); + }); + + describe("clearAllScheduledTasks", () => { + it("clears all scheduled tasks and extension alarms", async () => { + jest.spyOn(BrowserApi, "clearAllAlarms"); + jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms"); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(BrowserApi.clearAllAlarms).toHaveBeenCalled(); + expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]); + expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({}); + expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0); + }); + }); + + describe("handleOnAlarm", () => { + it("triggers the alarm", async () => { + const alarm = mock({ name: ScheduledTaskNames.eventUploadsInterval }); + const callback = jest.fn(); + browserTaskSchedulerService["onAlarmHandlers"][alarm.name] = callback; + + await browserTaskSchedulerService["handleOnAlarm"](alarm); + + expect(callback).toHaveBeenCalled(); + }); + }); }); diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.ts index a3daca0d5d..06dc86c7df 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.ts @@ -129,6 +129,10 @@ export class BrowserTaskSchedulerService } } + /** + * Clears all scheduled tasks by clearing all browser extension + * alarms and resetting the active alarms state. + */ async clearAllScheduledTasks(): Promise { await BrowserApi.clearAllAlarms(); await this.updateActiveAlarms([]); @@ -136,6 +140,12 @@ export class BrowserTaskSchedulerService this.recoveredAlarms.clear(); } + /** + * Creates a browser extension alarm with the given name and create info. + * + * @param name - The name of the alarm. + * @param createInfo - The alarm create info. + */ private async createAlarm( name: ScheduledTaskName, createInfo: chrome.alarms.AlarmCreateInfo, @@ -150,6 +160,12 @@ export class BrowserTaskSchedulerService await this.setActiveAlarm({ name, startTime: Date.now(), createInfo }); } + /** + * Registers an alarm handler for the given name. + * + * @param name - The name of the alarm. + * @param handler - The alarm handler. + */ private registerAlarmHandler(name: ScheduledTaskName, handler: CallableFunction): void { if (this.onAlarmHandlers[name]) { this.logService.warning(`Alarm handler for ${name} already exists. Overwriting.`); @@ -158,6 +174,10 @@ export class BrowserTaskSchedulerService this.onAlarmHandlers[name] = () => handler(); } + /** + * Verifies the state of the active alarms by checking if + * any alarms have been missed or need to be created. + */ private async verifyAlarmsState(): Promise { const currentTime = Date.now(); const activeAlarms = await firstValueFrom(this.activeAlarms$); @@ -169,12 +189,12 @@ export class BrowserTaskSchedulerService continue; } - if ( - (createInfo.when && createInfo.when < currentTime) || - (!createInfo.periodInMinutes && - createInfo.delayInMinutes && - startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime) - ) { + const shouldAlarmHaveBeenTriggered = createInfo.when && createInfo.when < currentTime; + const hasSetTimeoutAlarmExceededDelay = + !createInfo.periodInMinutes && + createInfo.delayInMinutes && + startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; + if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { this.recoveredAlarms.add(name); continue; }