import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { prepareUserAvatar, USER_AVATAR_MAX_DIMENSION, } from '@/lib/user-avatar'; const decodeMock = vi.hoisted(() => vi.fn()); const inspectFrameCountMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/browser-image-codec', () => ({ browserImageCodec: { inspectFrameCount: (...args: unknown[]) => inspectFrameCountMock(...args), decode: (...args: unknown[]) => decodeMock(...args), }, })); describe('user avatar preparation', () => { beforeEach(() => { decodeMock.mockReset(); inspectFrameCountMock.mockReset(); inspectFrameCountMock.mockResolvedValue(1); }); afterEach(() => { vi.restoreAllMocks(); }); it('center-crops a portrait image to a square before encoding it', async () => { const dispose = vi.fn(); const source = {} as CanvasImageSource; const drawImage = vi.fn(); const context = { imageSmoothingEnabled: false, imageSmoothingQuality: 'low' as ImageSmoothingQuality, clearRect: vi.fn(), drawImage, } as unknown as CanvasRenderingContext2D; const canvas = { width: 0, height: 0, getContext: vi.fn(() => context), toBlob: vi.fn((callback: BlobCallback) => callback(new Blob(['avatar'], { type: 'image/webp' }))), } as unknown as HTMLCanvasElement; vi.spyOn(document, 'createElement').mockReturnValue(canvas as unknown as HTMLElement); decodeMock.mockResolvedValue({ source, width: 100, height: 200, dispose }); const result = await prepareUserAvatar(new File(['source'], 'portrait.png', { type: 'image/png' })); expect(drawImage).toHaveBeenCalledWith( source, 0, 50, 100, 100, 0, 0, 100, 100, ); expect(result).toMatchObject({ fileName: 'avatar.webp', mimeType: 'image/webp', width: 100, height: 100, previewUrl: expect.stringMatching(/^data:image\/webp;base64,/), }); expect(result.width).toBeLessThanOrEqual(USER_AVATAR_MAX_DIMENSION); expect(dispose).toHaveBeenCalledOnce(); expect(canvas.width).toBe(1); expect(canvas.height).toBe(1); }); it('rejects unsupported avatar formats before decoding', async () => { await expect(prepareUserAvatar(new File(['text'], 'avatar.gif', { type: 'image/gif' }))) .rejects.toThrow('头像仅支持 PNG、JPEG 或 WebP 图片'); expect(inspectFrameCountMock).not.toHaveBeenCalled(); expect(decodeMock).not.toHaveBeenCalled(); }); it('rejects animated avatar files before decoding', async () => { inspectFrameCountMock.mockResolvedValue(2); await expect(prepareUserAvatar(new File(['animated'], 'avatar.webp', { type: 'image/webp' }))) .rejects.toThrow('头像不支持动态图片'); expect(decodeMock).not.toHaveBeenCalled(); }); });