159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
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<string, () => 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<string, () => 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<void>((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);
|
|
});
|
|
});
|