Files
Genarrative/src/components/common/publishShareCardImage.test.ts
kdletters c5763fdf25 重构作品分享链路
统一发布分享弹窗为作品分享卡片

支持下载分享卡与小程序九宫切图保存

小程序复制链接改为可直达作品详情的 web-view 路径

修复本地 dev Rust 构建绕过损坏 sccache

补充分享链路与 dev 启动文档和测试
2026-06-11 21:32:29 +08:00

147 lines
4.2 KiB
TypeScript

/* @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('陶泥儿');
});
});