68ecd881d9
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 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The deliberate offline mode is now visible: an installed web-PWA user with a confirmed email can switch to offline in Settings, and the header turns blue with an "Offline" chip. (Network gating, the dict preload, the offline lobby + local-game creation, and the service-worker precache follow in later Phase C steps.) - offline.ts / offline.svelte.ts: a sticky, device-scoped reactive offline flag (offlineMode), distinct from connection.svelte's transient reachability signal, with the pure persistence + readiness helpers (offlineReady / missingDicts) unit-tested. - Settings.svelte: an Online / Offline toggle, shown only for an installed web PWA with a confirmed email (the SW-launch + durable-account preconditions). - Header.svelte: a blue-tinted nav (color-mix from the accent, so it tracks light/dark and a Telegram theme override) + an "Offline" chip while offline; the deliberate mode suppresses the transient "Connecting…" indicator. Behaviour-preserving online (the toggle is hidden and offlineMode is false there; e2e 196 green). App entry stays within its size budget (111.6 / 112 KB gzip).
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { loadOfflinePref, saveOfflinePref, offlineReady, missingDicts } from './offline';
|
|
import type { Variant } from './model';
|
|
|
|
// A minimal in-memory localStorage for the persistence tests (node has none).
|
|
beforeEach(() => {
|
|
const store = new Map<string, string>();
|
|
(globalThis as unknown as { localStorage: Storage }).localStorage = {
|
|
getItem: (k: string) => store.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void store.set(k, v),
|
|
removeItem: (k: string) => void store.delete(k),
|
|
clear: () => store.clear(),
|
|
key: () => null,
|
|
length: 0,
|
|
} as Storage;
|
|
});
|
|
|
|
describe('offline mode helpers', () => {
|
|
it('persists and reads the device-scoped offline flag', () => {
|
|
expect(loadOfflinePref()).toBe(false);
|
|
saveOfflinePref(true);
|
|
expect(loadOfflinePref()).toBe(true);
|
|
saveOfflinePref(false);
|
|
expect(loadOfflinePref()).toBe(false);
|
|
});
|
|
|
|
it('offlineReady requires every enabled variant to have a dictionary', () => {
|
|
const has = (v: Variant): boolean => v !== 'erudit_ru';
|
|
expect(offlineReady(['scrabble_en', 'scrabble_ru'], has)).toBe(true);
|
|
expect(offlineReady(['scrabble_en', 'erudit_ru'], has)).toBe(false);
|
|
expect(offlineReady([], has)).toBe(false);
|
|
});
|
|
|
|
it('missingDicts lists the enabled variants without a dictionary', () => {
|
|
const has = (v: Variant): boolean => v === 'scrabble_en';
|
|
expect(missingDicts(['scrabble_en', 'scrabble_ru', 'erudit_ru'], has)).toEqual(['scrabble_ru', 'erudit_ru']);
|
|
});
|
|
});
|