From 4d3a0b4c6a15de8ecf5c6e685b3c2dc8c15adee6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 21:01:53 +0000 Subject: [PATCH 1/3] test(desktop): reproduce #7700, annotation tool keys ignored after clicking a toolbar button Adds a jsdom test project for the desktop package and a PickPreload test that starts an annotation, clicks the Region toolbar button, then presses V. On main the tool stays on Region because the window keydown handler ignores every key whose target is inside the overlay host. These tests reproduce #7700 and fail until it is fixed. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/package.json | 1 + apps/desktop/src/preview/PickPreload.test.ts | 53 ++++++++++++++++++++ apps/desktop/vite.config.ts | 28 +++++++++-- pnpm-lock.yaml | 3 ++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/preview/PickPreload.test.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 728ab36f7fcb2..6edb576f0c864 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -37,6 +37,7 @@ "acorn": "8.16.0", "cross-env": "^10.1.0", "electron-builder": "26.16.1", + "jsdom": "^30.0.1", "tailwindcss": "^4.0.0", "vite-plus": "catalog:" }, diff --git a/apps/desktop/src/preview/PickPreload.test.ts b/apps/desktop/src/preview/PickPreload.test.ts new file mode 100644 index 0000000000000..3c8aa0e473bd7 --- /dev/null +++ b/apps/desktop/src/preview/PickPreload.test.ts @@ -0,0 +1,53 @@ +import { expect, it, vi } from "vite-plus/test"; + +import { START_PICK_CHANNEL } from "./GuestProtocol.ts"; + +const ipcListeners = vi.hoisted(() => new Map void>>()); + +vi.mock("electron", () => ({ + ipcRenderer: { + on: (channel: string, listener: (...args: unknown[]) => void) => { + ipcListeners.set(channel, [...(ipcListeners.get(channel) ?? []), listener]); + }, + off: () => {}, + send: () => {}, + invoke: async () => undefined, + }, +})); + +vi.mock("react-grab/primitives", () => ({ getElementContext: async () => null })); + +const startAnnotation = async (): Promise => { + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + disconnect() {} + }, + ); + await import("./PickPreload.ts"); + using attachShadow = vi.spyOn(Element.prototype, "attachShadow"); + for (const listener of ipcListeners.get(START_PICK_CHANNEL) ?? []) listener({}, undefined, false); + const shadowRoot = attachShadow.mock.results[0]?.value; + if (!(shadowRoot instanceof ShadowRoot)) throw new Error("annotation overlay did not start"); + return shadowRoot; +}; + +it("switches tools from the keyboard after a toolbar button is clicked", async () => { + const shadowRoot = await startAnnotation(); + const region = Array.from(shadowRoot.querySelectorAll("button")).find( + (button) => button.textContent === "Region", + ); + if (!region) throw new Error("Region toolbar button not found"); + + region.focus(); + region.click(); + expect(document.documentElement.getAttribute("data-t3code-annotation-tool")).toBe("marquee"); + + region.dispatchEvent(new KeyboardEvent("keydown", { key: "v", bubbles: true, composed: true })); + + expect( + document.documentElement.getAttribute("data-t3code-annotation-tool"), + "V should switch to Select while focus is on a toolbar button", + ).toBe("select"); +}); diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 55c3aa03b6490..bde058ca1b140 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -5,6 +5,7 @@ import { isDesktopRuntimeExternalDependency } from "../../scripts/lib/desktop-ex import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; const repoEnv = loadRepoEnv(); +const domTests = ["src/preview/PickPreload.test.ts"]; // Install only bundle externals; the server bundle already carries its dependency tree. const isMainProcessExternal = (id: string) => @@ -142,13 +143,32 @@ export default defineConfig({ }, ], test: { - include: [...defaultInclude, "../../native/kde-snap-shot/src/*.test.ts"], // The Windows lane runs workspace suites concurrently; filesystem-heavy // desktop integration tests can exceed Vitest's 5 second default there. testTimeout: 15_000, - setupFiles: [ - "../../packages/shared/src/testing/longTempDir.ts", - "../../packages/shared/src/testing/providerEnv.ts", + projects: [ + { + extends: true, + test: { + name: "unit", + include: [...defaultInclude, "../../native/kde-snap-shot/src/*.test.ts"], + exclude: ["**/node_modules/**", ...domTests], + setupFiles: [ + "../../packages/shared/src/testing/longTempDir.ts", + "../../packages/shared/src/testing/providerEnv.ts", + ], + }, + }, + { + extends: true, + test: { + name: "dom", + include: domTests, + // Guest preloads run in the page; these tests drive them against a DOM. + environment: "jsdom", + setupFiles: ["../../packages/shared/src/testing/providerEnv.ts"], + }, + }, ], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ceba3cbcfd83..bda017169f485 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -490,6 +490,9 @@ importers: electron-builder: specifier: 26.16.1 version: 26.16.1(electron-builder-squirrel-windows@26.16.1)(supports-color@8.1.1) + jsdom: + specifier: ^30.0.1 + version: 30.1.0(@noble/hashes@1.8.0) tailwindcss: specifier: 4.3.3 version: 4.3.3 From 1667f6fbf15e0b2e5a8269ecdbdc00f793017bab Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 21:04:36 +0000 Subject: [PATCH 2/3] fix(desktop): annotation tool keys work after clicking a toolbar button The annotation overlay ignored every non-Escape key whose target was inside the overlay host, so V/R/D/E did nothing once a toolbar button held focus. Skip tool keys only when an input, textarea or select inside the overlay's shadow root has focus. Fixes pingdotgg/t3code#7700 Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/preview/PickPreload.test.ts | 11 +++++++++++ apps/desktop/src/preview/PickPreload.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/preview/PickPreload.test.ts b/apps/desktop/src/preview/PickPreload.test.ts index 3c8aa0e473bd7..4f3c9c193c347 100644 --- a/apps/desktop/src/preview/PickPreload.test.ts +++ b/apps/desktop/src/preview/PickPreload.test.ts @@ -51,3 +51,14 @@ it("switches tools from the keyboard after a toolbar button is clicked", async ( "V should switch to Select while focus is on a toolbar button", ).toBe("select"); }); + +it("keeps the tool while typing a tool key in the comment", async () => { + const shadowRoot = await startAnnotation(); + const comment = shadowRoot.querySelector("textarea"); + if (!comment) throw new Error("comment textarea not found"); + + comment.focus(); + comment.dispatchEvent(new KeyboardEvent("keydown", { key: "r", bubbles: true, composed: true })); + + expect(document.documentElement.getAttribute("data-t3code-annotation-tool")).toBe("select"); +}); diff --git a/apps/desktop/src/preview/PickPreload.ts b/apps/desktop/src/preview/PickPreload.ts index 4cb5987cdeb85..07f1f9c308713 100644 --- a/apps/desktop/src/preview/PickPreload.ts +++ b/apps/desktop/src/preview/PickPreload.ts @@ -1365,13 +1365,13 @@ function startAnnotation(sendEnabled: boolean): void { const onCancel = (): void => teardown(false); const onCaptured = (): void => teardown(false); const onKeyDown = (event: KeyboardEvent): void => { - if (isAnnotationNode(event.target as Element) && event.key !== "Escape") return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); teardown(true); return; } + if (shadowRoot.activeElement?.matches("input, textarea, select")) return; if (event.key === "v") tool = "select"; else if (event.key === "r") tool = "marquee"; else if (event.key === "d") tool = "draw"; From 7787049cdaa20f9cd76e3a4714b91ea9e0dd6c2e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 00:08:31 +0000 Subject: [PATCH 3/3] test(desktop): load the annotation preload at collection, not inside each test A cold transform of the preload under heavy load exceeded the 15s test timeout when it ran inside the test body. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/preview/PickPreload.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/preview/PickPreload.test.ts b/apps/desktop/src/preview/PickPreload.test.ts index 4f3c9c193c347..8a080183b3830 100644 --- a/apps/desktop/src/preview/PickPreload.test.ts +++ b/apps/desktop/src/preview/PickPreload.test.ts @@ -1,6 +1,7 @@ import { expect, it, vi } from "vite-plus/test"; import { START_PICK_CHANNEL } from "./GuestProtocol.ts"; +import "./PickPreload.ts"; const ipcListeners = vi.hoisted(() => new Map void>>()); @@ -17,7 +18,7 @@ vi.mock("electron", () => ({ vi.mock("react-grab/primitives", () => ({ getElementContext: async () => null })); -const startAnnotation = async (): Promise => { +const startAnnotation = (): ShadowRoot => { vi.stubGlobal( "ResizeObserver", class { @@ -25,7 +26,6 @@ const startAnnotation = async (): Promise => { disconnect() {} }, ); - await import("./PickPreload.ts"); using attachShadow = vi.spyOn(Element.prototype, "attachShadow"); for (const listener of ipcListeners.get(START_PICK_CHANNEL) ?? []) listener({}, undefined, false); const shadowRoot = attachShadow.mock.results[0]?.value; @@ -33,8 +33,8 @@ const startAnnotation = async (): Promise => { return shadowRoot; }; -it("switches tools from the keyboard after a toolbar button is clicked", async () => { - const shadowRoot = await startAnnotation(); +it("switches tools from the keyboard after a toolbar button is clicked", () => { + const shadowRoot = startAnnotation(); const region = Array.from(shadowRoot.querySelectorAll("button")).find( (button) => button.textContent === "Region", ); @@ -52,8 +52,8 @@ it("switches tools from the keyboard after a toolbar button is clicked", async ( ).toBe("select"); }); -it("keeps the tool while typing a tool key in the comment", async () => { - const shadowRoot = await startAnnotation(); +it("keeps the tool while typing a tool key in the comment", () => { + const shadowRoot = startAnnotation(); const comment = shadowRoot.querySelector("textarea"); if (!comment) throw new Error("comment textarea not found");