From cb6aeb7bfd45e69829a342441e63964f3ad34e4d Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 21 Sep 2026 23:19:41 +0000 Subject: [PATCH] test(members): stop the location stub from swallowing the request it waits for Two cases replaced window.location wholesale to spy on reload. The copy is flat, the real object's fields are getters on its prototype, so the replacement has no href for a relative request URL to resolve against and the DELETE the leave-trip case waits for never reached the mock server. It also stayed replaced for the rest of the file, which took six later cases down with it. The leave-trip case asserts the request, so it keeps the real location: jsdom's reload only logs. The transfer case asserts the reload, so it keeps the stub, and the original is put back after every test. Nobody saw this: the client coverage job needs i18n parity, which has been red. --- .../Trips/TripMembersModal.test.tsx | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/client/src/components/Trips/TripMembersModal.test.tsx b/client/src/components/Trips/TripMembersModal.test.tsx index d82fa8396..0e35564c9 100644 --- a/client/src/components/Trips/TripMembersModal.test.tsx +++ b/client/src/components/Trips/TripMembersModal.test.tsx @@ -55,6 +55,33 @@ function mockClipboard(): Mock<(text: string) => Promise> { return writeText; } +/** + * A `location` whose `reload` is a spy. + * + * jsdom's own `reload` cannot be redefined, so the whole object has to be + * replaced, and `{ ...window.location }` is the trap: its fields are getters on + * the prototype, so the copy has no `href` for axios to resolve a relative URL + * against and the request never reaches MSW. The fields are therefore read out + * by name. + */ +/** + * A `location` whose `reload` is a spy, for the two flows that reload the app. + * + * jsdom's own `reload` cannot be redefined, so the whole object is replaced, and + * the replacement is a flat copy without the prototype's getters. That is enough + * for a test that only asserts the reload, but it leaves a request with nothing + * to resolve a relative URL against, so a test that asserts the call itself must + * not use this. The original is put back after every test: left in place, it + * broke every later case in the file. + */ +let realLocation: PropertyDescriptor | undefined; +function stubReload(): Mock<() => void> { + const reload = vi.fn(); + realLocation ??= Object.getOwnPropertyDescriptor(window, 'location'); + Object.defineProperty(window, 'location', { value: { ...window.location, reload }, writable: true, configurable: true }); + return reload; +} + beforeEach(() => { resetAllStores(); server.use( @@ -82,6 +109,10 @@ afterEach(() => { delete window.__addToast; vi.useRealTimers(); vi.restoreAllMocks(); + if (realLocation) { + Object.defineProperty(window, 'location', realLocation); + realLocation = undefined; + } Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true, writable: true }); }); @@ -410,11 +441,9 @@ describe('TripMembersModal', () => { it('FE-COMP-MEMBERS-024: leave trip calls DELETE for current user', async () => { const user = userEvent.setup(); vi.spyOn(window, 'confirm').mockReturnValue(true); - Object.defineProperty(window, 'location', { - value: { ...window.location, reload: vi.fn() }, - writable: true, - configurable: true, - }); + // No reload stub here on purpose: this case asserts the DELETE, and a stubbed + // location is exactly what stops the request from reaching it. jsdom's own + // reload is a no-op that only logs. seedStore(useAuthStore, { user: memberUser, isAuthenticated: true }); seedStore(useTripStore, { trip: buildTrip({ id: 1, user_id: ownerUser.id }) }); @@ -747,8 +776,7 @@ describe('TripMembersModal', () => { it('FE-COMP-MEMBERS-042: transferring ownership reloads the app', async () => { const onClose = vi.fn(); - const reload = vi.fn(); - Object.defineProperty(window, 'location', { value: { ...window.location, reload }, writable: true, configurable: true }); + const reload = stubReload(); vi.spyOn(window, 'confirm').mockReturnValue(true); mockRoster([{ id: memberUser.id, username: 'alice', avatar_url: null }]); let transferBody: Record | null = null;