Initial Commit
This commit is contained in:
158
packages/foundation/data/clipboard/src/security/security.test.ts
Normal file
158
packages/foundation/data/clipboard/src/security/security.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user