fix(ai-toolbar): align fallback shortcut and identity guards
This commit is contained in:
@@ -222,6 +222,7 @@ export const AIToolbar = ({
|
|||||||
const isV = e.key === "v" || e.key === "V";
|
const isV = e.key === "v" || e.key === "V";
|
||||||
const isModifier = e.ctrlKey || e.metaKey;
|
const isModifier = e.ctrlKey || e.metaKey;
|
||||||
if (!isV || !isModifier || e.shiftKey || e.altKey) return;
|
if (!isV || !isModifier || e.shiftKey || e.altKey) return;
|
||||||
|
if (e.ctrlKey && e.metaKey) return;
|
||||||
if (isEditableTarget(e.target)) return;
|
if (isEditableTarget(e.target)) return;
|
||||||
|
|
||||||
pasteHandledByEvent = false;
|
pasteHandledByEvent = false;
|
||||||
|
|||||||
@@ -693,6 +693,26 @@ describe("AI toolbar paste capture", () => {
|
|||||||
expect(onImagesSelect.mock.calls[0]?.[0][0]?.type).toBe("image/png");
|
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 () => {
|
test("clipboard fallback creates distinct files for same-sized clipboard images", async () => {
|
||||||
const onImagesSelect = mock();
|
const onImagesSelect = mock();
|
||||||
const clipboardRead = mock(async () => [
|
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 () => {
|
test("async clipboard fallback does not double-handle a paste already handled by the synchronous document paste flow", async () => {
|
||||||
const onImagesSelect = mock();
|
const onImagesSelect = mock();
|
||||||
const clipboardRead = mock(async () => [
|
const clipboardRead = mock(async () => [
|
||||||
|
|||||||
Reference in New Issue
Block a user