Files
Genarrative/src/services/puzzleReferenceImage.test.ts
2026-05-08 11:44:42 +08:00

171 lines
5.0 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH,
cropPuzzleReferenceImageDataUrl,
isPuzzleReferenceImageSquare,
readPuzzleReferenceImageForUpload,
readPuzzleReferenceImageAsDataUrl,
} from './puzzleReferenceImage';
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
function stubFileReader(dataUrl: string) {
class MockFileReader {
result: string | null = null;
error: Error | null = null;
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
readAsDataURL() {
this.result = dataUrl;
this.onload?.();
}
}
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
}
function stubImage(width = 4096, height = 3072) {
class MockImage {
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
naturalWidth = width;
naturalHeight = height;
width = width;
height = height;
set src(_value: string) {
this.onload?.();
}
}
vi.stubGlobal('Image', MockImage as unknown as typeof Image);
}
function stubCanvas(dataUrls: string[]) {
const drawImage = vi.fn();
const toDataURL = vi
.fn()
.mockImplementation(
() => dataUrls.shift() ?? 'data:image/jpeg;base64,small',
);
const originalCreateElement = document.createElement.bind(document);
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
if (tagName !== 'canvas') {
return originalCreateElement(tagName);
}
return {
width: 0,
height: 0,
getContext: () => ({
drawImage,
fillRect: vi.fn(),
fillStyle: '',
imageSmoothingEnabled: false,
imageSmoothingQuality: 'low',
}),
toDataURL,
} as unknown as HTMLCanvasElement;
});
return { drawImage, toDataURL };
}
describe('readPuzzleReferenceImageAsDataUrl', () => {
test('compresses large puzzle reference images before JSON upload', async () => {
stubFileReader(`data:image/png;base64,${'A'.repeat(3 * 1024 * 1024)}`);
stubImage();
const { drawImage, toDataURL } = stubCanvas([
`data:image/jpeg;base64,${'B'.repeat(1200)}`,
`data:image/jpeg;base64,${'C'.repeat(1000)}`,
`data:image/jpeg;base64,${'D'.repeat(1400)}`,
]);
const file = new File(['x'.repeat(2 * 1024 * 1024)], 'reference.png', {
type: 'image/png',
});
const dataUrl = await readPuzzleReferenceImageAsDataUrl(file);
expect(dataUrl).toBe(`data:image/jpeg;base64,${'C'.repeat(1000)}`);
expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 1536, 1152);
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.84);
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.76);
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.68);
});
test('rejects reference images that still exceed the upload budget', async () => {
stubFileReader(
`data:image/png;base64,${'A'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH + 1)}`,
);
stubImage();
stubCanvas([
`data:image/jpeg;base64,${'B'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH + 1)}`,
`data:image/jpeg;base64,${'C'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH + 2)}`,
`data:image/jpeg;base64,${'D'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH + 3)}`,
]);
const file = new File(['x'.repeat(2 * 1024 * 1024)], 'reference.png', {
type: 'image/png',
});
await expect(readPuzzleReferenceImageAsDataUrl(file)).rejects.toThrow(
'参考图过大,请换一张尺寸更小的图片。',
);
});
});
describe('puzzle reference image square crop helpers', () => {
test('reports square upload dimensions without opening crop flow', async () => {
const sourceDataUrl = 'data:image/png;base64,square';
stubFileReader(sourceDataUrl);
stubImage(512, 512);
const file = new File(['x'], 'square.png', { type: 'image/png' });
const result = await readPuzzleReferenceImageForUpload(file);
expect(result).toEqual({
dataUrl: sourceDataUrl,
width: 512,
height: 512,
});
expect(isPuzzleReferenceImageSquare(result)).toBe(true);
});
test('crops non-square uploads to a centered square data URL', async () => {
const sourceDataUrl = 'data:image/png;base64,wide';
const croppedDataUrl = 'data:image/jpeg;base64,cropped';
stubImage(800, 600);
const { drawImage, toDataURL } = stubCanvas([
`data:image/jpeg;base64,${'A'.repeat(30)}`,
croppedDataUrl,
`data:image/jpeg;base64,${'B'.repeat(40)}`,
]);
const dataUrl = await cropPuzzleReferenceImageDataUrl({
source: sourceDataUrl,
cropX: 100,
cropY: 0,
cropSize: 600,
});
expect(dataUrl).toBe(croppedDataUrl);
expect(drawImage).toHaveBeenCalledWith(
expect.anything(),
100,
0,
600,
600,
0,
0,
600,
600,
);
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.88);
});
});