fix(ai-toolbar): finalize normalized clipboard test coverage
This commit is contained in:
@@ -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 === "TEXTAREA" ||
|
||||||
element.tagName === "INPUT" ||
|
element.tagName === "INPUT" ||
|
||||||
element.isContentEditable
|
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) => {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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, "\\$&");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user