83 lines
3.0 KiB
TypeScript
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();
|
|
});
|
|
});
|