From 71e4133d57afba329bacdd04d7a9c4a2bc525b38 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Thu, 23 Apr 2026 10:38:35 -0400 Subject: [PATCH] fix(ai-toolbar): finalize normalized clipboard test coverage --- src/components/ai-toolbar.tsx | 14 ++++++------ tests/ai-toolbar-paste.test.ts | 28 ++++++++++++------------ tests/ai-toolbar-state.test.ts | 10 +++++++-- tests/helpers/ai-toolbar-test-helpers.ts | 15 +++++++++---- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/components/ai-toolbar.tsx b/src/components/ai-toolbar.tsx index 711ac3d..73bb393 100644 --- a/src/components/ai-toolbar.tsx +++ b/src/components/ai-toolbar.tsx @@ -95,10 +95,11 @@ function ShortcutsList({ os }: { os: Os }) { function isEditableTarget(target: EventTarget | null): target is HTMLElement { const element = target as HTMLElement | null; - return !!element && ( - element.tagName === "TEXTAREA" || - element.tagName === "INPUT" || - element.isContentEditable + return ( + !!element && + (element.tagName === "TEXTAREA" || + element.tagName === "INPUT" || + element.isContentEditable) ); } @@ -187,8 +188,7 @@ export const AIToolbar = ({ // focused element or OS clipboard model (X11/Wayland). // This is the approach used by Excalidraw's actionPaste. useEffect(() => { - if (!(isAuthenticated && !isPending && canUseAi && !aiLoading)) - return; + if (!(isAuthenticated && !isPending && canUseAi && !aiLoading)) return; // ── Handler 1: paste event (works when textarea is NOT focused) ─────── const handleDocumentPaste = (e: ClipboardEvent) => { @@ -281,7 +281,7 @@ export const AIToolbar = ({ document.removeEventListener("paste", handlePasteHandled, { capture: true, }); - document.removeEventListener("keydown", handleDocumentKeydown); + document.removeEventListener("keydown", handleDocumentKeydown); }; }, [isAuthenticated, isPending, canUseAi, aiLoading]); // onImagesSelect intentionally omitted — ref stays current diff --git a/tests/ai-toolbar-paste.test.ts b/tests/ai-toolbar-paste.test.ts index de143fd..987ff65 100644 --- a/tests/ai-toolbar-paste.test.ts +++ b/tests/ai-toolbar-paste.test.ts @@ -122,7 +122,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -155,7 +155,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -173,7 +173,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -193,7 +193,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -213,7 +213,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -222,7 +222,7 @@ describe("AI toolbar paste capture", () => { await handleDocumentKeydown( createDocumentKeydownEvent({ ctrlKey: true, - target: { tagName: "DIV", isContentEditable: true }, + target: { tagName: "DIV", isContentEditable: true } as unknown as EventTarget, }), ); @@ -236,7 +236,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -258,7 +258,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -282,7 +282,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -310,7 +310,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -339,7 +339,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -376,7 +376,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); @@ -403,7 +403,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; const { textareaProps } = await renderToolbarBoundary({ onImagesSelect: syncOnImagesSelect, @@ -452,7 +452,7 @@ describe("AI toolbar paste capture", () => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read: clipboardRead }, - } as Navigator; + } as unknown as Navigator; await renderToolbar({ onImagesSelect }); diff --git a/tests/ai-toolbar-state.test.ts b/tests/ai-toolbar-state.test.ts index 8fddc25..292e51f 100644 --- a/tests/ai-toolbar-state.test.ts +++ b/tests/ai-toolbar-state.test.ts @@ -8,6 +8,12 @@ import { registerAIToolbarMarkupHooks(); +const makeEvent = (id = "event-1") => ({ + id, + title: "Event", + start: "2026-04-22T09:00:00.000Z", +}); + describe("AI toolbar state rendering", () => { test("unauthenticated users see the sign-in-required callout", async () => { const markup = await renderToolbarMarkup({ isAuthenticated: false }); @@ -66,7 +72,7 @@ describe("AI toolbar state rendering", () => { test("Summarize only appears when there are events to summarize", async () => { const emptyMarkup = await renderToolbarMarkup({ events: [] }); const populatedMarkup = await renderToolbarMarkup({ - events: [{ id: "event-1" }], + events: [makeEvent()], }); expect(emptyMarkup).not.toContain("Summarize"); @@ -138,7 +144,7 @@ describe("AI toolbar state rendering", () => { test("Summarize button calls onAiSummarize when events are present", async () => { const onAiSummarizeMock = mock(); const actions = await renderToolbarActionBoundary({ - events: [{ id: "event-1" }], + events: [makeEvent()], onAiSummarize: onAiSummarizeMock, }); diff --git a/tests/helpers/ai-toolbar-test-helpers.ts b/tests/helpers/ai-toolbar-test-helpers.ts index bbdec67..56b3a20 100644 --- a/tests/helpers/ai-toolbar-test-helpers.ts +++ b/tests/helpers/ai-toolbar-test-helpers.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, expect, mock } from "bun:test"; import * as React from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import type { CalendarEvent } from "@/lib/types"; export type AIToolbarProps = { adminAiEnabled: boolean; @@ -19,7 +20,7 @@ export type AIToolbarProps = { onSummaryDismiss: () => void; summary: string | null; summaryUpdated: string | null; - events: Array<{ id?: string }>; + events: CalendarEvent[]; }; const actualReact = React; @@ -63,7 +64,7 @@ export const registerAIToolbarEffectHooks = () => { globalThis.document = { addEventListener: documentAddEventListener, removeEventListener: documentRemoveEventListener, - } as Document; + } as unknown as Document; documentAddEventListener.mockImplementation( ( type: string, @@ -102,7 +103,7 @@ export const registerAIToolbarMarkupHooks = () => { globalThis.document = { addEventListener: () => {}, removeEventListener: () => {}, - } as Document; + } as unknown as Document; mock.module("@/hooks/use-mobile", () => ({ useIsMobile: () => false, })); @@ -276,10 +277,16 @@ export const createDocumentKeydownEvent = ( metaKey: false, shiftKey: false, altKey: false, - target: { tagName: "DIV", isContentEditable: false }, + target: { tagName: "DIV", isContentEditable: false } as unknown as EventTarget, ...overrides, }) as unknown as KeyboardEvent; +export const setMockNavigatorClipboardRead = (read: () => Promise) => { + (globalThis as { navigator?: Navigator }).navigator = { + clipboard: { read }, + } as unknown as Navigator; +}; + const escapeForRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");