src/lib/entry-units.test.ts
Rama main · réplica de lectura del repositorio original.
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
ENTRY_UNITS_KEY,
clearEntryUnits,
parseEntryUnits,
readStoredEntryUnits,
subscribeToEntryUnits,
toggleDimension,
writeEntryUnits,
} from "@/lib/entry-units";
import { memoryStorage, useStorage } from "@/test/memory-storage";
// `window.localStorage` is installed per test rather than used as jsdom provides
// it - see `test/memory-storage.ts` for why. Without it every read here would go
// through the module's try/catch and come back as "no override", and this whole
// file would pass with the feature deleted.
// The pair every consumer composes: read the raw entry, then parse it.
function readOverrides() {
return parseEntryUnits(readStoredEntryUnits());
}
function stored(): string | null {
return window.localStorage.getItem(ENTRY_UNITS_KEY);
}
describe("readStoredEntryUnits / writeEntryUnits", () => {
beforeEach(() => {
useStorage(memoryStorage());
});
it("has no overrides before anything is stored", () => {
expect(readOverrides()).toEqual({});
expect(readStoredEntryUnits()).toBeNull();
});
it("round-trips one dimension", () => {
writeEntryUnits({ pressure: "imperial" });
expect(readOverrides()).toEqual({ pressure: "imperial" });
});
it("round-trips several dimensions independently", () => {
writeEntryUnits({ pressure: "imperial", depth: "metric" });
expect(readOverrides()).toEqual({ pressure: "imperial", depth: "metric" });
});
// No overrides is the natural state, so it is spelled as no entry rather than
// as a stored `{}` that would mean the same thing.
it("removes the key rather than storing an empty record", () => {
writeEntryUnits({ pressure: "imperial" });
expect(stored()).not.toBeNull();
writeEntryUnits({});
expect(stored()).toBeNull();
expect(readOverrides()).toEqual({});
});
// `getSnapshot` runs on every render of every consumer, and two of those sit on
// the dive form's hottest path - `MixtureSetWarning` re-renders per keystroke.
it("serves repeat reads from the cache instead of hitting storage", () => {
const storage = memoryStorage();
const getItem = vi.spyOn(storage, "getItem");
useStorage(storage);
readStoredEntryUnits();
readStoredEntryUnits();
readStoredEntryUnits();
expect(getItem).toHaveBeenCalledTimes(1);
});
// The property that lets the cache survive per-file module state without a
// test-only reset export: a fresh store is a different object, so it re-primes.
it("re-primes when the storage object itself changes", () => {
writeEntryUnits({ weight: "imperial" });
expect(readOverrides()).toEqual({ weight: "imperial" });
useStorage(memoryStorage());
expect(readOverrides()).toEqual({});
});
// A store that already holds an entry, standing in for the next page load
// reading back what a previous visit left behind.
it("reads an entry this session never wrote", () => {
const storage = memoryStorage();
storage.setItem(ENTRY_UNITS_KEY, JSON.stringify({ depth: "imperial" }));
useStorage(storage);
expect(readOverrides()).toEqual({ depth: "imperial" });
});
});
describe("parseEntryUnits", () => {
// Filtered rather than rejected, like `parseSeriesVisibility`: one unusable
// entry does not throw away a perfectly good record around it.
it("drops a dimension this build has never heard of", () => {
expect(
parseEntryUnits(
JSON.stringify({ pressure: "imperial", buoyancy: "imperial" }),
),
).toEqual({ pressure: "imperial" });
});
it("drops a dimension whose value is not a unit system", () => {
expect(
parseEntryUnits(JSON.stringify({ pressure: "psi", depth: "imperial" })),
).toEqual({ depth: "imperial" });
});
it("drops a dimension holding something that is not a string at all", () => {
expect(
parseEntryUnits(JSON.stringify({ pressure: 3000, depth: "imperial" })),
).toEqual({ depth: "imperial" });
});
it("reads a non-object entry as no overrides", () => {
expect(parseEntryUnits(JSON.stringify("imperial"))).toEqual({});
expect(parseEntryUnits(JSON.stringify(7))).toEqual({});
expect(parseEntryUnits(JSON.stringify(null))).toEqual({});
expect(parseEntryUnits(JSON.stringify(["depth"]))).toEqual({});
});
it("reads a malformed entry as no overrides", () => {
expect(parseEntryUnits("{not json")).toEqual({});
});
it("reads an absent or empty entry as no overrides", () => {
expect(parseEntryUnits(null)).toEqual({});
expect(parseEntryUnits("")).toEqual({});
});
});
describe("clearEntryUnits", () => {
beforeEach(() => {
useStorage(memoryStorage());
});
it("forgets every override", () => {
writeEntryUnits({ pressure: "imperial", weight: "imperial" });
clearEntryUnits();
expect(stored()).toBeNull();
expect(readOverrides()).toEqual({});
});
it("is harmless when there was nothing stored", () => {
clearEntryUnits();
expect(readOverrides()).toEqual({});
});
});
describe("subscribeToEntryUnits", () => {
beforeEach(() => {
useStorage(memoryStorage());
});
it("tells every listener about a write", () => {
const first = vi.fn();
const second = vi.fn();
subscribeToEntryUnits(first);
subscribeToEntryUnits(second);
writeEntryUnits({ depth: "imperial" });
expect(first).toHaveBeenCalledTimes(1);
expect(second).toHaveBeenCalledTimes(1);
});
it("tells every listener about a clear", () => {
const listener = vi.fn();
const unsubscribe = subscribeToEntryUnits(listener);
clearEntryUnits();
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
});
it("stops telling a listener that has unsubscribed", () => {
const listener = vi.fn();
const unsubscribe = subscribeToEntryUnits(listener);
unsubscribe();
writeEntryUnits({ depth: "imperial" });
expect(listener).not.toHaveBeenCalled();
});
});
describe("toggleDimension", () => {
// The absolute system is what gets recorded - "my SPG reads psi" - not
// "flipped", so re-theming the account later does not invert the override.
it("stores the other system for a metric account", () => {
expect(toggleDimension({}, "pressure", "metric")).toEqual({
pressure: "imperial",
});
});
it("stores the other system for an imperial account", () => {
expect(toggleDimension({}, "pressure", "imperial")).toEqual({
pressure: "metric",
});
});
// No-override is the natural state, so flipping back removes the key rather
// than storing a value equal to the account's - which is what lets a later
// account-level change carry every unoverridden dimension with it.
it("removes the key when flipped back to the account system", () => {
const flipped = toggleDimension({}, "pressure", "metric");
expect(toggleDimension(flipped, "pressure", "metric")).toEqual({});
});
it("leaves the other dimensions alone", () => {
const overrides = toggleDimension(
{ depth: "imperial", weight: "imperial" },
"pressure",
"metric",
);
expect(overrides).toEqual({
depth: "imperial",
weight: "imperial",
pressure: "imperial",
});
});
it("does not mutate the record it was given", () => {
const before = { depth: "imperial" as const };
toggleDimension(before, "pressure", "metric");
expect(before).toEqual({ depth: "imperial" });
});
// The state a diver reaches by flipping pressure to psi and then switching the
// account itself to imperial: the entry still says "imperial", which is now
// simply redundant. Toggling from there has to move it, not read as already-off.
it("moves a redundant override off the account system", () => {
expect(
toggleDimension({ pressure: "imperial" }, "pressure", "imperial"),
).toEqual({ pressure: "metric" });
});
});
describe("when the browser hands out no usable storage", () => {
// Safari private mode, storage disabled, quota - and under this runner, the
// ordinary state before `useStorage` is called. Entering in account units is a
// fine outcome, not an error case.
// Each of these primes a real store first, so the assertion is that the broken
// storage was actually consulted rather than that the cache happened to be
// empty already.
beforeEach(() => {
useStorage(memoryStorage());
writeEntryUnits({ pressure: "imperial" });
});
it("reads as no overrides when storage is absent", () => {
useStorage(undefined);
expect(readOverrides()).toEqual({});
});
it("reads as no overrides when reaching for storage throws", () => {
Object.defineProperty(window, "localStorage", {
configurable: true,
get() {
throw new Error("storage is blocked");
},
});
expect(readOverrides()).toEqual({});
});
it("reads as no overrides when the read itself throws", () => {
const storage = memoryStorage();
vi.spyOn(storage, "getItem").mockImplementation(() => {
throw new Error("storage is blocked");
});
useStorage(storage);
expect(readOverrides()).toEqual({});
});
// The toggle still flips for this session, deliberately: the choice is not
// remembered, which is honest, but a refused write must not leave a dead
// control on the form.
it("still flips the boxes when the write is refused", () => {
const storage = memoryStorage();
vi.spyOn(storage, "setItem").mockImplementation(() => {
throw new Error("quota exceeded");
});
useStorage(storage);
writeEntryUnits({ pressure: "imperial" });
expect(readOverrides()).toEqual({ pressure: "imperial" });
});
it("survives a refused remove", () => {
const storage = memoryStorage();
vi.spyOn(storage, "removeItem").mockImplementation(() => {
throw new Error("storage is blocked");
});
useStorage(storage);
expect(() => clearEntryUnits()).not.toThrow();
expect(readOverrides()).toEqual({});
});
it("survives a refused remove on an emptied record", () => {
const storage = memoryStorage();
vi.spyOn(storage, "removeItem").mockImplementation(() => {
throw new Error("storage is blocked");
});
useStorage(storage);
expect(() => writeEntryUnits({})).not.toThrow();
expect(readOverrides()).toEqual({});
});
});