sync-alarm.test.ts

4.0 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";23// vi.mock is hoisted to the top of the file — it cannot see runtime-created4// `const`s, so the shared stub has to live in a vi.hoisted block.5const { triggerSync, setSyncRunner } = vi.hoisted(() => ({6  triggerSync: vi.fn(async () => undefined),7  setSyncRunner: vi.fn(),8}));910vi.mock("./sync-trigger", () => ({11  triggerSyncIfReachable: triggerSync,12  setSyncRunner,13}));1415import {16  setupSyncAlarm,17  SYNC_ALARM_NAME,18  SYNC_ALARM_PERIOD_MINUTES,19  _resetSyncAlarmForTest,20} from "./sync-alarm";2122describe("sync-alarm", () => {23  type Alarm = { name: string; periodInMinutes?: number; delayInMinutes?: number };24  let alarmListener: ((alarm: Alarm) => void) | undefined;25  let createCalls: Array<{ name: string; opts: Record<string, unknown> }>;26  let clears: string[];2728  beforeEach(() => {29    triggerSync.mockClear();30    setSyncRunner.mockClear();31    _resetSyncAlarmForTest();32    alarmListener = undefined;33    createCalls = [];34    clears = [];35    (globalThis as { chrome?: unknown }).chrome = {36      alarms: {37        clear: vi.fn((name: string) => {38          clears.push(name);39        }),40        create: vi.fn((name: string, opts: Record<string, unknown>) => {41          createCalls.push({ name, opts });42        }),43        onAlarm: {44          addListener: vi.fn((fn: (alarm: Alarm) => void) => {45            alarmListener = fn;46          }),47        },48      },49      runtime: { sendMessage: vi.fn() },50      storage: {51        local: {52          get: vi.fn(async () => ({})),53          set: vi.fn(async () => undefined),54          remove: vi.fn(async () => undefined),55        },56      },57    };58  });5960  it("schedules a recurring alarm at SYNC_ALARM_PERIOD_MINUTES with the named alarm", () => {61    setupSyncAlarm();62    expect(createCalls).toHaveLength(1);63    expect(createCalls[0].name).toBe(SYNC_ALARM_NAME);64    expect(createCalls[0].opts).toEqual({ periodInMinutes: SYNC_ALARM_PERIOD_MINUTES });65    // Period must be at least Chrome's 1-minute recurring-alarm floor.66    expect(SYNC_ALARM_PERIOD_MINUTES).toBeGreaterThanOrEqual(1);67  });6869  it("is idempotent: calling setup twice clears the first and reschedules once", () => {70    setupSyncAlarm();71    setupSyncAlarm();72    expect(clears).toEqual([SYNC_ALARM_NAME, SYNC_ALARM_NAME]);73    expect(createCalls).toHaveLength(2);74  });7576  it("registers the alarm listener exactly once across multiple setup calls", () => {77    setupSyncAlarm();78    setupSyncAlarm();79    setupSyncAlarm();80    const addListener = (81      globalThis as unknown as { chrome: { alarms: { onAlarm: { addListener: ReturnType<typeof vi.fn> } } } }82    ).chrome.alarms.onAlarm.addListener;83    expect(addListener).toHaveBeenCalledTimes(1);84  });8586  it("on alarm fire, calls triggerSyncIfReachable with reason 'alarm'", async () => {87    setupSyncAlarm();88    await alarmListener?.({ name: SYNC_ALARM_NAME });89    expect(triggerSync).toHaveBeenCalledTimes(1);90    expect(triggerSync).toHaveBeenCalledWith({ reason: "alarm" });91  });9293  it("ignores alarms for other names — does not dial on the auto-lock alarm", async () => {94    setupSyncAlarm();95    await alarmListener?.({ name: "autolock" });96    expect(triggerSync).not.toHaveBeenCalled();97  });9899  it("alarm fire that fails inside triggerSyncIfReachable does not break the listener", async () => {100    // The handler swallows triggerSync errors — a one-off dial failure must101    // not unsubscribe the listener or throw into Chrome's alarm dispatcher.102    triggerSync.mockRejectedValueOnce(new Error("dial failed"));103    setupSyncAlarm();104    // alarmListener is registered sync in setupSyncAlarm; fire it and105    // observe triggerSync was called without the rejection propagating up.106    await alarmListener?.({ name: SYNC_ALARM_NAME });107    expect(triggerSync).toHaveBeenCalledTimes(1);108    // Subsequent fires still dial — the listener is still installed.109    await alarmListener?.({ name: SYNC_ALARM_NAME });110    expect(triggerSync).toHaveBeenCalledTimes(2);111  });112});