From 7d954e0985d2ffac8f38b810cbd1f6ba4bc73cfb Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 2 Jul 2026 21:14:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=A4=8D=E5=88=B6=E5=88=B0=E7=B3=BB=E7=BB=9F=E5=89=AA?= =?UTF-8?q?=E5=88=87=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将图片剪切板写入提前到用户点击调用栈内发起 固定系统剪切板输出为 image/png 补充异步图片读取前已启动系统剪切板写入的回归测试 --- .../useImageCanvasLayerCommands.test.tsx | 45 ++++++++++++++++--- .../useImageCanvasLayerCommands.ts | 31 ++++++++----- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/components/image-editor/useImageCanvasLayerCommands.test.tsx b/src/components/image-editor/useImageCanvasLayerCommands.test.tsx index 717210232..8c3ee2d97 100644 --- a/src/components/image-editor/useImageCanvasLayerCommands.test.tsx +++ b/src/components/image-editor/useImageCanvasLayerCommands.test.tsx @@ -43,17 +43,18 @@ function createLayer(id: string, x: number, zIndex: number): CanvasLayer { const originalClipboard = navigator.clipboard; const originalClipboardItem = globalThis.ClipboardItem; const originalCreateImageBitmap = globalThis.createImageBitmap; +type ClipboardImageItems = Record>; function installImageClipboardMock() { class ClipboardItemMock { - readonly items: Record; + readonly items: ClipboardImageItems; - constructor(items: Record) { + constructor(items: ClipboardImageItems) { this.items = items; } } const write = vi.fn( - async (_items: Array<{ items: Record }>) => undefined, + async (_items: Array<{ items: ClipboardImageItems }>) => undefined, ); Object.defineProperty(globalThis, 'ClipboardItem', { configurable: true, @@ -311,15 +312,41 @@ describe('useImageCanvasLayerCommands', () => { await waitFor(() => expect(write).toHaveBeenCalledTimes(1)); const clipboardItems = write.mock.calls[0]?.[0] as - | Array<{ items: Record }> + | Array<{ items: ClipboardImageItems }> | undefined; - expect(clipboardItems?.[0]?.items['image/png']).toBeInstanceOf(Blob); + await expect( + clipboardItems?.[0]?.items['image/png'], + ).resolves.toBeInstanceOf(Blob); expect(readLayerImageBlob).toHaveBeenCalledWith( expect.objectContaining({ id: 'second' }), ); expect(screen.getByTestId('clipboard').textContent).toBe('copy:2'); }); + it('starts system clipboard write before image bytes finish loading', async () => { + const write = installImageClipboardMock(); + let resolveBlob: (blob: Blob) => void = () => undefined; + vi.mocked(readLayerImageBlob).mockReturnValueOnce( + new Promise((resolve) => { + resolveBlob = resolve; + }), + ); + render(); + + fireEvent.click(screen.getByRole('button', { name: '复制选中' })); + + await waitFor(() => expect(write).toHaveBeenCalledTimes(1)); + const clipboardItems = write.mock.calls[0]?.[0] as + | Array<{ items: ClipboardImageItems }> + | undefined; + const pngBlobPromise = clipboardItems?.[0]?.items['image/png']; + expect(pngBlobPromise).toBeInstanceOf(Promise); + + resolveBlob(new Blob(['late-png'], { type: 'image/png' })); + + await expect(pngBlobPromise).resolves.toBeInstanceOf(Blob); + }); + it('normalizes copied images to png for the system clipboard', async () => { const write = installImageClipboardMock(); const drawImage = vi.fn(); @@ -351,9 +378,13 @@ describe('useImageCanvasLayerCommands', () => { await waitFor(() => expect(write).toHaveBeenCalledTimes(1)); const clipboardItems = write.mock.calls[0]?.[0] as - | Array<{ items: Record }> + | Array<{ items: ClipboardImageItems }> | undefined; - expect(clipboardItems?.[0]?.items['image/png']?.type).toBe('image/png'); + await expect( + Promise.resolve(clipboardItems?.[0]?.items['image/png']).then( + (blob) => blob?.type, + ), + ).resolves.toBe('image/png'); expect(drawImage).toHaveBeenCalledTimes(1); }); diff --git a/src/components/image-editor/useImageCanvasLayerCommands.ts b/src/components/image-editor/useImageCanvasLayerCommands.ts index bc0b4d414..2550fb774 100644 --- a/src/components/image-editor/useImageCanvasLayerCommands.ts +++ b/src/components/image-editor/useImageCanvasLayerCommands.ts @@ -99,18 +99,27 @@ async function normalizeClipboardImageBlob(blob: Blob) { return blob; } +async function readLayerClipboardPngBlob(layer: CanvasLayer) { + const blob = await normalizeClipboardImageBlob( + await readLayerImageBlob(layer), + ); + return blob.type === 'image/png' + ? blob + : blob.slice(0, blob.size, 'image/png'); +} + function copyLayerImageToSystemClipboard(layers: CanvasLayer[]) { - void (async () => { - const layer = layers.slice().reverse().find(isClipboardImageLayer); - const clipboard = globalThis.navigator?.clipboard; - if (!layer || !clipboard?.write || typeof ClipboardItem !== 'function') { - return; - } - const blob = await normalizeClipboardImageBlob( - await readLayerImageBlob(layer), - ); - await clipboard.write([new ClipboardItem({ [blob.type]: blob })]); - })().catch(() => { + const layer = layers.slice().reverse().find(isClipboardImageLayer); + const clipboard = globalThis.navigator?.clipboard; + const ClipboardItemCtor = globalThis.ClipboardItem; + if (!layer || !clipboard?.write || typeof ClipboardItemCtor !== 'function') { + return; + } + void clipboard.write([ + new ClipboardItemCtor({ + 'image/png': readLayerClipboardPngBlob(layer), + }), + ]).catch(() => { // 系统剪贴板只是画布内复制的附加能力,失败不阻断内部粘贴。 }); }