mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(tests): use node:buffer.Blob so URL.createObjectURL works on Node 22
Node 22 URL.createObjectURL strictly requires a native node:buffer Blob and throws ERR_INVALID_ARG_TYPE when given a jsdom Blob (caught by fetchImageAsBlob, returning ''). Node 24 relaxed this check, masking the failure locally. Tests 007, 011: replace MSW/Response-based fetch mocks with direct vi.spyOn(fetch) mocks returning node:buffer Blobs via a duck-typed response object. The real URL.createObjectURL now handles the correct Blob type and returns a genuine blob: URL on all Node versions. Test 012: URL.createObjectURL identity varies across Node versions making it impossible to spy on reliably. Replace createObjectURLSpy assertion with a completedFetches counter in the fetch mock, which proves the same semantic guarantee (6 requests ran, 7th was cleared). setup.ts: restore the original conditional guard so the vi.fn fallback only applies when URL.createObjectURL is completely absent, not overwriting a working real implementation.
This commit is contained in:
@@ -59,11 +59,15 @@ globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
disconnect: vi.fn(),
|
||||
})) as unknown as typeof ResizeObserver;
|
||||
|
||||
// URL.createObjectURL / revokeObjectURL — in jsdom, modules access globals
|
||||
// through window.URL (not globalThis.URL — these are different objects on CI).
|
||||
// Targeting window.URL is required so the mock is visible to source modules.
|
||||
Object.defineProperty(window.URL, 'createObjectURL', { writable: true, configurable: true, value: vi.fn(() => 'blob:mock') });
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', { writable: true, configurable: true, value: vi.fn() });
|
||||
// URL.createObjectURL / revokeObjectURL — Node 22 URL.createObjectURL requires
|
||||
// a native node:buffer Blob; passing a jsdom Blob throws ERR_INVALID_ARG_TYPE.
|
||||
// Tests that need blob URLs should mock fetch to return node:buffer Blobs so
|
||||
// the real URL.createObjectURL works. For tests that only need the method to
|
||||
// exist without returning a real URL, stub it here as a vi.fn fallback.
|
||||
if (typeof URL.createObjectURL === 'undefined') {
|
||||
Object.defineProperty(URL, 'createObjectURL', { writable: true, configurable: true, value: vi.fn(() => 'blob:mock') });
|
||||
Object.defineProperty(URL, 'revokeObjectURL', { writable: true, configurable: true, value: vi.fn() });
|
||||
}
|
||||
|
||||
// Element.prototype.scrollIntoView — jsdom doesn't implement it
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
@@ -8,8 +8,7 @@ const flushPromises = () => new Promise<void>(r => setTimeout(r, 10));
|
||||
|
||||
beforeEach(() => {
|
||||
clearImageQueue();
|
||||
vi.restoreAllMocks(); // remove vi.spyOn() wrappers, restoring to the setup.ts vi.fn()
|
||||
vi.clearAllMocks(); // reset accumulated call counts on window.URL mocks from setup.ts
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
@@ -82,15 +81,16 @@ describe('fetchImageAsBlob', () => {
|
||||
|
||||
describe('FE-COMP-AUTHURL-007: successful fetch returns blob object URL', () => {
|
||||
it('resolves to a blob URL for a valid image response', async () => {
|
||||
server.use(
|
||||
http.get('/uploads/photo.jpg', () =>
|
||||
new HttpResponse(new Blob(['fake-image'], { type: 'image/jpeg' }), {
|
||||
status: 200,
|
||||
})
|
||||
)
|
||||
);
|
||||
// Node 22 URL.createObjectURL requires a native node:buffer Blob, not a
|
||||
// jsdom Blob — passing the wrong type throws ERR_INVALID_ARG_TYPE (caught,
|
||||
// returns ''). Mock fetch directly with a Node Blob so the real
|
||||
// URL.createObjectURL works without any mocking needed.
|
||||
const { Blob: NodeBlob } = await import('node:buffer');
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
||||
ok: true,
|
||||
blob: () => Promise.resolve(new NodeBlob(['fake-image'], { type: 'image/jpeg' }) as unknown as Blob),
|
||||
} as unknown as Response);
|
||||
const result = await fetchImageAsBlob('/uploads/photo.jpg');
|
||||
// URL.createObjectURL is native in Node 20+; just assert it's a blob URL
|
||||
expect(result).toMatch(/^blob:/);
|
||||
});
|
||||
});
|
||||
@@ -159,10 +159,14 @@ describe('fetchImageAsBlob', () => {
|
||||
describe('FE-COMP-AUTHURL-011: queued request runs after active slot frees', () => {
|
||||
it('7th request eventually resolves once one of the 6 active slots is freed', async () => {
|
||||
const resolvers: Array<() => void> = [];
|
||||
const { Blob: NodeBlob } = await import('node:buffer');
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
||||
await new Promise<void>(r => resolvers.push(r));
|
||||
return new Response(new Blob(['img'], { type: 'image/jpeg' }), { status: 200 });
|
||||
return {
|
||||
ok: true,
|
||||
blob: () => Promise.resolve(new NodeBlob(['img'], { type: 'image/jpeg' }) as unknown as Blob),
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
const urls = Array.from({ length: 7 }, (_, i) => `/uploads/queue${i}.jpg`);
|
||||
@@ -194,11 +198,19 @@ describe('clearImageQueue', () => {
|
||||
describe('FE-COMP-AUTHURL-012: clearImageQueue discards pending entries', () => {
|
||||
it('removes queued items so they never execute after active slots drain', async () => {
|
||||
const resolvers: Array<() => void> = [];
|
||||
const createObjectURLSpy = vi.spyOn(window.URL, 'createObjectURL');
|
||||
// Track completions via fetch mock instead of URL.createObjectURL spy —
|
||||
// URL.createObjectURL is a Node built-in whose identity varies across
|
||||
// Node versions, making it unreliable to spy on in jsdom tests on CI.
|
||||
let completedFetches = 0;
|
||||
const { Blob: NodeBlob } = await import('node:buffer');
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
||||
await new Promise<void>(r => resolvers.push(r));
|
||||
return new Response(new Blob(['img'], { type: 'image/jpeg' }), { status: 200 });
|
||||
completedFetches++;
|
||||
return {
|
||||
ok: true,
|
||||
blob: () => Promise.resolve(new NodeBlob(['img'], { type: 'image/jpeg' }) as unknown as Blob),
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
const urls = Array.from({ length: 7 }, (_, i) => `/uploads/clear${i}.jpg`);
|
||||
@@ -215,7 +227,7 @@ describe('clearImageQueue', () => {
|
||||
await flushPromises();
|
||||
|
||||
// 6 active slots completed; queue was cleared so the 7th never ran
|
||||
expect(createObjectURLSpy).toHaveBeenCalledTimes(6);
|
||||
expect(completedFetches).toBe(6);
|
||||
|
||||
// First 6 promises resolved; 7th is orphaned (never resolves)
|
||||
await Promise.all(promises.slice(0, 6));
|
||||
|
||||
Reference in New Issue
Block a user