fix(ai-toolbar): tighten modifier and clipboard fallback guards
This commit is contained in:
@@ -40,6 +40,15 @@ function useOs(): Os {
|
|||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createClipboardFallbackFile(blob: Blob, index: number): File {
|
||||||
|
const createdAt = Date.now() + index;
|
||||||
|
const extension = blob.type.split("/")[1] || "png";
|
||||||
|
return new File([blob], `clipboard-image-${createdAt}.${extension}`, {
|
||||||
|
type: blob.type,
|
||||||
|
lastModified: createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Shared shortcuts list (rendered in both HoverCard and Popover) ───────────
|
// ─── Shared shortcuts list (rendered in both HoverCard and Popover) ───────────
|
||||||
|
|
||||||
function ShortcutsList({ os }: { os: Os }) {
|
function ShortcutsList({ os }: { os: Os }) {
|
||||||
@@ -220,9 +229,7 @@ export const AIToolbar = ({
|
|||||||
for (const mimeType of typesToTry) {
|
for (const mimeType of typesToTry) {
|
||||||
try {
|
try {
|
||||||
const blob = await clipboardItem.getType(mimeType);
|
const blob = await clipboardItem.getType(mimeType);
|
||||||
files.push(
|
files.push(createClipboardFallbackFile(blob, files.length));
|
||||||
new File([blob], "clipboard-image", { type: mimeType }),
|
|
||||||
);
|
|
||||||
break; // got this item, move to next clipboardItem
|
break; // got this item, move to next clipboardItem
|
||||||
} catch {
|
} catch {
|
||||||
// NotFoundError — type not present, try next
|
// NotFoundError — type not present, try next
|
||||||
@@ -310,6 +317,8 @@ export const AIToolbar = ({
|
|||||||
if (
|
if (
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
(e.metaKey || e.ctrlKey) &&
|
(e.metaKey || e.ctrlKey) &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!e.altKey &&
|
||||||
!aiLoading &&
|
!aiLoading &&
|
||||||
canUseAi &&
|
canUseAi &&
|
||||||
(aiPrompt.trim() || hasImages)
|
(aiPrompt.trim() || hasImages)
|
||||||
|
|||||||
@@ -488,6 +488,24 @@ describe("AI toolbar paste capture", () => {
|
|||||||
expect(onAiCreate).toHaveBeenCalledTimes(1);
|
expect(onAiCreate).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Mod+Enter ignores extra modifiers so Shift+Ctrl+Enter does not generate", async () => {
|
||||||
|
const onAiCreate = mock();
|
||||||
|
|
||||||
|
const { textareaProps } = await renderToolbarBoundary({
|
||||||
|
aiPrompt: "Draft a kickoff",
|
||||||
|
onAiCreate,
|
||||||
|
});
|
||||||
|
const event = createTextareaKeydownEvent({
|
||||||
|
ctrlKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
textareaProps.onKeyDown?.(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||||
|
expect(onAiCreate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test("Mod+Enter does not trigger AI generation while loading", async () => {
|
test("Mod+Enter does not trigger AI generation while loading", async () => {
|
||||||
const onAiCreate = mock();
|
const onAiCreate = mock();
|
||||||
|
|
||||||
@@ -630,6 +648,41 @@ 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("clipboard fallback creates distinct files for same-sized clipboard images", async () => {
|
||||||
|
const onImagesSelect = mock();
|
||||||
|
const clipboardRead = mock(async () => [
|
||||||
|
{
|
||||||
|
types: ["image/png"],
|
||||||
|
getType: async () => new Blob(["abcd"], { type: "image/png" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
types: ["image/png"],
|
||||||
|
getType: async () => new Blob(["wxyz"], { type: "image/png" }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
(globalThis as { navigator?: Navigator }).navigator = {
|
||||||
|
clipboard: { read: clipboardRead },
|
||||||
|
} as Navigator;
|
||||||
|
|
||||||
|
await renderToolbar({ onImagesSelect });
|
||||||
|
|
||||||
|
const handleDocumentKeydown = getDocumentListener("keydown");
|
||||||
|
|
||||||
|
await handleDocumentKeydown(
|
||||||
|
createDocumentKeydownEvent({ ctrlKey: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onImagesSelect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onImagesSelect.mock.calls[0]?.[0]).toHaveLength(2);
|
||||||
|
expect(onImagesSelect.mock.calls[0]?.[0][0]?.name).not.toBe(
|
||||||
|
onImagesSelect.mock.calls[0]?.[0][1]?.name,
|
||||||
|
);
|
||||||
|
expect(onImagesSelect.mock.calls[0]?.[0][0]?.size).toBe(
|
||||||
|
onImagesSelect.mock.calls[0]?.[0][1]?.size,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
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