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 {
const element = target as HTMLElement | null;
return !!element && (
element.tagName === "TEXTAREA" ||
return (
!!element &&
(element.tagName === "TEXTAREA" ||
element.tagName === "INPUT" ||
element.isContentEditable
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) => {

View File

@@ -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 });

View File

@@ -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,
});

View File

@@ -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<unknown>) => {
(globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read },
} as unknown as Navigator;
};
const escapeForRegex = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");