[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
216
application/state/useMainWindowInputFocusRecovery.test.ts
Normal file
216
application/state/useMainWindowInputFocusRecovery.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
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\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user