From 724cdd1c0ea9fdc111a87c715a02cb7e3857c162 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 12:03:13 +0200 Subject: [PATCH] test(client): keep the sign-in background work out of the auth specs login, completeMfaLogin and register start tripSyncManager.syncAll() and the system notice fetch without waiting for them, and every sign-in, loadUser included, registers the sync triggers with their 30 s heartbeat. #2613 stubbed the sync in the push spec after it was caught logging while the worker shut down ("Closing rpc while onUserConsoleLog was pending"); the same shape was left in the other specs that sign in for real. authStore.test.ts ran the real sync on every successful sign-in and logged DatabaseClosedError from it, and its notice fetch went past MSW to the network and logged a failed fetch, both after the tests that started them. The sync is now stubbed there, and the login, MFA and register tests assert that it was started. The notice store is reset before each test, since FE-STORE-AUTH-024 left a stubbed fetch behind. LoginPage.test.tsx signs in through the form, and AccountTab.test.tsx and MSettingsAccount.test.tsx reload the user after an MFA change; they now stub the sync triggers (and the login spec the trip sync), which none of them is about. GET /api/system-notices/active gets a default MSW handler returning no notices, so a login or a mounted notice host anywhere in the suite settles that fetch in the test instead of on the network. The notice store's own tests still override it. --- .../src/components/Settings/AccountTab.test.tsx | 6 ++++++ client/src/pages/LoginPage.test.tsx | 12 ++++++++++++ .../tests/helpers/msw/handlers/notifications.ts | 5 +++++ .../mobile/settings/MSettingsAccount.test.tsx | 6 ++++++ client/tests/unit/stores/authStore.test.ts | 16 ++++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/client/src/components/Settings/AccountTab.test.tsx b/client/src/components/Settings/AccountTab.test.tsx index 85bdfd21e..8bd5d180a 100644 --- a/client/src/components/Settings/AccountTab.test.tsx +++ b/client/src/components/Settings/AccountTab.test.tsx @@ -10,6 +10,12 @@ import { buildUser, buildSettings } from '../../../tests/helpers/factories'; import AccountTab from './AccountTab'; import { ToastContainer } from '../shared/Toast'; +// Enabling or disabling MFA reloads the user, and loadUser registers the +// background sync triggers: a 30 s heartbeat and window listeners that nothing +// here unregisters, so they outlive the test and can fire, and log, while the +// file's worker shuts down. Nothing here is about sync. +vi.mock('../../sync/syncTriggers', () => ({ registerSyncTriggers: vi.fn(), unregisterSyncTriggers: vi.fn() })); + beforeEach(() => { resetAllStores(); server.use( diff --git a/client/src/pages/LoginPage.test.tsx b/client/src/pages/LoginPage.test.tsx index d78530cb8..cd88cbd1c 100644 --- a/client/src/pages/LoginPage.test.tsx +++ b/client/src/pages/LoginPage.test.tsx @@ -13,6 +13,18 @@ import LoginPage from './LoginPage'; // made FE-PAGE-LOGIN-014 fail intermittently in CI while passing in isolation. vi.mock('./login/LoginWorld', () => ({ default: () => null })); +// A successful sign-in registers the background sync triggers (a 30 s heartbeat +// and window listeners that nothing here unregisters) and starts a full trip +// sync it does not wait for. Left real, both outlive the test that started them +// and can still be logging when the file's worker shuts down, which vitest +// reports as an unhandled error although every test passed. Nothing here is +// about sync. +vi.mock('../sync/syncTriggers', () => ({ registerSyncTriggers: vi.fn(), unregisterSyncTriggers: vi.fn() })); +vi.mock('../sync/tripSyncManager', async (importOriginal) => ({ + ...(await importOriginal()), + tripSyncManager: { syncAll: vi.fn(async () => {}) }, +})); + // LoginPage uses inline styles for labels (no htmlFor/id pairing). // We find inputs by placeholder text. const EMAIL_PLACEHOLDER = 'your@email.com'; diff --git a/client/tests/helpers/msw/handlers/notifications.ts b/client/tests/helpers/msw/handlers/notifications.ts index f009cee0f..aae795544 100644 --- a/client/tests/helpers/msw/handlers/notifications.ts +++ b/client/tests/helpers/msw/handlers/notifications.ts @@ -91,4 +91,9 @@ export const notificationHandlers = [ }, }); }), + + // A login and the notice host both start this fetch without waiting for it. + // Unhandled, MSW let it through to the network, where it could fail after the + // test had ended and log a warning into a worker that was shutting down. + http.get('/api/system-notices/active', () => HttpResponse.json([])), ]; diff --git a/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx b/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx index 850e8400a..17d6a567f 100644 --- a/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx @@ -22,6 +22,12 @@ vi.mock('react-router', async (importOriginal) => { vi.mock('@simplewebauthn/browser', () => ({ startRegistration: vi.fn() })); +// Enabling or disabling MFA reloads the user, and loadUser registers the +// background sync triggers: a 30 s heartbeat and window listeners that nothing +// here unregisters, so they outlive the test and can fire, and log, while the +// file's worker shuts down. Nothing here is about sync. +vi.mock('../../../../src/sync/syncTriggers', () => ({ registerSyncTriggers: vi.fn(), unregisterSyncTriggers: vi.fn() })); + const webauthn = startRegistration as unknown as ReturnType; const BACKUP_KEY = 'trek_mfa_backup_codes_pending'; diff --git a/client/tests/unit/stores/authStore.test.ts b/client/tests/unit/stores/authStore.test.ts index 305bebe3b..1189fce87 100644 --- a/client/tests/unit/stores/authStore.test.ts +++ b/client/tests/unit/stores/authStore.test.ts @@ -20,6 +20,18 @@ const syncTriggers = vi.hoisted(() => ({ })); vi.mock('../../../src/sync/syncTriggers', () => syncTriggers); +// login, completeMfaLogin and register start a full trip sync they do not wait +// for. Left real, it outlives the test that started it and can still be logging +// when the file's worker shuts down, which vitest reports as an unhandled error +// although every test passed (the authStore.push spec had the same flake). The +// stub keeps the sync out and lets the tests assert that it was started. +const tripSync = vi.hoisted(() => ({ syncAll: vi.fn(async () => {}) })); +vi.mock('../../../src/sync/tripSyncManager', () => ({ tripSyncManager: tripSync })); + +// The notice store is not part of resetAllStores, and FE-STORE-AUTH-024 swaps +// its fetch for a stub; each test starts from the real, unloaded store again. +const initialNoticeState = useSystemNoticeStore.getState(); + // The user-scoped offline DB is real (fake-indexeddb); only the reopen step is // made failable so the "auth still succeeds when the DB won't open" path runs. const dbControl = vi.hoisted(() => ({ reopenFails: false, reopenedFor: null as number | string | null })); @@ -37,6 +49,7 @@ vi.mock('../../../src/db/offlineDb', async (importOriginal) => { beforeEach(() => { resetAllStores(); + useSystemNoticeStore.setState(initialNoticeState, true); vi.clearAllMocks(); dbControl.reopenFails = false; dbControl.reopenedFor = null; @@ -63,6 +76,7 @@ describe('authStore', () => { expect(state.isAuthenticated).toBe(true); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); + expect(tripSync.syncAll).toHaveBeenCalledOnce(); }); }); @@ -194,6 +208,7 @@ describe('authStore', () => { expect(state.user).toEqual(user); expect(state.isAuthenticated).toBe(true); expect(state.isLoading).toBe(false); + expect(tripSync.syncAll).toHaveBeenCalledOnce(); }); }); @@ -273,6 +288,7 @@ describe('authStore', () => { expect(state.isAuthenticated).toBe(true); expect(state.isLoading).toBe(false); expect(connect).toHaveBeenCalledOnce(); + expect(tripSync.syncAll).toHaveBeenCalledOnce(); }); });