deadhead-space-freight-tycoon/apps/web/test/preferences.test.ts

83 lines
3.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPreferencesStore,
DEFAULT_PREFERENCES,
PREFERENCES_KEY,
reducedMotionOn,
type PreferenceStorage,
} from "../src/state/preferences";
/** An in-memory storage, like localStorage. */
function memoryStorage(): PreferenceStorage & { data: Map<string, string> } {
const data = new Map<string, string>();
return {
data,
getItem: (key) => data.get(key) ?? null,
setItem: (key, value) => void data.set(key, value),
};
}
/** jsdom has no matchMedia: stand one in that answers the reduced-motion query. */
const mockOsReducedMotion = (reduce: boolean) =>
vi.stubGlobal("matchMedia", (query: string) => ({ matches: reduce, media: query }));
describe("device preferences (Step 10K.1)", () => {
afterEach(() => vi.unstubAllGlobals());
it("starts with the defaults: locale clock, real-time countdowns, the device's reduced motion", () => {
const store = createPreferencesStore(memoryStorage());
expect(store.getState()).toMatchObject(DEFAULT_PREFERENCES);
mockOsReducedMotion(true);
expect(reducedMotionOn(store.getState())).toBe(true);
mockOsReducedMotion(false);
expect(reducedMotionOn(store.getState())).toBe(false);
// An override wins over the device.
store.getState().set({ reducedMotion: true });
expect(reducedMotionOn(store.getState())).toBe(true);
});
it("keeps saved values across a reload", () => {
const storage = memoryStorage();
const first = createPreferencesStore(storage);
first.getState().set({ clock: "24h", countdown: "both" });
first.getState().dismissTip("tutorial-intro");
first.getState().set({ mapCamera: { zoom: 3, panX: 10, panY: -4 } });
const reloaded = createPreferencesStore(storage);
expect(reloaded.getState()).toMatchObject({
clock: "24h",
countdown: "both",
dismissedTips: ["tutorial-intro"],
mapCamera: { zoom: 3, panX: 10, panY: -4 },
});
});
it("ignores broken saved data", () => {
const storage = memoryStorage();
storage.setItem(PREFERENCES_KEY, "{not json");
expect(createPreferencesStore(storage).getState()).toMatchObject(DEFAULT_PREFERENCES);
storage.setItem(
PREFERENCES_KEY,
JSON.stringify({ clock: "sundial", mapCamera: { zoom: "x" } }),
);
expect(createPreferencesStore(storage).getState()).toMatchObject(DEFAULT_PREFERENCES);
});
it("works in memory when storage throws, without errors", () => {
const broken: PreferenceStorage = {
getItem: () => {
throw new Error("SecurityError");
},
setItem: () => {
throw new Error("QuotaExceededError");
},
};
const store = createPreferencesStore(broken);
expect(store.getState()).toMatchObject(DEFAULT_PREFERENCES);
expect(() => store.getState().set({ clock: "12h" })).not.toThrow();
expect(store.getState().clock).toBe("12h");
expect(() => createPreferencesStore(null).getState().dismissTip("x")).not.toThrow();
});
});