54d701fd8a
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
An installed PWA relaunched with the network off hung on the splash: C1's precache served the shell, but bootstrap() adopted the session and fetched the profile over the network, which hung. Persist the profile (on every online adopt + refresh, cleared on logout) and short-circuit bootstrap when the deliberate offline flag is sticky-on: with a cached session + profile, skip the network and land straight in the offline lobby. Without a cached profile (never online), drop the sticky flag and boot online — the first launch must be online. - session.ts: saveProfile/loadProfile/clearProfile (mirror saveSession). - offline.ts: shouldBootOffline decision (unit-tested). - app.svelte.ts: persist in adoptSession + refreshProfile, clear on logout, the offline boot short-circuit in bootstrap. Docs: ARCHITECTURE. Online cold-start unaffected (e2e 196); the offline boot is contour-verified (the mock e2e cannot enter sticky-offline).
56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { loadOfflinePref, saveOfflinePref, offlineReady, missingDicts, offlinePreloadEligible, shouldBootOffline } 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']);
|
|
});
|
|
|
|
it('offlinePreloadEligible requires a standalone PWA with email, online, outside mini-apps', () => {
|
|
const base = { hasEmail: true, standalone: true, inTelegram: false, inVK: false, online: true };
|
|
expect(offlinePreloadEligible(base)).toBe(true);
|
|
expect(offlinePreloadEligible({ ...base, hasEmail: false })).toBe(false);
|
|
expect(offlinePreloadEligible({ ...base, standalone: false })).toBe(false);
|
|
expect(offlinePreloadEligible({ ...base, inTelegram: true })).toBe(false);
|
|
expect(offlinePreloadEligible({ ...base, inVK: true })).toBe(false);
|
|
expect(offlinePreloadEligible({ ...base, online: false })).toBe(false);
|
|
});
|
|
|
|
it('shouldBootOffline requires the offline flag plus a cached session and profile', () => {
|
|
expect(shouldBootOffline({ offlineActive: true, hasSession: true, hasProfile: true })).toBe(true);
|
|
expect(shouldBootOffline({ offlineActive: false, hasSession: true, hasProfile: true })).toBe(false);
|
|
expect(shouldBootOffline({ offlineActive: true, hasSession: false, hasProfile: true })).toBe(false);
|
|
expect(shouldBootOffline({ offlineActive: true, hasSession: true, hasProfile: false })).toBe(false);
|
|
});
|
|
});
|