Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,829 @@
import { describe, expect, it, vi } from 'vitest';
import {
calculateImageResizeTarget,
estimateSelectedImageTokens,
getEffectiveImageMimeType,
getImageEncodeCandidates,
getImageFileNameForMime,
IMAGE_MAX_AUTO_PROCESS_SOURCE_BYTES,
processImageAttachment,
shouldSkipAutomaticImageProcessing,
type BrowserImageCodec,
type DecodedImage,
type RenderedImage,
} from '@/lib/image-attachment-compression';
import { browserImageCodec } from '@/lib/browser-image-codec';
function createDeferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, reject, resolve };
}
describe('calculateImageResizeTarget', () => {
it('keeps a small image unchanged', () => {
expect(calculateImageResizeTarget(1280, 720)).toEqual({
width: 1280,
height: 720,
scale: 1,
resized: false,
});
});
it('caps a 4K image by the pixel limit', () => {
const target = calculateImageResizeTarget(3840, 2160);
expect(target).toEqual({
width: 1930,
height: 1086,
scale: expect.closeTo(Math.sqrt(2_097_152 / (3840 * 2160)), 8),
resized: true,
});
expect(target.width * target.height).toBeLessThanOrEqual(2_097_152);
});
it('caps a very wide image by the long edge and never returns zero', () => {
expect(calculateImageResizeTarget(4096, 256)).toMatchObject({
width: 2048,
height: 128,
resized: true,
});
expect(calculateImageResizeTarget(100_000, 1)).toMatchObject({
width: 2048,
height: 1,
resized: true,
});
});
it.each([
[Number.NaN, 10],
[Number.POSITIVE_INFINITY, 10],
[10, Number.NEGATIVE_INFINITY],
[0, 10],
[-1, 10],
[10, 0],
])('rejects invalid dimensions %s × %s', (width, height) => {
expect(() => calculateImageResizeTarget(width, height))
.toThrowError(new RangeError('Image dimensions must be finite positive numbers'));
});
});
describe('image MIME and encoding rules', () => {
it('infers a missing MIME from the filename', () => {
expect(getEffectiveImageMimeType(new File([], 'animation.GIF'))).toBe('image/gif');
expect(getEffectiveImageMimeType(new File([], 'vector.svg'))).toBe('image/svg+xml');
expect(getEffectiveImageMimeType(new File([], 'unknown.bin'))).toBe('application/octet-stream');
});
it.each(['image/gif', 'IMAGE/GIF', 'image/svg+xml'])('skips %s immediately', (mimeType) => {
expect(shouldSkipAutomaticImageProcessing(mimeType)).toBe(true);
});
it('selects lossless output for PNG and BMP', () => {
expect(getImageEncodeCandidates('image/png', true)).toEqual([
{ mimeType: 'image/png' },
]);
expect(getImageEncodeCandidates('image/bmp', false)).toEqual([
{ mimeType: 'image/png' },
]);
});
it('selects JPEG quality and WebP fallbacks deterministically', () => {
expect(getImageEncodeCandidates('image/jpeg', false)).toEqual([
{ mimeType: 'image/jpeg', quality: 0.85 },
]);
expect(getImageEncodeCandidates('image/webp', true)).toEqual([
{ mimeType: 'image/webp', quality: 0.85 },
{ mimeType: 'image/png' },
]);
expect(getImageEncodeCandidates('image/avif', false)).toEqual([
{ mimeType: 'image/webp', quality: 0.85 },
{ mimeType: 'image/jpeg', quality: 0.85 },
]);
});
it('makes transport filename extensions match the selected MIME', () => {
expect(getImageFileNameForMime('capture.bmp', 'image/png')).toBe('capture.png');
expect(getImageFileNameForMime('photo.avif', 'image/webp')).toBe('photo.webp');
expect(getImageFileNameForMime('photo.avif', 'image/jpeg')).toBe('photo.jpg');
expect(getImageFileNameForMime('photo.jpeg', 'image/jpg')).toBe('photo.jpg');
expect(getImageFileNameForMime('.hidden', 'image/png')).toBe('.hidden.png');
expect(getImageFileNameForMime('notes.txt', 'text/plain')).toBe('notes.txt');
});
});
describe('estimateSelectedImageTokens', () => {
it('delegates only an imported Qwen Plus model to the shared profile', () => {
expect(estimateSelectedImageTokens(
'niancode-user-models/qwen3.7-plus',
3840,
2160,
)).toBe(8102);
expect(estimateSelectedImageTokens(
'niancode-user-models/qwen3.7-plus',
1930,
1086,
)).toBe(2049);
});
it('returns null for unverified models and a same-named model on another provider', () => {
expect(estimateSelectedImageTokens(
'niancode-user-models/qwen-vl-max',
1930,
1086,
)).toBeNull();
expect(estimateSelectedImageTokens(
'openai/qwen3.7-plus',
1930,
1086,
)).toBeNull();
expect(estimateSelectedImageTokens('qwen3.7-plus', 1930, 1086)).toBeNull();
expect(estimateSelectedImageTokens(
'niancode-user-models/qwen3.7-plus/extra',
1930,
1086,
)).toBeNull();
});
});
function createFakeCodec(options: {
frameCount?: number | null;
width?: number;
height?: number;
hasAlpha?: boolean;
encoded?: Array<Blob | null>;
}) {
const decoded: DecodedImage = {
source: {} as CanvasImageSource,
width: options.width ?? 3840,
height: options.height ?? 2160,
dispose: vi.fn(),
};
const rendered: RenderedImage = {
canvas: {} as HTMLCanvasElement,
hasAlpha: options.hasAlpha ?? false,
dispose: vi.fn(),
};
const encoded = [...(options.encoded ?? [])];
const codec = {
inspectFrameCount: vi.fn(async () => (
Object.prototype.hasOwnProperty.call(options, 'frameCount')
? options.frameCount ?? null
: 1
)),
decode: vi.fn(async () => decoded),
render: vi.fn(() => rendered),
encode: vi.fn(async () => encoded.shift() ?? null),
} satisfies BrowserImageCodec;
return { codec, decoded, rendered };
}
describe('processImageAttachment', () => {
it('keeps an over-limit encoded source sendable without invoking the codec', async () => {
const { codec } = createFakeCodec({});
const file = new File([], 'huge.png', { type: 'image/png' });
Object.defineProperty(file, 'size', {
configurable: true,
value: IMAGE_MAX_AUTO_PROCESS_SOURCE_BYTES + 1,
});
await expect(processImageAttachment(file, codec)).resolves.toMatchObject({
status: 'skipped',
original: {
blob: file,
mimeType: 'image/png',
bytes: IMAGE_MAX_AUTO_PROCESS_SOURCE_BYTES + 1,
},
defaultVariant: 'original',
skipReason: 'source-too-large',
warning: expect.any(String),
});
expect(codec.inspectFrameCount).not.toHaveBeenCalled();
expect(codec.decode).not.toHaveBeenCalled();
expect(codec.render).not.toHaveBeenCalled();
expect(codec.encode).not.toHaveBeenCalled();
});
it('inspects one frame, honors EXIF orientation, resizes, and uses WebP first', async () => {
const compressed = new Blob(['compressed'], { type: 'image/webp' });
const { codec, decoded, rendered } = createFakeCodec({
frameCount: 1,
encoded: [compressed],
});
const file = new File(['original'], 'photo.avif', { type: 'image/avif' });
const result = await processImageAttachment(file, codec);
expect(codec.inspectFrameCount).toHaveBeenCalledWith(
file,
'image/avif',
undefined,
);
expect(codec.decode).toHaveBeenCalledWith(file, {
imageOrientation: 'from-image',
}, undefined);
expect(codec.render).toHaveBeenCalledWith(decoded, {
width: 1930,
height: 1086,
}, undefined);
expect(codec.encode).toHaveBeenCalledWith(rendered, {
mimeType: 'image/webp',
quality: 0.85,
}, undefined);
expect(result).toMatchObject({
status: 'compressed',
defaultVariant: 'compressed',
compressed: {
blob: compressed,
mimeType: 'image/webp',
width: 1930,
height: 1086,
},
});
expect(decoded.dispose).toHaveBeenCalledTimes(1);
expect(rendered.dispose).toHaveBeenCalledTimes(1);
});
it('falls back from unsupported WebP to PNG for an alpha image', async () => {
const png = new Blob(['png'], { type: 'image/png' });
const { codec } = createFakeCodec({
frameCount: 1,
hasAlpha: true,
encoded: [null, png],
});
const result = await processImageAttachment(
new File(['original'], 'alpha.avif', { type: 'image/avif' }),
codec,
);
expect(codec.encode).toHaveBeenNthCalledWith(1, expect.anything(), {
mimeType: 'image/webp',
quality: 0.85,
}, undefined);
expect(codec.encode).toHaveBeenNthCalledWith(2, expect.anything(), {
mimeType: 'image/png',
}, undefined);
expect(result).toMatchObject({
status: 'compressed',
compressed: { mimeType: 'image/png' },
});
});
it.each([
['image/png', 'animated.png'],
['image/webp', 'animated.webp'],
['image/avif', 'animated.avif'],
])('skips multi-frame %s without decoding its first frame', async (mimeType, name) => {
const { codec } = createFakeCodec({ frameCount: 3 });
const result = await processImageAttachment(
new File(['animated'], name, { type: mimeType }),
codec,
);
expect(result).toMatchObject({
status: 'skipped',
skipReason: 'animated',
defaultVariant: 'original',
warning: '检测到动画图片,已保留原图。',
});
expect(codec.decode).not.toHaveBeenCalled();
expect(codec.render).not.toHaveBeenCalled();
expect(codec.encode).not.toHaveBeenCalled();
});
it('skips an animation-capable raster when single-frame status is unknown', async () => {
const { codec } = createFakeCodec({ frameCount: null });
const result = await processImageAttachment(
new File(['unknown'], 'unknown.webp', { type: 'image/webp' }),
codec,
);
expect(result).toMatchObject({
status: 'skipped',
skipReason: 'static-unverified',
warning: '无法确认图片为单帧,已保留原图。',
});
expect(codec.decode).not.toHaveBeenCalled();
});
it.each([
['image/gif', 'animation.gif', 'animated'],
['image/svg+xml', 'vector.svg', 'vector'],
] as const)('skips %s before frame inspection', async (mimeType, name, skipReason) => {
const { codec } = createFakeCodec({});
const result = await processImageAttachment(
new File(['original'], name, { type: mimeType }),
codec,
);
expect(result).toMatchObject({
status: 'skipped',
skipReason,
defaultVariant: 'original',
});
expect(codec.inspectFrameCount).not.toHaveBeenCalled();
expect(codec.decode).not.toHaveBeenCalled();
});
it('does not render or encode a small one-frame image', async () => {
const { codec } = createFakeCodec({
frameCount: 1,
width: 1280,
height: 720,
});
const result = await processImageAttachment(
new File(['small'], 'small.jpg', { type: 'image/jpeg' }),
codec,
);
expect(result).toMatchObject({
status: 'unchanged',
defaultVariant: 'original',
original: { width: 1280, height: 720 },
});
expect(codec.render).not.toHaveBeenCalled();
expect(codec.encode).not.toHaveBeenCalled();
});
it('fails open when decode throws', async () => {
const { codec } = createFakeCodec({ frameCount: 1 });
codec.decode.mockRejectedValueOnce(new Error('decode failed'));
const file = new File(['original'], 'broken.png', { type: 'image/png' });
await expect(processImageAttachment(file, codec)).resolves.toEqual({
status: 'failed',
original: {
blob: file,
mimeType: 'image/png',
bytes: file.size,
},
defaultVariant: 'original',
warning: '图片压缩失败,发送时将使用原图。',
});
});
it('fails open when every MIME encoder is unsupported', async () => {
const { codec } = createFakeCodec({
frameCount: 1,
hasAlpha: false,
encoded: [null, null],
});
const result = await processImageAttachment(
new File(['original'], 'photo.avif', { type: 'image/avif' }),
codec,
);
expect(result).toMatchObject({
status: 'failed',
defaultVariant: 'original',
warning: '图片压缩失败,发送时将使用原图。',
});
});
it('preserves decoded original dimensions when rendering later fails', async () => {
const { codec } = createFakeCodec({
frameCount: 1,
width: 3000,
height: 2000,
});
codec.render.mockImplementationOnce(() => {
throw new Error('render failed');
});
const file = new File(['original'], 'photo.png', { type: 'image/png' });
await expect(processImageAttachment(file, codec)).resolves.toMatchObject({
status: 'failed',
original: {
blob: file,
width: 3000,
height: 2000,
},
defaultVariant: 'original',
});
});
it('releases decoded and rendered resources when aborting after render', async () => {
const controller = new AbortController();
const { codec, decoded, rendered } = createFakeCodec({
frameCount: 1,
width: 3000,
height: 2000,
});
codec.render.mockImplementationOnce(() => {
controller.abort();
return rendered;
});
await expect(processImageAttachment(
new File(['original'], 'photo.png', { type: 'image/png' }),
codec,
{ signal: controller.signal },
)).rejects.toMatchObject({ name: 'AbortError' });
expect(decoded.dispose).toHaveBeenCalledTimes(1);
expect(rendered.dispose).toHaveBeenCalledTimes(1);
expect(codec.encode).not.toHaveBeenCalled();
});
it('releases resources when an encode completes after cancellation', async () => {
const controller = new AbortController();
const { codec, decoded, rendered } = createFakeCodec({
frameCount: 1,
width: 3000,
height: 2000,
});
codec.encode.mockImplementationOnce(async () => {
controller.abort();
return new Blob(['compressed'], { type: 'image/png' });
});
await expect(processImageAttachment(
new File(['original'], 'photo.png', { type: 'image/png' }),
codec,
{ signal: controller.signal },
)).rejects.toMatchObject({ name: 'AbortError' });
expect(decoded.dispose).toHaveBeenCalledTimes(1);
expect(rendered.dispose).toHaveBeenCalledTimes(1);
});
it('preserves a compressed result when rendered cleanup throws', async () => {
const compressed = new Blob(['compressed'], { type: 'image/webp' });
const { codec, decoded, rendered } = createFakeCodec({
frameCount: 1,
encoded: [compressed],
});
rendered.dispose = vi.fn(() => {
throw new Error('rendered cleanup failed');
});
decoded.dispose = vi.fn(() => {
throw new Error('decoded cleanup failed');
});
await expect(processImageAttachment(
new File(['original'], 'photo.avif', { type: 'image/avif' }),
codec,
)).resolves.toMatchObject({
status: 'compressed',
defaultVariant: 'compressed',
compressed: { blob: compressed },
});
expect(rendered.dispose).toHaveBeenCalledTimes(1);
expect(decoded.dispose).toHaveBeenCalledTimes(1);
});
it('preserves an unchanged result when decoded cleanup throws', async () => {
const { codec, decoded } = createFakeCodec({
frameCount: 1,
width: 1280,
height: 720,
});
decoded.dispose = vi.fn(() => {
throw new Error('decoded cleanup failed');
});
await expect(processImageAttachment(
new File(['original'], 'small.jpg', { type: 'image/jpeg' }),
codec,
)).resolves.toMatchObject({
status: 'unchanged',
defaultVariant: 'original',
original: { width: 1280, height: 720 },
});
expect(decoded.dispose).toHaveBeenCalledTimes(1);
});
});
describe('browserImageCodec', () => {
it('passes a Blob stream to ImageDecoder without buffering the whole source', async () => {
const stream = {} as ReadableStream<Uint8Array>;
const blob = new Blob(['png'], { type: 'image/png' });
const arrayBuffer = vi.spyOn(blob, 'arrayBuffer');
Object.defineProperty(blob, 'stream', {
configurable: true,
value: vi.fn(() => stream),
});
let decoderData: unknown;
class FakeImageDecoder {
static isTypeSupported = vi.fn(async () => true);
tracks = {
ready: Promise.resolve(),
selectedTrack: { frameCount: 1 },
};
close = vi.fn();
constructor(init: { data: unknown }) {
decoderData = init.data;
}
}
vi.stubGlobal('ImageDecoder', FakeImageDecoder);
try {
await expect(browserImageCodec.inspectFrameCount(
blob,
'image/png',
)).resolves.toBe(1);
expect(decoderData).toBe(stream);
expect(arrayBuffer).not.toHaveBeenCalled();
} finally {
vi.unstubAllGlobals();
}
});
it('does not create a fallback ImageDecoder after buffering is aborted', async () => {
const buffered = createDeferred<ArrayBuffer>();
const blob = new Blob(['png'], { type: 'image/png' });
const arrayBuffer = vi.spyOn(blob, 'arrayBuffer')
.mockImplementation(() => buffered.promise);
Object.defineProperty(blob, 'stream', {
configurable: true,
value: vi.fn(() => {
throw new TypeError('ReadableStream input is unsupported');
}),
});
const construct = vi.fn();
class FakeImageDecoder {
static isTypeSupported = vi.fn(async () => true);
tracks = {
ready: Promise.resolve(),
selectedTrack: { frameCount: 1 },
};
close = vi.fn();
constructor() {
construct();
}
}
vi.stubGlobal('ImageDecoder', FakeImageDecoder);
const controller = new AbortController();
try {
const inspected = browserImageCodec.inspectFrameCount(
blob,
'image/png',
controller.signal,
);
await vi.waitFor(() => expect(arrayBuffer).toHaveBeenCalledTimes(1));
controller.abort();
buffered.resolve(new ArrayBuffer(3));
await expect(inspected).rejects.toMatchObject({ name: 'AbortError' });
expect(construct).not.toHaveBeenCalled();
} finally {
vi.unstubAllGlobals();
}
});
it('closes ImageDecoder and rejects when frame inspection is aborted', async () => {
const ready = createDeferred<void>();
const close = vi.fn();
const construct = vi.fn();
class FakeImageDecoder {
static isTypeSupported = vi.fn(async () => true);
tracks = {
ready: ready.promise,
selectedTrack: { frameCount: 1 },
};
close = close;
constructor() {
construct();
}
}
vi.stubGlobal('ImageDecoder', FakeImageDecoder);
const controller = new AbortController();
try {
const inspected = browserImageCodec.inspectFrameCount(
new Blob(['png'], { type: 'image/png' }),
'image/png',
controller.signal,
);
await vi.waitFor(() => expect(construct).toHaveBeenCalledTimes(1));
controller.abort();
ready.resolve();
await expect(inspected).rejects.toMatchObject({ name: 'AbortError' });
expect(close).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllGlobals();
}
});
it('closes a late ImageBitmap when decode is aborted', async () => {
const bitmap = {
width: 640,
height: 480,
close: vi.fn(),
};
const pendingBitmap = createDeferred<typeof bitmap>();
vi.stubGlobal('createImageBitmap', vi.fn(() => pendingBitmap.promise));
const controller = new AbortController();
try {
const decoded = browserImageCodec.decode(
new Blob(['image'], { type: 'image/png' }),
{ imageOrientation: 'from-image' },
controller.signal,
);
controller.abort();
pendingBitmap.resolve(bitmap);
await expect(decoded).rejects.toMatchObject({ name: 'AbortError' });
expect(bitmap.close).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllGlobals();
}
});
it('revokes the fallback image URL when decode is aborted', async () => {
const createObjectURL = vi.fn(() => 'blob:pending-image');
const revokeObjectURL = vi.fn();
class PendingImage {
decoding = '';
naturalWidth = 0;
naturalHeight = 0;
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
src = '';
}
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
vi.stubGlobal('Image', PendingImage);
vi.stubGlobal('createImageBitmap', undefined);
const controller = new AbortController();
try {
const decoded = browserImageCodec.decode(
new Blob(['image'], { type: 'image/png' }),
{ imageOrientation: 'from-image' },
controller.signal,
);
controller.abort();
await expect(decoded).rejects.toMatchObject({ name: 'AbortError' });
expect(revokeObjectURL).toHaveBeenCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledWith('blob:pending-image');
} finally {
vi.unstubAllGlobals();
}
});
it('rejects a pending canvas encoder when aborted', async () => {
let finishEncoding!: BlobCallback;
const rendered: RenderedImage = {
canvas: {
toBlob: vi.fn((callback: BlobCallback) => {
finishEncoding = callback;
}),
} as unknown as HTMLCanvasElement,
hasAlpha: false,
dispose: vi.fn(),
};
const controller = new AbortController();
const encoded = browserImageCodec.encode(
rendered,
{ mimeType: 'image/png' },
controller.signal,
);
controller.abort();
finishEncoding(new Blob(['late'], { type: 'image/png' }));
await expect(encoded).rejects.toMatchObject({ name: 'AbortError' });
});
it.each(['image/jpeg', 'image/bmp'] as const)(
'marks animation-capable bytes mislabeled as %s as unverified',
async (mimeType) => {
const staticHeader = mimeType === 'image/jpeg'
? new Uint8Array([0xff, 0xd8, 0xff])
: new Uint8Array([0x42, 0x4d]);
const webpHeader = new Uint8Array([
0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0,
0x57, 0x45, 0x42, 0x50,
]);
await expect(browserImageCodec.inspectFrameCount(
new Blob([staticHeader], { type: mimeType }),
mimeType,
)).resolves.toBe(1);
await expect(browserImageCodec.inspectFrameCount(
new Blob([webpHeader], { type: mimeType }),
mimeType,
)).resolves.toBeNull();
},
);
it('closes ImageDecoder after reading a verified animated-capable raster', async () => {
const close = vi.fn();
class FakeImageDecoder {
static isTypeSupported = vi.fn(async () => true);
tracks = {
ready: Promise.resolve(),
selectedTrack: { frameCount: 2 },
};
close = close;
}
vi.stubGlobal('ImageDecoder', FakeImageDecoder);
try {
await expect(browserImageCodec.inspectFrameCount(
new Blob(['png'], { type: 'image/png' }),
'image/png',
)).resolves.toBe(2);
expect(FakeImageDecoder.isTypeSupported).toHaveBeenCalledWith('image/png');
expect(close).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllGlobals();
}
});
it('detects alpha from canvas pixels and releases the canvas allocation', () => {
const context = {
clearRect: vi.fn(),
drawImage: vi.fn(),
getImageData: vi.fn(() => ({
data: new Uint8ClampedArray([0, 0, 0, 254]),
})),
} as unknown as CanvasRenderingContext2D;
const canvas = {
width: 0,
height: 0,
getContext: vi.fn(() => context),
} as unknown as HTMLCanvasElement;
const createElement = vi.spyOn(document, 'createElement').mockReturnValue(canvas);
try {
const rendered = browserImageCodec.render({
source: {} as CanvasImageSource,
width: 2,
height: 1,
dispose: vi.fn(),
}, { width: 2, height: 1 });
expect(createElement).toHaveBeenCalledWith('canvas');
expect(rendered.hasAlpha).toBe(true);
expect(canvas.width).toBe(2);
rendered.dispose();
expect(canvas.width).toBe(1);
expect(canvas.height).toBe(1);
} finally {
createElement.mockRestore();
}
});
it('rejects an encoder result with a MIME that differs from the candidate', async () => {
const toBlob = vi.fn((callback: BlobCallback) => {
callback(new Blob(['png'], { type: 'image/png' }));
});
const rendered: RenderedImage = {
canvas: { toBlob } as unknown as HTMLCanvasElement,
hasAlpha: false,
dispose: vi.fn(),
};
await expect(browserImageCodec.encode(rendered, {
mimeType: 'image/jpeg',
quality: 0.85,
})).resolves.toBeNull();
expect(toBlob).toHaveBeenCalledWith(
expect.any(Function),
'image/jpeg',
0.85,
);
});
it('revokes the fallback image URL exactly once when Image setup throws', async () => {
const createObjectURL = vi.fn(() => 'blob:test-image');
const revokeObjectURL = vi.fn();
class ThrowingImage {
constructor() {
throw new Error('Image setup failed');
}
}
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
vi.stubGlobal('Image', ThrowingImage);
vi.stubGlobal('createImageBitmap', undefined);
try {
await expect(browserImageCodec.decode(
new Blob(['image'], { type: 'image/png' }),
{ imageOrientation: 'from-image' },
)).rejects.toThrow('Image setup failed');
expect(revokeObjectURL).toHaveBeenCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledWith('blob:test-image');
} finally {
vi.unstubAllGlobals();
}
});
});