修复画布图片复制到系统剪切板
将图片剪切板写入提前到用户点击调用栈内发起 固定系统剪切板输出为 image/png 补充异步图片读取前已启动系统剪切板写入的回归测试
This commit is contained in:
@@ -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<string, Blob | Promise<Blob>>;
|
||||
|
||||
function installImageClipboardMock() {
|
||||
class ClipboardItemMock {
|
||||
readonly items: Record<string, Blob>;
|
||||
readonly items: ClipboardImageItems;
|
||||
|
||||
constructor(items: Record<string, Blob>) {
|
||||
constructor(items: ClipboardImageItems) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
const write = vi.fn(
|
||||
async (_items: Array<{ items: Record<string, Blob> }>) => 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<string, Blob> }>
|
||||
| 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<Blob>((resolve) => {
|
||||
resolveBlob = resolve;
|
||||
}),
|
||||
);
|
||||
render(<LayerCommandsHarness />);
|
||||
|
||||
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<string, Blob> }>
|
||||
| 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
// 系统剪贴板只是画布内复制的附加能力,失败不阻断内部粘贴。
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user