From 43aa339e89442980cbd8eb133ac9abbb4d5b13c5 Mon Sep 17 00:00:00 2001 From: Duncan Date: Tue, 18 Aug 2026 17:44:46 -0400 Subject: [PATCH] test(desktop): cover deep-link focus retry and env-key expansion Exercise useAgentEditDeepLinkFocus directly against the two scenario-8 blockers: the model combobox must stay unfocused while disabled during discovery and focus once it resolves; an env_key target must expand the Advanced accordion; and onOpenAutoFocus must defer to Radix while the target is unfocusable and claim focus once it is. Each assertion goes red when its fix is reverted. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/agentEditDeepLinkFocus.test.mjs | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 desktop/src/features/agents/ui/agentEditDeepLinkFocus.test.mjs diff --git a/desktop/src/features/agents/ui/agentEditDeepLinkFocus.test.mjs b/desktop/src/features/agents/ui/agentEditDeepLinkFocus.test.mjs new file mode 100644 index 0000000000..338e7db2f0 --- /dev/null +++ b/desktop/src/features/agents/ui/agentEditDeepLinkFocus.test.mjs @@ -0,0 +1,193 @@ +/** + * Regression tests for useAgentEditDeepLinkFocus — the two deep-link focus + * blockers that failed scenario 8, exercised against the real hook. + * + * Blocker 2 (model): the provider/model combobox trigger is `disabled` while + * model discovery is in flight. The one-shot focus effect must NOT latch or + * fire against a disabled control; it must retry and land focus once discovery + * resolves (modelDiscoveryLoading flips false and the control loses `disabled`). + * + * Blocker 1 (env_key): env_key targets live inside the Advanced accordion, + * which seeds collapsed. The hook must expand Advanced (setShowAdvancedFields) + * when an env_key target is requested so EnvVarsEditor can mount and focus. + * + * Autofocus handler: with a normalized target that is not yet focusable, the + * returned onOpenAutoFocus must NOT preventDefault (so Radix supplies a baseline + * and focus is never stranded on ); once the target is focusable it must + * preventDefault and claim focus. + */ + +import assert from "node:assert/strict"; +import { afterEach, test } from "node:test"; +import { JSDOM } from "jsdom"; + +const dom = new JSDOM("", { + url: "http://localhost", +}); +Object.assign(globalThis, { + document: dom.window.document, + HTMLElement: dom.window.HTMLElement, + Node: dom.window.Node, + IS_REACT_ACT_ENVIRONMENT: true, + window: dom.window, +}); +// Real rAF (macrotask-backed): the focus effect schedules the actual focus() +// call in a rAF, so a no-op rAF would swallow it. +globalThis.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 0); +globalThis.cancelAnimationFrame = (id) => clearTimeout(id); + +/** Insert a focusable control with the given id; optionally disabled. */ +function mountTarget(id, { disabled = false } = {}) { + const el = dom.window.document.createElement("button"); + el.id = id; + if (disabled) el.setAttribute("disabled", ""); + dom.window.document.body.appendChild(el); + let focusCalls = 0; + el.focus = () => { + focusCalls++; + }; + el.scrollIntoView = () => {}; + return { + el, + get focusCalls() { + return focusCalls; + }, + }; +} + +afterEach(() => { + dom.window.document.body.innerHTML = ""; +}); + +async function importHook() { + return import("./useAgentEditDeepLinkFocus.ts"); +} + +async function flushRaf(act) { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 5)); + }); +} + +test("model target stays unfocused while disabled, then focuses once discovery resolves", async () => { + const { act, cleanup, renderHook } = await import("@testing-library/react"); + const { useAgentEditDeepLinkFocus } = await importHook(); + const model = mountTarget("edit-agent-model", { disabled: true }); + + try { + const { rerender } = renderHook( + (props) => useAgentEditDeepLinkFocus(props), + { + initialProps: { + open: true, + initialFocus: { type: "normalized_field", field: "model" }, + contextKey: "pk:", + llmProviderFieldVisible: true, + modelDiscoveryLoading: true, + setShowAdvancedFields: () => {}, + }, + }, + ); + await flushRaf(act); + assert.equal( + model.focusCalls, + 0, + "must not focus the model combobox while it is disabled during discovery", + ); + + // Discovery resolves: control becomes focusable and the loading signal flips. + model.el.removeAttribute("disabled"); + rerender({ + open: true, + initialFocus: { type: "normalized_field", field: "model" }, + contextKey: "pk:", + llmProviderFieldVisible: true, + modelDiscoveryLoading: false, + setShowAdvancedFields: () => {}, + }); + await flushRaf(act); + assert.equal( + model.focusCalls, + 1, + "must focus the model combobox exactly once after discovery resolves", + ); + } finally { + cleanup(); + } +}); + +test("env_key target expands the Advanced accordion", async () => { + const { act, cleanup, renderHook } = await import("@testing-library/react"); + const { useAgentEditDeepLinkFocus } = await importHook(); + const expanded = []; + + try { + renderHook(() => + useAgentEditDeepLinkFocus({ + open: true, + initialFocus: { type: "env_key", key: "ANTHROPIC_API_KEY" }, + contextKey: "pk:", + llmProviderFieldVisible: true, + modelDiscoveryLoading: false, + setShowAdvancedFields: (next) => expanded.push(next), + }), + ); + await act(async () => { + await Promise.resolve(); + }); + assert.deepEqual( + expanded, + [true], + "env_key target must expand Advanced so EnvVarsEditor mounts", + ); + } finally { + cleanup(); + } +}); + +test("onOpenAutoFocus defers to Radix while the target is disabled and claims it once focusable", async () => { + const { cleanup, renderHook } = await import("@testing-library/react"); + const { useAgentEditDeepLinkFocus } = await importHook(); + const model = mountTarget("edit-agent-model", { disabled: true }); + + try { + const { result } = renderHook(() => + useAgentEditDeepLinkFocus({ + open: true, + initialFocus: { type: "normalized_field", field: "model" }, + contextKey: "pk:", + llmProviderFieldVisible: true, + modelDiscoveryLoading: true, + setShowAdvancedFields: () => {}, + }), + ); + + let prevented = false; + const disabledEvent = { preventDefault: () => (prevented = true) }; + result.current.onOpenAutoFocus(disabledEvent); + assert.equal( + prevented, + false, + "must not suppress Radix autofocus while the target is disabled (else focus strands on )", + ); + assert.equal(model.focusCalls, 0, "must not focus a disabled target"); + + model.el.removeAttribute("disabled"); + prevented = false; + result.current.onOpenAutoFocus({ + preventDefault: () => (prevented = true), + }); + assert.equal( + prevented, + true, + "must suppress Radix autofocus once the target is focusable", + ); + assert.equal( + model.focusCalls, + 1, + "must claim focus on the focusable target", + ); + } finally { + cleanup(); + } +});