统一发布分享弹窗为作品分享卡片 支持下载分享卡与小程序九宫切图保存 小程序复制链接改为可直达作品详情的 web-view 路径 修复本地 dev Rust 构建绕过损坏 sccache 补充分享链路与 dev 启动文档和测试
147 lines
4.2 KiB
TypeScript
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('陶泥儿');
|
|
});
|
|
});
|