import { describe, expect, test } from "bun:test"; import { cancelSessionRowRevealIntent, createSessionFocusCompletion, clearSessionComposerFocusIntent, consumeSessionComposerFocusIntent, consumeSessionRowRevealIntent, focusSessionComposerWhenReady, requestSessionComposerFocus, sessionComposerFocusIntentIsEligible, shouldFocusSessionComposer, shouldMoveSessionRowFocus, shouldRecordSessionRowFocusIntent, shouldRestoreSessionFocus, } from "./session-focus"; const SESSION_ID = "session-26"; function fakeElement( attributes: Record = {}, options: { connected?: boolean | undefined; closestSessionMenu?: string | null | undefined; closestSessionRow?: string | null | undefined; } = {}, ): HTMLElement { const connected = options.connected ?? true; const menu = options.closestSessionMenu === undefined ? null : fakeElement({ "data-session-menu": options.closestSessionMenu }); const row = options.closestSessionRow === undefined ? null : fakeElement({ "data-session-row": options.closestSessionRow }); return { isConnected: connected, getAttribute: (name: string) => attributes[name] ?? null, closest: (selector: string) => (selector === "[data-session-row]" ? row : menu), } as unknown as HTMLElement; } describe("session pin focus restoration", () => { test("records and consumes one exact session-composer navigation intent", () => { clearSessionComposerFocusIntent(); const app = new EventTarget(); const otherApp = new EventTarget(); const signalled: string[] = []; app.addEventListener("opengeni:focus-session-composer", () => signalled.push("app")); otherApp.addEventListener("opengeni:focus-session-composer", () => signalled.push("other")); requestSessionComposerFocus(app, "workspace-a", SESSION_ID); expect(signalled).toEqual(["app"]); expect(consumeSessionComposerFocusIntent(otherApp, "workspace-a", SESSION_ID)).toBeNull(); expect(consumeSessionComposerFocusIntent(app, "workspace-a", "other-session")).toBeNull(); expect(consumeSessionComposerFocusIntent(app, "workspace-a", SESSION_ID)).toMatchObject({ workspaceId: "workspace-a", sessionId: SESSION_ID, }); expect(consumeSessionComposerFocusIntent(app, "workspace-a", SESSION_ID)).toBeNull(); }); test("allows the explicit row click to hand focus to chat, but no dialog or other control", () => { const body = fakeElement(); expect( shouldFocusSessionComposer( fakeElement({}, { closestSessionRow: SESSION_ID }), SESSION_ID, body, false, ), ).toBe(true); expect(shouldFocusSessionComposer(body, SESSION_ID, body, false)).toBe(true); expect( shouldFocusSessionComposer( fakeElement({}, { closestSessionRow: "other-session" }), SESSION_ID, body, false, ), ).toBe(false); expect( shouldFocusSessionComposer( fakeElement({}, { closestSessionRow: SESSION_ID }), SESSION_ID, body, true, ), ).toBe(false); }); test("limits rail-click composer focus to an actionable desktop session", () => { const eligible = { viewportWidth: 1280, coarsePointer: false, terminal: false, requiresAction: false, pendingHumanInput: false, pendingApproval: false, }; expect(sessionComposerFocusIntentIsEligible(eligible)).toBe(true); expect(sessionComposerFocusIntentIsEligible({ ...eligible, viewportWidth: 900 })).toBe(false); expect(sessionComposerFocusIntentIsEligible({ ...eligible, coarsePointer: true })).toBe(false); expect(sessionComposerFocusIntentIsEligible({ ...eligible, terminal: true })).toBe(false); expect(sessionComposerFocusIntentIsEligible({ ...eligible, requiresAction: true })).toBe(false); expect(sessionComposerFocusIntentIsEligible({ ...eligible, pendingHumanInput: true })).toBe( false, ); expect(sessionComposerFocusIntentIsEligible({ ...eligible, pendingApproval: true })).toBe( false, ); }); test("consumes one explicit row reveal without turning repeated data churn into scrolling", () => { let scrollCalls = 0; const rows = Array.from({ length: 3_800 }, (_, index) => ({ dataset: { sessionRow: `session-${index}` }, scrollIntoView: () => { scrollCalls += 1; }, })) as unknown as HTMLElement[]; const root = { querySelectorAll: () => rows, } as unknown as HTMLElement; const intent = { current: "session-2_400" }; // A target that is not mounted remains pending without moving the rail. expect(consumeSessionRowRevealIntent(root, intent)).toBeNull(); expect(scrollCalls).toBe(0); intent.current = "session-2400"; expect(consumeSessionRowRevealIntent(root, intent)).toBe(rows[2400]!); expect(intent.current).toBeNull(); expect(scrollCalls).toBe(1); // Polls, title/status churn, pin reconciliation, and page merges may all // rerun the effect; no new explicit intent means the offset stays owned by // the reader. for (let update = 0; update < 1_000; update += 1) { expect(consumeSessionRowRevealIntent(root, intent)).toBeNull(); } expect(scrollCalls).toBe(1); }); test("lets manual scrolling cancel a reveal before its row mounts", () => { let scrollCalls = 0; const root = { querySelectorAll: () => [ { dataset: { sessionRow: "session-late" }, scrollIntoView: () => { scrollCalls += 1; }, }, ], } as unknown as HTMLElement; const intent = { current: "session-late" as string | null }; cancelSessionRowRevealIntent(intent); expect(consumeSessionRowRevealIntent(root, intent)).toBeNull(); expect(scrollCalls).toBe(0); }); test("does not retain intent for boundary navigation that is already current", () => { expect(shouldRecordSessionRowFocusIntent(0, 0)).toBe(false); expect(shouldRecordSessionRowFocusIntent(2, 2)).toBe(false); expect(shouldRecordSessionRowFocusIntent(1, 0)).toBe(true); expect(shouldRecordSessionRowFocusIntent(null, 0)).toBe(false); }); test("does not turn a pin or project remount reorder into a roving row-focus request", () => { // The actions trigger is restored by the group-changing operation after // its row remount. The subsequent focus-index effect has no keyboard intent // and must leave that restored destination alone. expect(shouldMoveSessionRowFocus(null, SESSION_ID)).toBe(false); expect(shouldMoveSessionRowFocus(SESSION_ID, SESSION_ID)).toBe(true); expect(shouldMoveSessionRowFocus(SESSION_ID, "other-session")).toBe(false); }); test("recognizes focus lost to body, a disconnected node, or the same row's other target", () => { const destination = fakeElement({ "data-session-row": SESSION_ID }); const body = fakeElement(); expect(shouldRestoreSessionFocus(null, destination, SESSION_ID, body)).toBe(true); expect( shouldRestoreSessionFocus( fakeElement({}, { connected: false }), destination, SESSION_ID, body, ), ).toBe(true); expect( shouldRestoreSessionFocus( fakeElement({ "data-session-actions": SESSION_ID }), destination, SESSION_ID, body, ), ).toBe(true); }); test("recognizes the operation's Radix menu and focus guard", () => { const destination = fakeElement({ "data-session-actions": SESSION_ID }); const body = fakeElement(); expect( shouldRestoreSessionFocus( fakeElement({}, { closestSessionMenu: SESSION_ID }), destination, SESSION_ID, body, ), ).toBe(true); expect( shouldRestoreSessionFocus( fakeElement({ "data-radix-focus-guard": "" }), destination, SESSION_ID, body, ), ).toBe(true); }); test("does not steal unrelated focus and does not refocus the existing destination", () => { const destination = fakeElement({ "data-session-actions": SESSION_ID }); const body = fakeElement(); expect( shouldRestoreSessionFocus( fakeElement({ "data-session-row": "other-session" }), destination, SESSION_ID, body, ), ).toBe(false); expect(shouldRestoreSessionFocus(destination, destination, SESSION_ID, body)).toBe(false); expect( shouldRestoreSessionFocus( fakeElement({}, { closestSessionMenu: "other-session" }), destination, SESSION_ID, body, ), ).toBe(false); }); test("rejects a destination that was removed during rollback", () => { expect( shouldRestoreSessionFocus( null, fakeElement({ "data-session-row": SESSION_ID }, { connected: false }), SESSION_ID, null, ), ).toBe(false); }); }); describe("deferred session focus completion", () => { test("retains a settled request until its rollback layout commits", () => { const pending: { current: { operation: number; settled: boolean } | null } = { current: { operation: 1, settled: false }, }; let committedRow = "optimistic Pin"; const restored: string[] = []; const oldFrame = createSessionFocusCompletion(pending, () => restored.push(committedRow)); // The request settles before React commits its queued rollback. The old // layout frame still targets the optimistic row, which will be removed. pending.current!.settled = true; oldFrame(); expect(restored).toEqual(["optimistic Pin"]); expect(pending.current).toEqual({ operation: 1, settled: true }); committedRow = "rolled-back Unpin"; const committedFrame = createSessionFocusCompletion(pending, () => restored.push(committedRow)); committedFrame(); expect(restored).toEqual(["optimistic Pin", "rolled-back Unpin"]); expect(pending.current).toBeNull(); }); test("an older committed frame cannot clear a newer focus operation", () => { const pending: { current: { operation: number; settled: boolean } | null } = { current: { operation: 1, settled: true }, }; const oldFrame = createSessionFocusCompletion(pending, () => {}); pending.current = { operation: 2, settled: true }; oldFrame(); expect(pending.current).toEqual({ operation: 2, settled: true }); }); }); describe("composer focus when a chat opens", () => { test("a keyboard jump replaces the previous chat's focus; a load only fills empty focus", () => { const body = fakeElement(); const elsewhere = fakeElement({}, { closestSessionRow: "other-session" }); expect(shouldFocusSessionComposer(elsewhere, SESSION_ID, body, false, "keyboard")).toBe(true); expect(shouldFocusSessionComposer(elsewhere, SESSION_ID, body, false, "load")).toBe(false); expect(shouldFocusSessionComposer(body, SESSION_ID, body, false, "load")).toBe(true); expect(shouldFocusSessionComposer(null, SESSION_ID, body, false, "load")).toBe(true); expect(shouldFocusSessionComposer(elsewhere, SESSION_ID, body, true, "keyboard")).toBe(false); }); test("keyboard intents carry their source", () => { clearSessionComposerFocusIntent(); const app = new EventTarget(); requestSessionComposerFocus(app, "workspace-a", SESSION_ID, "keyboard"); expect(consumeSessionComposerFocusIntent(app, "workspace-a", SESSION_ID)).toMatchObject({ source: "keyboard", }); requestSessionComposerFocus(app, "workspace-a", SESSION_ID); expect(consumeSessionComposerFocusIntent(app, "workspace-a", SESSION_ID)).toMatchObject({ source: "rail", }); }); function harness() { let clock = 0; const frames: Array<() => void> = []; const state = { dialog: true as boolean, textarea: null as { disabled: boolean; focused: number } | null, active: null as unknown, }; const body = fakeElement(); const doc = { body, get activeElement() { return state.active ?? body; }, querySelector: () => (state.dialog ? {} : null), } as unknown as Document; const textareaFor = () => { const current = state.textarea; if (!current) return null; return { disabled: current.disabled, focus: () => { current.focused += 1; state.active = textareaFor(); }, } as unknown as HTMLTextAreaElement; }; const run = (source: "keyboard" | "load" | "rail") => focusSessionComposerWhenReady({ textarea: textareaFor, sessionId: SESSION_ID, source, doc, now: () => clock, requestFrame: (callback) => frames.push(callback), cancelFrame: () => {}, }); const tick = (ms = 16) => { clock += ms; const callback = frames.shift(); callback?.(); }; return { state, run, tick, frames, body }; } test("waits for the command menu to close and the composer to enable, then focuses once", () => { const { state, run, tick } = harness(); run("keyboard"); tick(); state.textarea = { disabled: true, focused: 0 }; tick(); expect(state.textarea.focused).toBe(0); state.dialog = false; tick(); expect(state.textarea.focused).toBe(0); state.textarea.disabled = false; tick(); expect(state.textarea.focused).toBe(1); }); test("gives up after a few seconds and when cancelled", () => { const slow = harness(); slow.run("keyboard"); slow.tick(3_500); slow.state.dialog = false; slow.state.textarea = { disabled: false, focused: 0 }; slow.tick(); expect(slow.state.textarea.focused).toBe(0); const cancelled = harness(); const cancel = cancelled.run("keyboard"); cancel(); cancelled.state.dialog = false; cancelled.state.textarea = { disabled: false, focused: 0 }; cancelled.tick(); expect(cancelled.state.textarea.focused).toBe(0); }); test("a load leaves focus the person already placed elsewhere", () => { const { state, run, tick } = harness(); state.dialog = false; state.textarea = { disabled: false, focused: 0 }; state.active = fakeElement({}, { closestSessionRow: "other-session" }); run("load"); tick(); expect(state.textarea.focused).toBe(0); }); });