import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; describe("background alarms", () => { let storage: StorageShape; let alarmListener: ((alarm: { name: string }) => void | Promise) | undefined; let idleListener: ((state: string) => void | Promise) | undefined; let sentRuntimeMessages: Array; let sentTabMessages: Array<{ tabId: number; message: unknown }>; beforeEach(() => { vi.resetModules(); storage = {}; sentRuntimeMessages = []; sentTabMessages = []; const chromeMock = { storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") { return { [key]: storage[key] }; } return key.reduce>((result, entry) => { result[entry] = storage[entry]; return result; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) { delete storage[key]; } }), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn((fn: (alarm: { name: string }) => void | Promise) => { alarmListener = fn; }), }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn((fn: (state: string) => void | Promise) => { idleListener = fn; }), }, }, runtime: { sendMessage: vi.fn(async (message: unknown) => { sentRuntimeMessages.push(message); return { ok: true }; }), }, tabs: { query: vi.fn((_query: unknown, callback: (tabs: Array<{ id?: number }>) => void) => { callback([{ id: 1 }, { id: 2 }]); }), sendMessage: vi.fn(async (tabId: number, message: unknown) => { sentTabMessages.push({ tabId, message }); return { ok: true }; }), }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled in tests"); })); }); it("touchAutoLock creates an alarm using autoLockMinutes from settings", async () => { const vault = await import("./vault"); const alarms = await import("./alarms"); await vault.createVault("correct-pass-12345"); await vault.updateSettings({ autoLockMinutes: 5 }); alarms.touchAutoLock(); expect(chrome.alarms.clear).toHaveBeenCalledWith("autolock"); expect(chrome.alarms.create).toHaveBeenLastCalledWith("autolock", { delayInMinutes: 5, }); }); it("touchAutoLock does not create an alarm when autoLockMinutes is 0", async () => { const vault = await import("./vault"); const alarms = await import("./alarms"); await vault.createVault("correct-pass-12345"); await vault.updateSettings({ autoLockMinutes: 0 }); (chrome.alarms.create as unknown as ReturnType).mockClear(); alarms.touchAutoLock(); expect(chrome.alarms.create).not.toHaveBeenCalled(); }); it("setupAutoLock configures idle detection using the settings period", async () => { const vault = await import("./vault"); const alarms = await import("./alarms"); await vault.createVault("correct-pass-12345"); await vault.updateSettings({ autoLockMinutes: 2 }); await alarms.setupAutoLock(); expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(120); }); it("setupAutoLock reads the persisted setting before applying idle detection", async () => { storage.peach_settings = { autoLockMinutes: 30 }; const alarms = await import("./alarms"); await alarms.setupAutoLock(); expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(1800); }); it("alarm firing locks the vault and broadcasts VAULT_LOCKED", async () => { const vault = await import("./vault"); await import("./alarms"); await vault.createVault("correct-pass-12345"); await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }); await alarmListener?.({ name: "autolock" }); expect(vault.isUnlocked()).toBe(false); expect(sentRuntimeMessages).toContainEqual({ type: "VAULT_LOCKED" }); expect(sentTabMessages).toEqual([ { tabId: 1, message: { type: "VAULT_LOCKED" } }, { tabId: 2, message: { type: "VAULT_LOCKED" } }, ]); }); it("idle state change to idle locks the vault and active does not", async () => { const vault = await import("./vault"); await import("./alarms"); await vault.createVault("correct-pass-12345"); await vault.updateSettings({ autoLockMinutes: 5 }); await idleListener?.("active"); expect(vault.isUnlocked()).toBe(true); await idleListener?.("idle"); expect(vault.isUnlocked()).toBe(false); expect(sentRuntimeMessages).toContainEqual({ type: "VAULT_LOCKED" }); }); });