import { describe, expect, it } from "@suaveplan/testing/runner"; import { CLIPBOARD_CODES, ClipboardError } from "../errors/errors.js"; import { isSecure, requireSecureContext, waitForNextFocus, } from "./security.js"; describe("isSecure", () => { it("treats undefined window as secure (SSR-safe)", { timeout: 5000, }, () => { expect(isSecure(undefined)).toBe(true); }); it("returns true when window.isSecureContext is true", { timeout: 5000, }, () => { expect(isSecure({ isSecureContext: true })).toBe(true); }); it("returns false when window.isSecureContext is false", { timeout: 5000, }, () => { expect(isSecure({ isSecureContext: false })).toBe(false); }); it("falls back to global window when none supplied", { timeout: 5000, }, () => { // happy-dom global window is a secure context by default expect(isSecure()).toBe(true); }); }); describe("requireSecureContext", () => { it("requireSecureContext throws INSECURE_CONTEXT when window flags insecure", { timeout: 5000, }, () => { let thrown: unknown; try { requireSecureContext({ isSecureContext: false }); } catch (err) { thrown = err; } expect(thrown).toBeInstanceOf(ClipboardError); expect((thrown as ClipboardError).code).toBe( CLIPBOARD_CODES.INSECURE_CONTEXT ); }); it("requireSecureContext returns silently for secure context", { timeout: 5000, }, () => { expect(() => requireSecureContext({ isSecureContext: true }) ).not.toThrow(); }); }); describe("waitForNextFocus", () => { it("resolves false when window has no addEventListener", { timeout: 5000, }, async () => { const resolved = await waitForNextFocus({}, 50); expect(resolved).toBe(false); }); it("resolves true when focus event fires before timeout", { timeout: 5000, }, async () => { const listeners = new Map void>(); const win = { addEventListener: (type: string, cb: () => void) => { listeners.set(type, cb); }, removeEventListener: (type: string) => { listeners.delete(type); }, }; const promise = waitForNextFocus(win, 200); setTimeout(() => { listeners.get("focus")?.(); }, 10); const resolved = await promise; expect(resolved).toBe(true); }); it("resolves false on timeout", { timeout: 5000 }, async () => { const listeners = new Map void>(); const win = { addEventListener: (type: string, cb: () => void) => { listeners.set(type, cb); }, removeEventListener: (type: string) => { listeners.delete(type); }, }; const resolved = await waitForNextFocus(win, 30); expect(resolved).toBe(false); }); it("ignores late focus after timeout fired", { timeout: 5000, }, async () => { const focusListeners: Array<() => void> = []; const win = { addEventListener: (_type: string, cb: () => void) => { focusListeners.push(cb); }, removeEventListener: () => { // Keep listener registered so we can fire it post-timeout to // exercise the settled-guard branch in waitForNextFocus. }, }; const result = await waitForNextFocus(win, 20); expect(result).toBe(false); // Now fire focus AFTER timeout — settled flag must suppress resolve. for (const cb of focusListeners) { cb(); } }); it("timeout firing after focus is a no-op", { timeout: 5000, }, async () => { const focusListeners: Array<() => void> = []; const win = { addEventListener: (_type: string, cb: () => void) => { focusListeners.push(cb); }, removeEventListener: () => {}, }; const promise = waitForNextFocus(win, 30); setTimeout(() => focusListeners[0]?.(), 5); const resolved = await promise; expect(resolved).toBe(true); // Allow the 30ms timeout to fire — it must noop because settled. await new Promise((r) => setTimeout(r, 50)); }); it("ignores duplicate focus events", { timeout: 5000, }, async () => { const focusListeners: Array<() => void> = []; const win = { addEventListener: (_type: string, cb: () => void) => { focusListeners.push(cb); }, removeEventListener: () => {}, }; const promise = waitForNextFocus(win, 200); focusListeners[0]?.(); focusListeners[0]?.(); const resolved = await promise; expect(resolved).toBe(true); }); });