完善客户端模块与工作区能力
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-13 19:45:30 +08:00
parent 0148b91a15
commit 22add3f01f
97 changed files with 4756 additions and 3226 deletions

View File

@@ -0,0 +1,87 @@
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();
});
});