重构作品分享链路
统一发布分享弹窗为作品分享卡片 支持下载分享卡与小程序九宫切图保存 小程序复制链接改为可直达作品详情的 web-view 路径 修复本地 dev Rust 构建绕过损坏 sccache 补充分享链路与 dev 启动文档和测试
This commit is contained in:
146
src/components/common/publishShareCardImage.test.ts
Normal file
146
src/components/common/publishShareCardImage.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { readAssetBytes } from '../../services/assetReadUrlService';
|
||||
import {
|
||||
downloadPublishShareCardImage,
|
||||
resolvePublishShareCardCanvasImageSource,
|
||||
} from './publishShareCardImage';
|
||||
|
||||
vi.mock('../../services/assetReadUrlService', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../services/assetReadUrlService')>(
|
||||
'../../services/assetReadUrlService',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
readAssetBytes: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const createObjectUrl = vi.fn(() => 'blob:share-card-cover');
|
||||
const revokeObjectUrl = vi.fn();
|
||||
const fillTextCalls: string[] = [];
|
||||
|
||||
function installObjectUrlMocks() {
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: createObjectUrl,
|
||||
});
|
||||
Object.defineProperty(URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
value: revokeObjectUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function installCanvasMocks() {
|
||||
class MockImage {
|
||||
crossOrigin = '';
|
||||
onload: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
naturalWidth = 900;
|
||||
naturalHeight = 900;
|
||||
width = 900;
|
||||
height = 900;
|
||||
|
||||
set src(_value: string) {
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('Image', MockImage);
|
||||
vi.spyOn(document.body, 'appendChild').mockImplementation((node) => node);
|
||||
vi.spyOn(document.body, 'removeChild').mockImplementation((node) => node);
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
beginPath: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
clip: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
createLinearGradient: vi.fn(() => ({
|
||||
addColorStop: vi.fn(),
|
||||
})),
|
||||
drawImage: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
fillText: vi.fn((text: string) => {
|
||||
fillTextCalls.push(text);
|
||||
}),
|
||||
lineTo: vi.fn(),
|
||||
measureText: vi.fn((text: string) => ({
|
||||
width: Array.from(text).length * 32,
|
||||
})),
|
||||
moveTo: vi.fn(),
|
||||
quadraticCurveTo: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
save: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
} as unknown as CanvasRenderingContext2D);
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation(
|
||||
(callback: BlobCallback) => {
|
||||
callback(new Blob(['share-card'], { type: 'image/png' }));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
fillTextCalls.length = 0;
|
||||
});
|
||||
|
||||
describe('publishShareCardImage', () => {
|
||||
test('loads generated covers through same-origin bytes before drawing to canvas', async () => {
|
||||
installObjectUrlMocks();
|
||||
vi.mocked(readAssetBytes).mockResolvedValue(
|
||||
new Response(new Blob(['cover-bytes'], { type: 'image/png' })),
|
||||
);
|
||||
|
||||
const imageSource = await resolvePublishShareCardCanvasImageSource(
|
||||
'/generated-puzzle-assets/session/profile/covers/main.png',
|
||||
);
|
||||
|
||||
expect(readAssetBytes).toHaveBeenCalledWith(
|
||||
'/generated-puzzle-assets/session/profile/covers/main.png',
|
||||
{ expireSeconds: 600 },
|
||||
);
|
||||
expect(imageSource.src).toBe('blob:share-card-cover');
|
||||
|
||||
imageSource.release();
|
||||
|
||||
expect(revokeObjectUrl).toHaveBeenCalledWith('blob:share-card-cover');
|
||||
});
|
||||
|
||||
test('keeps ordinary public covers as their original source', async () => {
|
||||
const imageSource = await resolvePublishShareCardCanvasImageSource(
|
||||
'/creation-type-references/puzzle.webp',
|
||||
);
|
||||
|
||||
expect(readAssetBytes).not.toHaveBeenCalled();
|
||||
expect(imageSource.src).toBe('/creation-type-references/puzzle.webp');
|
||||
});
|
||||
|
||||
test('exports the same card content as the modal instead of adding extra branding', async () => {
|
||||
installObjectUrlMocks();
|
||||
installCanvasMocks();
|
||||
|
||||
await expect(
|
||||
downloadPublishShareCardImage(
|
||||
{
|
||||
title: '三叶草',
|
||||
publicWorkCode: 'PZ-BE68CC73',
|
||||
stage: 'puzzle-gallery-detail',
|
||||
workTypeLabel: '拼图',
|
||||
coverImageSrc: '/cover.png',
|
||||
},
|
||||
'/cover.png',
|
||||
),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(fillTextCalls).toContain('拼图');
|
||||
expect(fillTextCalls).toContain('三叶草');
|
||||
expect(fillTextCalls).toContain('PZ-BE68CC73');
|
||||
expect(fillTextCalls).not.toContain('陶泥儿');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user