1import { beforeEach, describe, expect, it, vi } from "vitest";2345const { 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 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 101 102 triggerSync.mockRejectedValueOnce(new Error("dial failed"));103 setupSyncAlarm();104 105 106 await alarmListener?.({ name: SYNC_ALARM_NAME });107 expect(triggerSync).toHaveBeenCalledTimes(1);108 109 await alarmListener?.({ name: SYNC_ALARM_NAME });110 expect(triggerSync).toHaveBeenCalledTimes(2);111 });112});