From d8da0fffa5177cf9945eec0bb36e7f330428057c Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 7 Apr 2026 22:51:19 +0200 Subject: [PATCH] fix(tests): resolve URL.createObjectURL and fetch mocking failures on CI Three interrelated issues caused 4 tests to pass locally but fail on CI: 1. setup.ts only applied the URL.createObjectURL stub when it was undefined, but jsdom already defines it (returning ''). Changed to always override with configurable:true so the predictable 'blob:mock' value is set in every environment. 2. FE-API-013 used Object.defineProperty (non-configurable in jsdom) and MSW to handle a native fetch call. Replaced with vi.spyOn for both URL.createObjectURL/revokeObjectURL and a direct fetch mock, which is more reliable across environments. 3. FE-COMP-AUTHURL-012's vi.spyOn(URL, 'createObjectURL') returned the same vi.fn() instance set in setup.ts, accumulating calls from all prior tests in the file (1+8+7+6=22 instead of 6). Added mockClear() immediately after the spy setup to reset the count. --- client/tests/integration/api/client.test.ts | 18 ++++++++---------- client/tests/setup.ts | 11 ++++------- client/tests/unit/api/authUrl.test.ts | 1 + 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/client/tests/integration/api/client.test.ts b/client/tests/integration/api/client.test.ts index c9be28d6..529fc715 100644 --- a/client/tests/integration/api/client.test.ts +++ b/client/tests/integration/api/client.test.ts @@ -334,10 +334,14 @@ describe('API client interceptors', () => { // ── backupApi.download ─────────────────────────────────────────────────────── it('FE-API-013: backupApi.download creates a temp anchor and clicks it', async () => { - const createObjectURL = vi.fn(() => 'blob:mock-url'); - const revokeObjectURL = vi.fn(); - Object.defineProperty(URL, 'createObjectURL', { writable: true, value: createObjectURL }); - Object.defineProperty(URL, 'revokeObjectURL', { writable: true, value: revokeObjectURL }); + // backupApi.download uses native fetch (not axios), so mock it directly to + // avoid jsdom/MSW interception differences across environments. + const blob = new Blob(['zip-bytes'], { type: 'application/zip' }); + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(blob, { status: 200 }) + ); + const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url'); + const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); // Spy on createElement to intercept the anchor click const originalCreate = document.createElement.bind(document); @@ -350,12 +354,6 @@ describe('API client interceptors', () => { return el; }); - server.use( - http.get('/api/backup/download/backup.zip', () => { - return new HttpResponse(new Blob(['zip-bytes'], { type: 'application/zip' }), { status: 200 }); - }) - ); - await expect(backupApi.download('backup.zip')).resolves.toBeUndefined(); expect(createObjectURL).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalled(); diff --git a/client/tests/setup.ts b/client/tests/setup.ts index 2f507fed..0d6373e3 100644 --- a/client/tests/setup.ts +++ b/client/tests/setup.ts @@ -59,13 +59,10 @@ globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ disconnect: vi.fn(), })) as unknown as typeof ResizeObserver; -// URL.createObjectURL / revokeObjectURL — used by file uploads -if (typeof URL.createObjectURL === 'undefined') { - Object.defineProperty(URL, 'createObjectURL', { writable: true, value: vi.fn(() => 'blob:mock') }); -} -if (typeof URL.revokeObjectURL === 'undefined') { - Object.defineProperty(URL, 'revokeObjectURL', { writable: true, value: vi.fn() }); -} +// URL.createObjectURL / revokeObjectURL — jsdom defines these but returns '' for createObjectURL; +// always override so tests get a predictable 'blob:mock' string. +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(); diff --git a/client/tests/unit/api/authUrl.test.ts b/client/tests/unit/api/authUrl.test.ts index 3780678a..23e5f23a 100644 --- a/client/tests/unit/api/authUrl.test.ts +++ b/client/tests/unit/api/authUrl.test.ts @@ -193,6 +193,7 @@ describe('clearImageQueue', () => { it('removes queued items so they never execute after active slots drain', async () => { const resolvers: Array<() => void> = []; const createObjectURLSpy = vi.spyOn(URL, 'createObjectURL'); + createObjectURLSpy.mockClear(); // vi.spyOn returns the same vi.fn() set in setup.ts; reset accumulated calls from prior tests vi.spyOn(globalThis, 'fetch').mockImplementation(async () => { await new Promise(r => resolvers.push(r));