diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ab4e1ed382dab..1b3bb0e32bb9c 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..8a080183b3830 --- /dev/null +++ b/apps/desktop/src/preview/PickPreload.test.ts @@ -0,0 +1,64 @@ +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>>()); + +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 = (): ShadowRoot => { + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + disconnect() {} + }, + ); + 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", () => { + const shadowRoot = 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"); +}); + +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"); + + 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 dcb16b851dc23..95aa3066f0952 100644 --- a/apps/desktop/src/preview/PickPreload.ts +++ b/apps/desktop/src/preview/PickPreload.ts @@ -1652,13 +1652,13 @@ function startAnnotation( return; } } - 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"; diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index ba37576815097..4bb1e4541dd94 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) => @@ -151,13 +152,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 e7265664f0a44..928618460b79c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -489,6 +489,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