1import { beforeEach, describe, expect, it, vi } from "vitest";23type StorageShape = Record<string, unknown>;45describe("background alarms", () => {6 let storage: StorageShape;7 let alarmListener: ((alarm: { name: string }) => void | Promise<void>) | undefined;8 let idleListener: ((state: string) => void | Promise<void>) | undefined;9 let sentRuntimeMessages: Array<unknown>;10 let sentTabMessages: Array<{ tabId: number; message: unknown }>;1112 beforeEach(() => {13 vi.resetModules();14 storage = {};15 sentRuntimeMessages = [];16 sentTabMessages = [];1718 const chromeMock = {19 storage: {20 local: {21 get: vi.fn(async (key?: string | string[]) => {22 if (!key) return storage;23 if (typeof key === "string") {24 return { [key]: storage[key] };25 }2627 return key.reduce<Record<string, unknown>>((result, entry) => {28 result[entry] = storage[entry];29 return result;30 }, {});31 }),32 set: vi.fn(async (next: Record<string, unknown>) => {33 Object.assign(storage, next);34 }),35 remove: vi.fn(async (keys: string | string[]) => {36 const list = Array.isArray(keys) ? keys : [keys];37 for (const key of list) {38 delete storage[key];39 }40 }),41 },42 },43 alarms: {44 clear: vi.fn(),45 create: vi.fn(),46 onAlarm: {47 addListener: vi.fn((fn: (alarm: { name: string }) => void | Promise<void>) => {48 alarmListener = fn;49 }),50 },51 },52 idle: {53 setDetectionInterval: vi.fn(),54 onStateChanged: {55 addListener: vi.fn((fn: (state: string) => void | Promise<void>) => {56 idleListener = fn;57 }),58 },59 },60 runtime: {61 sendMessage: vi.fn(async (message: unknown) => {62 sentRuntimeMessages.push(message);63 return { ok: true };64 }),65 },66 tabs: {67 query: vi.fn((_query: unknown, callback: (tabs: Array<{ id?: number }>) => void) => {68 callback([{ id: 1 }, { id: 2 }]);69 }),70 sendMessage: vi.fn(async (tabId: number, message: unknown) => {71 sentTabMessages.push({ tabId, message });72 return { ok: true };73 }),74 },75 } as unknown as typeof chrome;7677 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;78 vi.stubGlobal("fetch", vi.fn(async () => {79 throw new Error("network disabled in tests");80 }));81 });8283 it("touchAutoLock creates an alarm using autoLockMinutes from settings", async () => {84 const vault = await import("./vault");85 const alarms = await import("./alarms");8687 await vault.createVault("correct-pass-12345");88 await vault.updateSettings({ autoLockMinutes: 5 });8990 alarms.touchAutoLock();9192 expect(chrome.alarms.clear).toHaveBeenCalledWith("autolock");93 expect(chrome.alarms.create).toHaveBeenLastCalledWith("autolock", {94 delayInMinutes: 5,95 });96 });9798 it("touchAutoLock does not create an alarm when autoLockMinutes is 0", async () => {99 const vault = await import("./vault");100 const alarms = await import("./alarms");101102 await vault.createVault("correct-pass-12345");103 await vault.updateSettings({ autoLockMinutes: 0 });104105 (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();106 alarms.touchAutoLock();107108 expect(chrome.alarms.create).not.toHaveBeenCalled();109 });110111 it("setupAutoLock configures idle detection using the settings period", async () => {112 const vault = await import("./vault");113 const alarms = await import("./alarms");114115 await vault.createVault("correct-pass-12345");116 await vault.updateSettings({ autoLockMinutes: 2 });117118 await alarms.setupAutoLock();119120 expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(120);121 });122123 it("setupAutoLock reads the persisted setting before applying idle detection", async () => {124 storage.peach_settings = { autoLockMinutes: 30 };125 const alarms = await import("./alarms");126127 await alarms.setupAutoLock();128129 expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(1800);130 });131132 it("alarm firing locks the vault and broadcasts VAULT_LOCKED", async () => {133 const vault = await import("./vault");134 await import("./alarms");135136 await vault.createVault("correct-pass-12345");137 await vault.addEntry({138 title: "GitHub",139 username: "june",140 password: "secret123",141 url: "https://github.com",142 });143144 await alarmListener?.({ name: "autolock" });145146 expect(vault.isUnlocked()).toBe(false);147 expect(sentRuntimeMessages).toContainEqual({ type: "VAULT_LOCKED" });148 expect(sentTabMessages).toEqual([149 { tabId: 1, message: { type: "VAULT_LOCKED" } },150 { tabId: 2, message: { type: "VAULT_LOCKED" } },151 ]);152 });153154 it("idle state change to idle locks the vault and active does not", async () => {155 const vault = await import("./vault");156 await import("./alarms");157158 await vault.createVault("correct-pass-12345");159 await vault.updateSettings({ autoLockMinutes: 5 });160161 await idleListener?.("active");162 expect(vault.isUnlocked()).toBe(true);163164 await idleListener?.("idle");165 expect(vault.isUnlocked()).toBe(false);166 expect(sentRuntimeMessages).toContainEqual({ type: "VAULT_LOCKED" });167 });168});