1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-04 05:08:06 +02:00

[PM-6426] Implementing jest tests for the BrowserTaskSchedulerService

This commit is contained in:
Cesar Gonzalez 2024-04-01 17:01:08 -05:00
parent 7fd41c37f5
commit 541d0cecf8
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
5 changed files with 180 additions and 19 deletions

View File

@ -554,7 +554,6 @@ describe("BrowserApi", () => {
describe("clearAlarm", () => {
it("clears the alarm with the provided name", async () => {
const alarmName = "alarm-name";
chrome.alarms.clear = jest.fn().mockImplementation((name, callback) => callback(true));
const wasCleared = await BrowserApi.clearAlarm(alarmName);
@ -565,8 +564,6 @@ describe("BrowserApi", () => {
describe("clearAllAlarms", () => {
it("clears all alarms", async () => {
chrome.alarms.clearAll = jest.fn().mockImplementation((callback) => callback(true));
const wasCleared = await BrowserApi.clearAllAlarms();
expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function));
@ -578,9 +575,6 @@ describe("BrowserApi", () => {
it("creates an alarm", async () => {
const alarmName = "alarm-name";
const alarmInfo = { when: 1000 };
chrome.alarms.create = jest
.fn()
.mockImplementation((_name, _createInfo, callback) => callback());
await BrowserApi.createAlarm(alarmName, alarmInfo);

View File

@ -0,0 +1,148 @@
import { mock, MockProxy } from "jest-mock-extended";
import { Observable } from "rxjs";
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";
import { ActiveAlarm } from "./abstractions/browser-task-scheduler.service";
import { BrowserTaskSchedulerService } from "./browser-task-scheduler.service";
let activeAlarms: ActiveAlarm[] = [];
jest.mock("rxjs", () => ({
firstValueFrom: jest.fn(() => Promise.resolve(activeAlarms)),
map: jest.fn(),
Observable: jest.fn(),
}));
describe("BrowserTaskSchedulerService", () => {
let logService: MockProxy<ConsoleLogService>;
let stateProvider: MockProxy<StateProvider>;
let browserTaskSchedulerService: BrowserTaskSchedulerService;
beforeEach(() => {
activeAlarms = [];
logService = mock<ConsoleLogService>();
stateProvider = mock<StateProvider>({
getGlobal: jest.fn(() =>
mock<GlobalState<any>>({
state$: mock<Observable<any>>(),
}),
),
});
browserTaskSchedulerService = new BrowserTaskSchedulerService(logService, stateProvider);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("setTimeout", () => {
it("uses the global setTimeout API if the delay is less than 1000ms", async () => {
const callback = jest.fn();
const delayInMs = 999;
jest.spyOn(globalThis, "setTimeout");
await browserTaskSchedulerService.setTimeout(
callback,
delayInMs,
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs);
expect(chrome.alarms.create).not.toHaveBeenCalled();
});
it("triggers a recovered alarm immediately and skips creating the alarm", async () => {
activeAlarms = [mock<ActiveAlarm>({ name: ScheduledTaskNames.loginStrategySessionTimeout })];
browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout,
);
const callback = jest.fn();
await browserTaskSchedulerService.setTimeout(
callback,
60 * 1000,
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(callback).toHaveBeenCalled();
expect(chrome.alarms.create).not.toHaveBeenCalled();
});
it("creates a timeout alarm", async () => {
const callback = jest.fn();
const delayInMinutes = 2;
await browserTaskSchedulerService.setTimeout(
callback,
delayInMinutes * 60 * 1000,
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
{ delayInMinutes },
expect.any(Function),
);
});
});
describe("setInterval", () => {
it("uses the global setInterval API if the interval is less than 1000ms", async () => {
const callback = jest.fn();
const intervalInMs = 999;
jest.spyOn(globalThis, "setInterval");
await browserTaskSchedulerService.setInterval(
callback,
intervalInMs,
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs);
expect(chrome.alarms.create).not.toHaveBeenCalled();
});
it("triggers a recovered alarm before creating the interval alarm", async () => {
const periodInMinutes = 4;
activeAlarms = [mock<ActiveAlarm>({ name: ScheduledTaskNames.loginStrategySessionTimeout })];
browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout,
);
const callback = jest.fn();
await browserTaskSchedulerService.setInterval(
callback,
periodInMinutes * 60 * 1000,
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(callback).toHaveBeenCalled();
expect(chrome.alarms.create).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
{ periodInMinutes, delayInMinutes: periodInMinutes },
expect.any(Function),
);
});
it("creates an interval alarm", async () => {
const callback = jest.fn();
const periodInMinutes = 2;
const initialDelayInMs = 1000;
await browserTaskSchedulerService.setInterval(
callback,
periodInMinutes * 60 * 1000,
ScheduledTaskNames.loginStrategySessionTimeout,
initialDelayInMs,
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
{ periodInMinutes, delayInMinutes: initialDelayInMs / 1000 / 60 },
expect.any(Function),
);
});
});
});

View File

@ -46,10 +46,19 @@ export class BrowserTaskSchedulerService
this.verifyAlarmsState().catch((e) => this.logService.error(e));
}
/**
* Sets a timeout to execute a callback after a delay. If the delay is less
* than 1 minute, it will use the global setTimeout. Otherwise, it will
* create a browser extension alarm to handle the delay.
*
* @param callback - The function to be called after the delay.
* @param delayInMs - The delay in milliseconds.
* @param taskName - The name of the task, used in defining the alarm.
*/
async setTimeout(
callback: () => void,
delayInMs: number,
taskName?: ScheduledTaskName,
taskName: ScheduledTaskName,
): Promise<number | NodeJS.Timeout> {
const delayInMinutes = delayInMs / 1000 / 60;
if (delayInMinutes < 1) {
@ -65,10 +74,20 @@ export class BrowserTaskSchedulerService
await this.createAlarm(taskName, { delayInMinutes });
}
/**
* Sets an interval to execute a callback at each interval. If the interval is
* less than 1 minute, it will use the global setInterval. Otherwise, it will
* create a browser extension alarm to handle the interval.
*
* @param callback
* @param intervalInMs
* @param taskName
* @param initialDelayInMs
*/
async setInterval(
callback: () => void,
intervalInMs: number,
taskName?: ScheduledTaskName,
taskName: ScheduledTaskName,
initialDelayInMs?: number,
): Promise<number | NodeJS.Timeout> {
const intervalInMinutes = intervalInMs / 1000 / 60;
@ -169,8 +188,8 @@ export class BrowserTaskSchedulerService
private async deleteActiveAlarm(name: ScheduledTaskName): Promise<void> {
delete this.onAlarmHandlers[name];
const activeAlarms = await firstValueFrom(this.activeAlarms$);
const filteredAlarms = activeAlarms.filter((alarm) => alarm.name !== name);
await this.updateActiveAlarms(filteredAlarms);
const filteredAlarms = activeAlarms?.filter((alarm) => alarm.name !== name);
await this.updateActiveAlarms(filteredAlarms || []);
}
private async updateActiveAlarms(alarms: ActiveAlarm[]): Promise<void> {

View File

@ -125,11 +125,11 @@ const offscreen = {
};
const alarms = {
clear: jest.fn(),
clearAll: jest.fn(),
create: jest.fn(),
get: jest.fn(),
getAll: jest.fn(),
clear: jest.fn().mockImplementation((_name, callback) => callback(true)),
clearAll: jest.fn().mockImplementation((callback) => callback(true)),
create: jest.fn().mockImplementation((_name, _createInfo, callback) => callback()),
get: jest.fn().mockImplementation((_name, callback) => callback(null)),
getAll: jest.fn().mockImplementation((callback) => callback([])),
onAlarm: {
addListener: jest.fn(),
removeListener: jest.fn(),

View File

@ -17,7 +17,7 @@ export class TaskSchedulerService implements TaskSchedulerServiceInterface {
delayInMs: number,
_taskName?: ScheduledTaskName,
): Promise<number | NodeJS.Timeout> {
return setTimeout(() => callback(), delayInMs);
return globalThis.setTimeout(() => callback(), delayInMs);
}
/**
@ -34,7 +34,7 @@ export class TaskSchedulerService implements TaskSchedulerServiceInterface {
_taskName?: ScheduledTaskName,
_initialDelayInMs?: number,
): Promise<number | NodeJS.Timeout> {
return setInterval(() => callback(), intervalInMs);
return globalThis.setInterval(() => callback(), intervalInMs);
}
/**
@ -44,11 +44,11 @@ export class TaskSchedulerService implements TaskSchedulerServiceInterface {
*/
async clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void> {
if (taskIdentifier.timeoutId) {
clearTimeout(taskIdentifier.timeoutId);
globalThis.clearTimeout(taskIdentifier.timeoutId);
}
if (taskIdentifier.intervalId) {
clearInterval(taskIdentifier.intervalId);
globalThis.clearInterval(taskIdentifier.intervalId);
}
}
}