1import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
2import { reactHookHarness as hooks } from "../../test/reactHookHarness";
3import { usePermissionStatus } from "./usePermissionStatus";
4
5const effects = vi.hoisted(() => [] as Array<() => (() => void) | undefined>);
6vi.mock("react", async (original) => {
7 const actual = await original<typeof import("react")>();
8 const { reactHookHarness } = await import("../../test/reactHookHarness");
9 return {
10 ...actual,
11 useState: reactHookHarness.useState,
12 useEffect: (effect: () => (() => void) | undefined) => effects.push(effect),
13 useEffectEvent: <T>(callback: T) => callback,
14 };
15});
16vi.mock("react/compiler-runtime", async () => {
17 const { reactHookHarness } = await import("../../test/reactHookHarness");
18 return { c: reactHookHarness.useMemoCache };
19});
20const check = vi.fn<() => Promise<{ screen: boolean; accessibility: boolean }>>();
21let cleanup: (() => void) | undefined;
22let page: EventTarget & { visibilityState: string };
23const render = () => {
24 hooks.beginRender();
25 return usePermissionStatus(check, { screen: false, accessibility: false });
26};
27beforeEach(() => {
28 hooks.reset();
29 effects.length = 0;
30 vi.useFakeTimers();
31 page = Object.assign(new EventTarget(), { visibilityState: "visible" });
32 vi.stubGlobal("document", page);
33 vi.stubGlobal("window", Object.assign(new EventTarget(), { setInterval, clearInterval }));
34 check.mockReset().mockResolvedValue({ screen: false, accessibility: false });
35});
36afterEach(() => {
37 cleanup?.();
38 vi.useRealTimers();
39 vi.unstubAllGlobals();
40});
41const start = async () => {
42 render();
43 cleanup = effects[0]!();
44 await Promise.resolve();
45};
46
47it("unlocks Continue only for required grants and relocks on revocation", async () => {
48 await start();
49 expect(render().isReady(["screen"])).toBe(false);
50 check.mockResolvedValue({ screen: true, accessibility: false });
51 await vi.advanceTimersByTimeAsync(1500);
52 expect(render().isReady(["screen"])).toBe(true);
53 expect(render().isReady(["screen", "accessibility"])).toBe(false);
54 check.mockResolvedValue({ screen: true, accessibility: true });
55 await vi.advanceTimersByTimeAsync(1500);
56 expect(render().isReady(["screen", "accessibility"])).toBe(true);
57 check.mockResolvedValue({ screen: false, accessibility: true });
58 window.dispatchEvent(new Event("focus"));
59 await Promise.resolve();
60 expect(render().isReady(["screen"])).toBe(false);
61});
62
63it("does not overlap checks and discards completion after closing", async () => {
64 let resolve!: (status: { screen: boolean; accessibility: boolean }) => void;
65 const promise = new Promise<{ screen: boolean; accessibility: boolean }>((done) => {
66 resolve = done;
67 });
68 const pending = { promise, resolve };
69 check.mockReturnValue(pending.promise);
70 await start();
71 await vi.advanceTimersByTimeAsync(4500);
72 window.dispatchEvent(new Event("focus"));
73 expect(check).toHaveBeenCalledTimes(1);
74 cleanup?.();
75 pending.resolve({ screen: true, accessibility: true });
76 await pending.promise;
77 expect(render().isReady(["screen"])).toBe(false);
78 expect(vi.getTimerCount()).toBe(0);
79});
80
81it("pauses in the background and blocks stale grants after a check failure", async () => {
82 check.mockResolvedValue({ screen: true, accessibility: true });
83 await start();
84 expect(render().isReady(["screen"])).toBe(true);
85 page.visibilityState = "hidden";
86 await vi.advanceTimersByTimeAsync(3000);
87 expect(check).toHaveBeenCalledTimes(1);
88 check.mockRejectedValue(new Error("IPC unavailable"));
89 page.visibilityState = "visible";
90 page.dispatchEvent(new Event("visibilitychange"));
91 await Promise.resolve();
92 expect(render().isReady(["screen"])).toBe(false);
93 check.mockResolvedValue({ screen: true, accessibility: true });
94 await vi.advanceTimersByTimeAsync(1500);
95 expect(render().isReady(["screen"])).toBe(true);
96});