fix(ai-toolbar): finalize normalized clipboard test coverage

This commit is contained in:
2026-04-23 10:38:35 -04:00
parent 251520fd29
commit 71e4133d57
4 changed files with 40 additions and 27 deletions

View File

@@ -95,10 +95,11 @@ function ShortcutsList({ os }: { os: Os }) {
function isEditableTarget(target: EventTarget | null): target is HTMLElement { function isEditableTarget(target: EventTarget | null): target is HTMLElement {
const element = target as HTMLElement | null; const element = target as HTMLElement | null;
return !!element && ( return (
element.tagName === "TEXTAREA" || !!element &&
element.tagName === "INPUT" || (element.tagName === "TEXTAREA" ||
element.isContentEditable element.tagName === "INPUT" ||
element.isContentEditable)
); );
} }
@@ -187,8 +188,7 @@ export const AIToolbar = ({
// focused element or OS clipboard model (X11/Wayland). // focused element or OS clipboard model (X11/Wayland).
// This is the approach used by Excalidraw's actionPaste. // This is the approach used by Excalidraw's actionPaste.
useEffect(() => { useEffect(() => {
if (!(isAuthenticated && !isPending && canUseAi && !aiLoading)) if (!(isAuthenticated && !isPending && canUseAi && !aiLoading)) return;
return;
// ── Handler 1: paste event (works when textarea is NOT focused) ─────── // ── Handler 1: paste event (works when textarea is NOT focused) ───────
const handleDocumentPaste = (e: ClipboardEvent) => { const handleDocumentPaste = (e: ClipboardEvent) => {
@@ -281,7 +281,7 @@ export const AIToolbar = ({
document.removeEventListener("paste", handlePasteHandled, { document.removeEventListener("paste", handlePasteHandled, {
capture: true, capture: true,
}); });
document.removeEventListener("keydown", handleDocumentKeydown); document.removeEventListener("keydown", handleDocumentKeydown);
}; };
}, [isAuthenticated, isPending, canUseAi, aiLoading]); // onImagesSelect intentionally omitted — ref stays current }, [isAuthenticated, isPending, canUseAi, aiLoading]); // onImagesSelect intentionally omitted — ref stays current

View File

@@ -122,7 +122,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -155,7 +155,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -173,7 +173,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -193,7 +193,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -213,7 +213,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -222,7 +222,7 @@ describe("AI toolbar paste capture", () => {
await handleDocumentKeydown( await handleDocumentKeydown(
createDocumentKeydownEvent({ createDocumentKeydownEvent({
ctrlKey: true, 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 = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -258,7 +258,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -282,7 +282,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -310,7 +310,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -339,7 +339,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -376,7 +376,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });
@@ -403,7 +403,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
const { textareaProps } = await renderToolbarBoundary({ const { textareaProps } = await renderToolbarBoundary({
onImagesSelect: syncOnImagesSelect, onImagesSelect: syncOnImagesSelect,
@@ -452,7 +452,7 @@ describe("AI toolbar paste capture", () => {
(globalThis as { navigator?: Navigator }).navigator = { (globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead }, clipboard: { read: clipboardRead },
} as Navigator; } as unknown as Navigator;
await renderToolbar({ onImagesSelect }); await renderToolbar({ onImagesSelect });

View File

@@ -8,6 +8,12 @@ import {
registerAIToolbarMarkupHooks(); registerAIToolbarMarkupHooks();
const makeEvent = (id = "event-1") => ({
id,
title: "Event",
start: "2026-04-22T09:00:00.000Z",
});
describe("AI toolbar state rendering", () => { describe("AI toolbar state rendering", () => {
test("unauthenticated users see the sign-in-required callout", async () => { test("unauthenticated users see the sign-in-required callout", async () => {
const markup = await renderToolbarMarkup({ isAuthenticated: false }); 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 () => { test("Summarize only appears when there are events to summarize", async () => {
const emptyMarkup = await renderToolbarMarkup({ events: [] }); const emptyMarkup = await renderToolbarMarkup({ events: [] });
const populatedMarkup = await renderToolbarMarkup({ const populatedMarkup = await renderToolbarMarkup({
events: [{ id: "event-1" }], events: [makeEvent()],
}); });
expect(emptyMarkup).not.toContain("Summarize"); expect(emptyMarkup).not.toContain("Summarize");
@@ -138,7 +144,7 @@ describe("AI toolbar state rendering", () => {
test("Summarize button calls onAiSummarize when events are present", async () => { test("Summarize button calls onAiSummarize when events are present", async () => {
const onAiSummarizeMock = mock(); const onAiSummarizeMock = mock();
const actions = await renderToolbarActionBoundary({ const actions = await renderToolbarActionBoundary({
events: [{ id: "event-1" }], events: [makeEvent()],
onAiSummarize: onAiSummarizeMock, onAiSummarize: onAiSummarizeMock,
}); });

View File

@@ -1,6 +1,7 @@
import { afterEach, beforeEach, expect, mock } from "bun:test"; import { afterEach, beforeEach, expect, mock } from "bun:test";
import * as React from "react"; import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import type { CalendarEvent } from "@/lib/types";
export type AIToolbarProps = { export type AIToolbarProps = {
adminAiEnabled: boolean; adminAiEnabled: boolean;
@@ -19,7 +20,7 @@ export type AIToolbarProps = {
onSummaryDismiss: () => void; onSummaryDismiss: () => void;
summary: string | null; summary: string | null;
summaryUpdated: string | null; summaryUpdated: string | null;
events: Array<{ id?: string }>; events: CalendarEvent[];
}; };
const actualReact = React; const actualReact = React;
@@ -63,7 +64,7 @@ export const registerAIToolbarEffectHooks = () => {
globalThis.document = { globalThis.document = {
addEventListener: documentAddEventListener, addEventListener: documentAddEventListener,
removeEventListener: documentRemoveEventListener, removeEventListener: documentRemoveEventListener,
} as Document; } as unknown as Document;
documentAddEventListener.mockImplementation( documentAddEventListener.mockImplementation(
( (
type: string, type: string,
@@ -102,7 +103,7 @@ export const registerAIToolbarMarkupHooks = () => {
globalThis.document = { globalThis.document = {
addEventListener: () => {}, addEventListener: () => {},
removeEventListener: () => {}, removeEventListener: () => {},
} as Document; } as unknown as Document;
mock.module("@/hooks/use-mobile", () => ({ mock.module("@/hooks/use-mobile", () => ({
useIsMobile: () => false, useIsMobile: () => false,
})); }));
@@ -276,10 +277,16 @@ export const createDocumentKeydownEvent = (
metaKey: false, metaKey: false,
shiftKey: false, shiftKey: false,
altKey: false, altKey: false,
target: { tagName: "DIV", isContentEditable: false }, target: { tagName: "DIV", isContentEditable: false } as unknown as EventTarget,
...overrides, ...overrides,
}) as unknown as KeyboardEvent; }) as unknown as KeyboardEvent;
export const setMockNavigatorClipboardRead = (read: () => Promise<unknown>) => {
(globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read },
} as unknown as Navigator;
};
const escapeForRegex = (value: string) => const escapeForRegex = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");