mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
3f9b751ee3
A connect still waiting for its ws token when the user logged out went on to open a socket authenticated as that user, which the next login on the same browser then reused. And a socket in its reconnect backoff waited up to 30 s after the network came back or the tab became visible. Both events now reconnect right away.
131 lines
4.5 KiB
TypeScript
131 lines
4.5 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() },
|
|
}));
|
|
let reconnects = 0;
|
|
vi.mock('../../../src/api/websocket', () => ({
|
|
setPreReconnectHook: (fn: (() => Promise<void>) | null) => { preReconnect = fn; },
|
|
setRefetchCallback: (fn: ((tripId: string) => void) | null) => { refetchCb = fn; },
|
|
getActiveTrips: () => ['7'],
|
|
reconnectNow: () => { reconnects++; },
|
|
}));
|
|
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; reconnects = 0;
|
|
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 and a tab coming back reconnect the socket right away instead of waiting out its backoff', async () => {
|
|
registerSyncTriggers();
|
|
window.dispatchEvent(new Event('online'));
|
|
await flushMicrotasks();
|
|
expect(reconnects).toBe(1);
|
|
|
|
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
await flushMicrotasks();
|
|
expect(reconnects).toBe(2);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|