feat(offline): gate the offline toggle on dictionary readiness
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m44s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m44s
Flipping the Settings toggle to offline now checks that every enabled variant's dictionary is on the device before entering offline mode: it fetches missing ones cache-first and waits up to ~5 s (raceOfflineReady + the lazy dict/offlineready), greying the toggle meanwhile. If they cannot be readied in time it stays online and shows a 'needs internet' note, while the fetch keeps warming the cache in the background so a later flip is instant. Leaving offline is never gated. Prevents entering a half-baked offline mode (no dawg -> cannot create/play a local game) when the background preload has not finished (poor connection, or an immediate flip right after install). - offline.ts: raceOfflineReady (pure, injected sleep; unit-tested red->green) - dict/offlineready.ts: ensureOfflineDicts (cache-first preloadDicts, lazy chunk) - offline.svelte.ts: requestOffline + TOGGLE_READY_BUDGET_MS - Settings.svelte: checking/needsData state, disabled toggle, inline note - i18n: settings.offlineChecking / settings.offlineNeedsData (en+ru) - docs: FUNCTIONAL(+_ru) offline story + ARCHITECTURE offline paragraph
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { loadOfflinePref, saveOfflinePref, offlineReady, missingDicts, offlinePreloadEligible, shouldBootOffline } from './offline';
|
||||
import { loadOfflinePref, saveOfflinePref, offlineReady, missingDicts, offlinePreloadEligible, shouldBootOffline, raceOfflineReady } from './offline';
|
||||
import type { Variant } from './model';
|
||||
|
||||
// A minimal in-memory localStorage for the persistence tests (node has none).
|
||||
@@ -53,3 +53,25 @@ describe('offline mode helpers', () => {
|
||||
expect(shouldBootOffline({ offlineActive: true, hasSession: true, hasProfile: false })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('raceOfflineReady (toggle-flip readiness wait)', () => {
|
||||
// A sleep that never elapses: the fetch always wins the race, exercising its resolution.
|
||||
const never = (): Promise<void> => new Promise<void>(() => {});
|
||||
// A sleep that elapses immediately: the budget always wins the race, exercising the timeout.
|
||||
const now = (): Promise<void> => Promise.resolve();
|
||||
|
||||
it('is ready when the fetch resolves with nothing failed before the budget', async () => {
|
||||
const run = Promise.resolve({ failed: [] as Variant[] });
|
||||
expect(await raceOfflineReady(run, 5000, never)).toBe(true);
|
||||
});
|
||||
|
||||
it('is not ready when a variant is still missing after the fetch', async () => {
|
||||
const run = Promise.resolve({ failed: ['erudit_ru'] as Variant[] });
|
||||
expect(await raceOfflineReady(run, 5000, never)).toBe(false);
|
||||
});
|
||||
|
||||
it('is not ready when the budget elapses before the fetch resolves', async () => {
|
||||
const run = new Promise<{ failed: Variant[] }>(() => {}); // never resolves
|
||||
expect(await raceOfflineReady(run, 5000, now)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user