fix(ai-toolbar): align fallback shortcut and identity guards

This commit is contained in:
2026-04-23 05:10:55 -04:00
parent 85f4066ce2
commit 502bd6237a
2 changed files with 66 additions and 0 deletions

View File

@@ -693,6 +693,26 @@ describe("AI toolbar paste capture", () => {
expect(onImagesSelect.mock.calls[0]?.[0][0]?.type).toBe("image/png");
});
test("Ctrl+Meta+V does not trigger the async clipboard fallback", async () => {
const onImagesSelect = mock();
const clipboardRead = mock(async () => []);
(globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead },
} as Navigator;
await renderToolbar({ onImagesSelect });
const handleDocumentKeydown = getDocumentListener("keydown");
await handleDocumentKeydown(
createDocumentKeydownEvent({ ctrlKey: true, metaKey: true }),
);
expect(clipboardRead).not.toHaveBeenCalled();
expect(onImagesSelect).not.toHaveBeenCalled();
});
test("clipboard fallback creates distinct files for same-sized clipboard images", async () => {
const onImagesSelect = mock();
const clipboardRead = mock(async () => [
@@ -797,6 +817,51 @@ describe("AI toolbar paste capture", () => {
);
});
test("synchronous paste and async fallback normalize identical clipboard payloads to the same file identity", async () => {
const syncOnImagesSelect = mock();
const asyncOnImagesSelect = mock();
const clipboardRead = mock(async () => [
{
types: ["image/png"],
getType: async () => new Blob(["same-image"], { type: "image/png" }),
},
]);
(globalThis as { navigator?: Navigator }).navigator = {
clipboard: { read: clipboardRead },
} as Navigator;
const { textareaProps } = await renderToolbarBoundary({
onImagesSelect: syncOnImagesSelect,
});
await textareaProps.onPaste?.({
clipboardData: createClipboardData([
new File(["same-image"], "clipboard.png", { type: "image/png" }),
]),
preventDefault: mock(),
} as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
await new Promise((resolve) => setTimeout(resolve, 0));
registeredDocumentListeners.clear();
documentAddEventListener.mockClear();
documentRemoveEventListener.mockClear();
await renderToolbar({ onImagesSelect: asyncOnImagesSelect });
const handleDocumentKeydown = getDocumentListener("keydown");
await handleDocumentKeydown(
createDocumentKeydownEvent({ ctrlKey: true }),
);
expect(syncOnImagesSelect).toHaveBeenCalledTimes(1);
expect(asyncOnImagesSelect).toHaveBeenCalledTimes(1);
expect(syncOnImagesSelect.mock.calls[0]?.[0][0]?.name).toBe(
asyncOnImagesSelect.mock.calls[0]?.[0][0]?.name,
);
expect(syncOnImagesSelect.mock.calls[0]?.[0][0]?.lastModified).toBe(
asyncOnImagesSelect.mock.calls[0]?.[0][0]?.lastModified,
);
});
test("async clipboard fallback does not double-handle a paste already handled by the synchronous document paste flow", async () => {
const onImagesSelect = mock();
const clipboardRead = mock(async () => [