mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
47a4276e84
logout() unregistered every sync trigger, and the only place that registers them is App's mount effect - which never runs again, because logout navigates through the router rather than reloading. Log out, log back in, go offline, edit something: it queues and nothing ever flushes it. No online listener, no visibilitychange, no 30s heartbeat, no WS reconnect hook. Register on auth success instead; registerSyncTriggers is already idempotent. The background sync loops only checked the auth gate on entry. Logout swaps the Dexie handle to the shared anonymous database, so a trip loop or a blob download still in flight afterwards wrote the logged-out account's itinerary and documents into it - the database that survives logout and greets the next person on a shared device. Check the gate per iteration. And the remote place:updated applier replaced the whole embedded place, while the local reducer right next to it preserves the assignment's own times. Same event, two answers, and the remote one wipes a scheduled 09:00-10:30 off the itinerary when someone else renames the place. One merge helper now, used by both, so they cannot drift again. The test that should have caught it seeded null times and asserted only the name. Also: moving a day note deleted before it created, and restored the note only locally when the create failed; t() interpolation was corrupted by a '$' in a parameter value; a 429 upload was treated as permanently failed; and the geolocation watch was started inside a state updater.
117 lines
4.0 KiB
TypeScript
117 lines
4.0 KiB
TypeScript
/**
|
|
* syncTriggers — reconnect/online wiring (H1).
|
|
*
|
|
* Verifies the previously-dead refetch path is wired: on WS reconnect and on the
|
|
* `online` event the active trip's store is re-hydrated (after the queue flush).
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
const flush = vi.fn(() => Promise.resolve());
|
|
const syncAll = vi.fn(() => Promise.resolve());
|
|
const hydrate = vi.fn(() => Promise.resolve());
|
|
const loadSettings = vi.fn(() => Promise.resolve());
|
|
|
|
let refetchCb: ((tripId: string) => void) | null = null;
|
|
let preReconnect: (() => Promise<void>) | null = null;
|
|
let settingsLoaded = false;
|
|
let authenticated = true;
|
|
|
|
vi.mock('../../../src/sync/mutationQueue', () => ({
|
|
mutationQueue: { flush: () => flush() },
|
|
}));
|
|
vi.mock('../../../src/sync/tripSyncManager', () => ({
|
|
tripSyncManager: { syncAll: () => syncAll() },
|
|
}));
|
|
vi.mock('../../../src/api/websocket', () => ({
|
|
setPreReconnectHook: (fn: (() => Promise<void>) | null) => { preReconnect = fn; },
|
|
setRefetchCallback: (fn: ((tripId: string) => void) | null) => { refetchCb = fn; },
|
|
getActiveTrips: () => ['7'],
|
|
}));
|
|
vi.mock('../../../src/store/tripStore', () => ({
|
|
useTripStore: { getState: () => ({ hydrateActiveTrip: hydrate }) },
|
|
}));
|
|
vi.mock('../../../src/store/settingsStore', () => ({
|
|
useSettingsStore: { getState: () => ({ isLoaded: settingsLoaded, loadSettings }) },
|
|
}));
|
|
vi.mock('../../../src/store/authStore', () => ({
|
|
useAuthStore: { getState: () => ({ isAuthenticated: authenticated }) },
|
|
}));
|
|
|
|
import { registerSyncTriggers, unregisterSyncTriggers } from '../../../src/sync/syncTriggers';
|
|
|
|
const flushMicrotasks = async () => {
|
|
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
flush.mockClear(); syncAll.mockClear(); hydrate.mockClear(); loadSettings.mockClear();
|
|
refetchCb = null; preReconnect = null;
|
|
settingsLoaded = false; authenticated = true;
|
|
Object.defineProperty(navigator, 'onLine', { value: true, writable: true, configurable: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
unregisterSyncTriggers();
|
|
});
|
|
|
|
describe('syncTriggers', () => {
|
|
it('registers a refetch callback that hydrates the active trip', () => {
|
|
registerSyncTriggers();
|
|
expect(refetchCb).toBeTypeOf('function');
|
|
refetchCb!('7');
|
|
expect(hydrate).toHaveBeenCalledWith('7');
|
|
});
|
|
|
|
it('also registers the pre-reconnect flush hook', () => {
|
|
registerSyncTriggers();
|
|
expect(preReconnect).toBeTypeOf('function');
|
|
});
|
|
|
|
it('clears both reconnect hooks on unregister', () => {
|
|
registerSyncTriggers();
|
|
unregisterSyncTriggers();
|
|
expect(refetchCb).toBeNull();
|
|
expect(preReconnect).toBeNull();
|
|
});
|
|
|
|
it('online event flushes, then re-seeds Dexie and re-hydrates active trips', async () => {
|
|
registerSyncTriggers();
|
|
window.dispatchEvent(new Event('online'));
|
|
await flushMicrotasks();
|
|
|
|
expect(flush).toHaveBeenCalled();
|
|
expect(syncAll).toHaveBeenCalled();
|
|
expect(hydrate).toHaveBeenCalledWith('7');
|
|
// The order is the point: re-seeding Dexie before the queue drains would
|
|
// overwrite edits that have not been sent yet.
|
|
expect(flush.mock.invocationCallOrder[0]).toBeLessThan(syncAll.mock.invocationCallOrder[0]);
|
|
expect(flush.mock.invocationCallOrder[0]).toBeLessThan(hydrate.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it('online event retries the settings load when it has not yet succeeded (#1618)', async () => {
|
|
registerSyncTriggers();
|
|
window.dispatchEvent(new Event('online'));
|
|
await flushMicrotasks();
|
|
|
|
expect(loadSettings).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not retry the settings load once it has loaded', async () => {
|
|
settingsLoaded = true;
|
|
registerSyncTriggers();
|
|
window.dispatchEvent(new Event('online'));
|
|
await flushMicrotasks();
|
|
|
|
expect(loadSettings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not retry the settings load while unauthenticated', async () => {
|
|
authenticated = false;
|
|
registerSyncTriggers();
|
|
window.dispatchEvent(new Event('online'));
|
|
await flushMicrotasks();
|
|
|
|
expect(loadSettings).not.toHaveBeenCalled();
|
|
});
|
|
});
|