Files
Genarrative/src/services/puzzleReferenceImage.test.ts
2026-05-24 19:00:21 +08:00

171 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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, 1024, 768);
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(
'参考图过大,请压缩后再上传(当前 10.0MB,最多 6MB。',
);
});
});
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);
});
});