fix: Keep the clipboard write within user activation
This commit is contained in:
@@ -97,14 +97,24 @@ describe('MessageBubble', () => {
|
||||
});
|
||||
|
||||
it('copies an attached WebP as PNG without opening the message text menu', async () => {
|
||||
const clipboardWrite = vi.fn().mockResolvedValue(undefined);
|
||||
let resolveAssetRead!: (response: Response) => void;
|
||||
const pendingAssetRead = new Promise<Response>((resolve) => {
|
||||
resolveAssetRead = resolve;
|
||||
});
|
||||
const clipboardWrite = vi.fn(
|
||||
async (
|
||||
items: Array<{ items: Record<string, Blob | PromiseLike<Blob>> }>,
|
||||
) => {
|
||||
await items[0]?.items['image/png'];
|
||||
},
|
||||
);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { write: clipboardWrite },
|
||||
});
|
||||
const ClipboardItemMock = vi.fn(function (
|
||||
this: { items: Record<string, Blob> },
|
||||
items: Record<string, Blob>,
|
||||
this: { items: Record<string, Blob | PromiseLike<Blob>> },
|
||||
items: Record<string, Blob | PromiseLike<Blob>>,
|
||||
) {
|
||||
this.items = items;
|
||||
});
|
||||
@@ -127,11 +137,7 @@ describe('MessageBubble', () => {
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation(
|
||||
(callback) => callback(new Blob(['png'], { type: 'image/png' })),
|
||||
);
|
||||
vi.mocked(readAssetBytes).mockResolvedValue({
|
||||
blob: vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Blob(['webp'], { type: 'image/webp' })),
|
||||
} as unknown as Response);
|
||||
vi.mocked(readAssetBytes).mockReturnValue(pendingAssetRead);
|
||||
const { container } = renderMessage({
|
||||
id: 5,
|
||||
role: 'user',
|
||||
@@ -158,14 +164,21 @@ describe('MessageBubble', () => {
|
||||
expect(screen.queryByRole('menuitem', { name: '复制文本' })).toBeNull();
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '复制图片' }));
|
||||
|
||||
await waitFor(() => expect(clipboardWrite).toHaveBeenCalledTimes(1));
|
||||
// The clipboard write starts synchronously, before private asset loading completes.
|
||||
expect(clipboardWrite).toHaveBeenCalledTimes(1);
|
||||
expect(readAssetBytes).toHaveBeenCalledWith(
|
||||
'/generated-editor-images/private.webp',
|
||||
{ objectKey: 'editor/private.webp' },
|
||||
);
|
||||
expect(ClipboardItemMock).toHaveBeenCalledWith({
|
||||
'image/png': expect.any(Blob),
|
||||
'image/png': expect.any(Promise),
|
||||
});
|
||||
resolveAssetRead({
|
||||
blob: vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Blob(['webp'], { type: 'image/webp' })),
|
||||
} as unknown as Response);
|
||||
await screen.findByRole('menuitem', { name: '已复制' });
|
||||
expect(bitmapClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,10 +93,11 @@ async function copyAssetImage(asset: EditorAgentContextAsset) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await readAssetBytes(asset.source, {
|
||||
const pngBlob = readAssetBytes(asset.source, {
|
||||
objectKey: asset.objectKey,
|
||||
});
|
||||
const pngBlob = await convertImageBlobToPng(await response.blob());
|
||||
}).then(async (response) => convertImageBlobToPng(await response.blob()));
|
||||
// Clipboard write must start in the menu click's user-activation stack.
|
||||
// Safari accepts promised ClipboardItem data and resolves it after asset loading/conversion.
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({ 'image/png': pngBlob }),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user