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