test(ai-toolbar): cover editable paste behavior

This commit is contained in:
2026-04-22 23:25:08 -04:00
parent 46f7aff815
commit c92633e647

View File

@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { readFileSync } from "node:fs";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { buttonVariants } from "@/components/ui/button";
import { getAiDisabledMessage } from "@/lib/ai-feature-flags";
import { cn } from "@/lib/utils";
@@ -175,6 +176,27 @@ const renderToolbar = async (overrides: Partial<AIToolbarProps> = {}) => {
AIToolbar(createToolbarProps(overrides));
};
const renderToolbarBoundary = async (overrides: Partial<AIToolbarProps> = {}) => {
let capturedTextareaProps: React.ComponentProps<"textarea"> | undefined;
mock.module("@/components/ui/textarea", () => ({
Textarea: (props: React.ComponentProps<"textarea">) => {
capturedTextareaProps = props;
return actualReact.createElement("textarea", props);
},
}));
const { AIToolbar } = await import("@/components/ai-toolbar");
renderToStaticMarkup(
actualReact.createElement(AIToolbar, createToolbarProps(overrides)),
);
expect(capturedTextareaProps).toBeDefined();
return capturedTextareaProps!;
};
const getExamplePromptButtonClassName = () => {
const source = readToolbarSource();
const match = source.match(
@@ -292,6 +314,25 @@ describe("Event count badge positioning contract", () => {
});
describe("AI capture redesign", () => {
test("textarea paste path still forwards clipboard images to onImagesSelect through the component boundary", async () => {
const onImagesSelect = mock();
const image = new File(["image-bytes"], "clipboard.png", {
type: "image/png",
});
const textareaProps = await renderToolbarBoundary({ onImagesSelect });
const preventDefault = mock();
textareaProps.onPaste?.({
clipboardData: createClipboardData([image]),
preventDefault,
} as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onImagesSelect).toHaveBeenCalledTimes(1);
expect(onImagesSelect).toHaveBeenCalledWith([image]);
});
test("document paste forwards clipboard images to onImagesSelect only for non-editable targets", async () => {
const onImagesSelect = mock();
const image = new File(["image-bytes"], "clipboard.png", {
@@ -317,6 +358,30 @@ describe("AI capture redesign", () => {
expect(onImagesSelect).toHaveBeenCalledWith([image]);
});
test("document paste ignores contenteditable targets so the focused editor owns the paste", async () => {
const onImagesSelect = mock();
const image = new File(["image-bytes"], "clipboard.png", {
type: "image/png",
});
await renderToolbar({ onImagesSelect });
const handleDocumentPaste = getDocumentListener(
"paste",
(entry) => !entry.options?.capture,
);
const preventDefault = mock();
handleDocumentPaste({
target: { tagName: "DIV", isContentEditable: true },
clipboardData: createClipboardData([image]),
preventDefault,
} as unknown as Event);
expect(preventDefault).not.toHaveBeenCalled();
expect(onImagesSelect).not.toHaveBeenCalled();
});
test("Ctrl/Cmd+V fallback forwards clipboard images to onImagesSelect for non-editable targets", async () => {
const onImagesSelect = mock();
const clipboardRead = mock(async () => [
@@ -350,7 +415,7 @@ describe("AI capture redesign", () => {
expect(onImagesSelect.mock.calls[0]?.[0][0]?.type).toBe("image/png");
});
test("Ctrl/Cmd+V fallback ignores editable targets", async () => {
test("Ctrl/Cmd+V fallback ignores contenteditable targets", async () => {
const onImagesSelect = mock();
const clipboardRead = mock(async () => []);
@@ -368,7 +433,7 @@ describe("AI capture redesign", () => {
metaKey: false,
shiftKey: false,
altKey: false,
target: { tagName: "INPUT", isContentEditable: false },
target: { tagName: "DIV", isContentEditable: true },
} as unknown as Event);
expect(clipboardRead).not.toHaveBeenCalled();