Files
NetMesh/application/state/useMainWindowInputFocusRecovery.test.ts

217 lines
6.8 KiB
TypeScript
Raw Permalink Normal View History

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { startMainWindowInputFocusRecovery } from "./useMainWindowInputFocusRecovery.ts";
const root = fileURLToPath(new URL("../..", import.meta.url));
function readProjectFile(path: string): string {
return readFileSync(join(root, path), "utf8");
}
type Listener = () => void;
function createEventTargetStub() {
const listeners = new Map<string, Set<Listener>>();
return {
addEventListener(eventName: string, listener: Listener) {
const eventListeners = listeners.get(eventName) ?? new Set<Listener>();
eventListeners.add(listener);
listeners.set(eventName, eventListeners);
},
removeEventListener(eventName: string, listener: Listener) {
listeners.get(eventName)?.delete(listener);
},
dispatch(eventName: string) {
for (const listener of listeners.get(eventName) ?? []) {
listener();
}
},
listenerCount(eventName: string) {
return listeners.get(eventName)?.size ?? 0;
},
};
}
test("main-window input focus recovery ignores generic focus and visible visibility changes", () => {
let visibilityState: DocumentVisibilityState = "visible";
let scheduleFocusCalls = 0;
let cancelFocusCalls = 0;
let hiddenCalls = 0;
let shownHandler: Listener | null = null;
let focusRequestedHandler: Listener | null = null;
let willHideHandler: Listener | null = null;
const documentTarget = createEventTargetStub();
const windowTarget = createEventTargetStub();
const cleanup = startMainWindowInputFocusRecovery(
{ onPageHidden: () => { hiddenCalls += 1; } },
{
documentRef: {
...documentTarget,
get visibilityState() {
return visibilityState;
},
},
windowRef: windowTarget,
bridge: {
onWindowShown(cb: Listener) {
shownHandler = cb;
return () => { shownHandler = null; };
},
onWindowFocusRequested(cb: Listener) {
focusRequestedHandler = cb;
return () => { focusRequestedHandler = null; };
},
onWindowWillHide(cb: Listener) {
willHideHandler = cb;
return () => { willHideHandler = null; };
},
},
scheduleFocus: () => {
scheduleFocusCalls += 1;
return { cancel: () => { cancelFocusCalls += 1; } };
},
},
);
assert.equal(windowTarget.listenerCount("focus"), 0);
windowTarget.dispatch("focus");
documentTarget.dispatch("visibilitychange");
assert.equal(scheduleFocusCalls, 0);
assert.equal(hiddenCalls, 0);
shownHandler?.();
assert.equal(scheduleFocusCalls, 0);
focusRequestedHandler?.();
assert.equal(scheduleFocusCalls, 1);
willHideHandler?.();
assert.equal(hiddenCalls, 1);
assert.equal(cancelFocusCalls, 1);
shownHandler?.();
assert.equal(scheduleFocusCalls, 1);
focusRequestedHandler?.();
assert.equal(scheduleFocusCalls, 2);
visibilityState = "hidden";
documentTarget.dispatch("visibilitychange");
assert.equal(hiddenCalls, 2);
assert.equal(cancelFocusCalls, 2);
cleanup();
});
test("main-window input focus recovery retries an explicit foreground request when visibility catches up", () => {
let visibilityState: DocumentVisibilityState = "hidden";
let scheduleFocusCalls = 0;
let hiddenCalls = 0;
let focusRequestedHandler: Listener | null = null;
const documentTarget = createEventTargetStub();
const cleanup = startMainWindowInputFocusRecovery(
{ onPageHidden: () => { hiddenCalls += 1; } },
{
documentRef: {
...documentTarget,
get visibilityState() {
return visibilityState;
},
},
bridge: {
onWindowFocusRequested(cb: Listener) {
focusRequestedHandler = cb;
return () => { focusRequestedHandler = null; };
},
},
scheduleFocus: () => {
scheduleFocusCalls += 1;
return { cancel: () => undefined };
},
},
);
focusRequestedHandler?.();
assert.equal(scheduleFocusCalls, 0);
visibilityState = "visible";
documentTarget.dispatch("visibilitychange");
assert.equal(scheduleFocusCalls, 1);
assert.equal(hiddenCalls, 0);
documentTarget.dispatch("visibilitychange");
assert.equal(scheduleFocusCalls, 1);
cleanup();
});
test("useMainWindowInputFocusRecovery restores input focus only after explicit foreground IPC", () => {
const source = readProjectFile("application/state/useMainWindowInputFocusRecovery.ts");
assert.match(source, /visibilityState !== "visible"/);
assert.match(source, /scheduleFocus\(\)/);
assert.match(source, /onWindowFocusRequested\?\.\(\(\) => \{\s*pendingExplicitFocusRecovery = true;\s*recoverFocus\(\);\s*\}\)/);
assert.doesNotMatch(source, /onWindowShown\?\.\(\(\) => \{[\s\S]*recoverFocus\(\)/);
assert.doesNotMatch(source, /window\.addEventListener\("focus"/);
assert.doesNotMatch(source, /window\.removeEventListener\("focus"/);
});
test("useMainWindowInputFocusRecovery uses visibility changes only to dismiss transient UI", () => {
const source = readProjectFile("application/state/useMainWindowInputFocusRecovery.ts");
const handlerStart = source.indexOf("const onVisibilityChange");
const handlerEnd = source.indexOf('addEventListener("visibilitychange"', handlerStart);
const visibilityHandler = source.slice(handlerStart, handlerEnd);
assert.match(source, /visibilityState === "hidden"/);
assert.match(source, /addEventListener\("visibilitychange", onVisibilityChange\)/);
assert.match(source, /onPageHidden\?\.\(\)/);
assert.match(source, /onWindowWillHide/);
assert.match(source, /cancelPendingFocusRecovery/);
assert.match(visibilityHandler, /dismissTransientUi\(\)/);
assert.doesNotMatch(visibilityHandler, /else\s*\{\s*recoverFocus\(\);\s*\}/);
assert.doesNotMatch(visibilityHandler, /visibilityState !== "hidden"[\s\S]*recoverFocus\(\)/);
});
test("scheduleWindowInputFocus skips deferred focus when the page is hidden", () => {
const source = readProjectFile("application/state/windowInputFocus.ts");
assert.match(source, /document\.visibilityState !== "visible"/);
assert.match(source, /cancelAnimationFrame/);
assert.match(source, /clearTimeout/);
});
test("AppView mounts main-window input focus recovery with overlay dismiss", () => {
const source = readProjectFile("application/app/AppView.tsx");
assert.match(source, /useMainWindowInputFocusRecovery\(\{ onPageHidden: dismissTransientOverlays \}\)/);
assert.match(source, /setIsQuickSwitcherOpen\(false\)/);
assert.match(source, /setProtocolSelectHost\(null\)/);
});
test("dropdown closes when the document becomes hidden", () => {
const source = readProjectFile("components/ui/dropdown.tsx");
assert.match(source, /document\.visibilityState === "hidden"/);
assert.match(source, /setOpen\(false\)/);
});