From c92633e647c730b8385e7ae84c0e8870001079fc Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Wed, 22 Apr 2026 23:25:08 -0400 Subject: [PATCH] test(ai-toolbar): cover editable paste behavior --- tests/ai-toolbar.test.ts | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/tests/ai-toolbar.test.ts b/tests/ai-toolbar.test.ts index 0ea264e..d963cd1 100644 --- a/tests/ai-toolbar.test.ts +++ b/tests/ai-toolbar.test.ts @@ -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 = {}) => { AIToolbar(createToolbarProps(overrides)); }; +const renderToolbarBoundary = async (overrides: Partial = {}) => { + 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); + + 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();