Add text size and keyboard settings (#38)
Settings sets the text size (100, 125 or 150 %) and the key of each button, kept in this browser's storage and validated on load. Every font size scales with --text-scale, and the dialogue box scrolls instead of growing past the screen. The game screen's label, the Play message and Help's control list name the current keys. Screen reader announcements for every native screen and touch control remapping remain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29248172bd
commit
f289615bd2
10 files changed
+588
-41
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buttonKeys,
|
||||
defaultPreferences,
|
||||
keyMap,
|
||||
keyName,
|
||||
loadPreferences,
|
||||
rebind,
|
||||
savePreferences,
|
||||
} from "../src/ui/preferences";
|
||||
|
||||
function storage(initial: Record<string, string> = {}) {
|
||||
const values = new Map(Object.entries(initial));
|
||||
return {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void values.set(key, value),
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
describe("interface preferences", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("defaults to the original keys and standard text", () => {
|
||||
const preferences = defaultPreferences();
|
||||
expect(preferences.textScale).toBe(1);
|
||||
const map = keyMap(preferences);
|
||||
expect(map.get("KeyX")).toBe("a");
|
||||
expect(map.get("KeyZ")).toBe("b");
|
||||
expect(map.get("Enter")).toBe("start");
|
||||
expect(map.get("ShiftRight")).toBe("select");
|
||||
expect(buttonKeys(preferences, "select")).toBe("Shift");
|
||||
expect(buttonKeys(preferences, "up")).toBe("↑");
|
||||
});
|
||||
|
||||
it("names keys readably", () => {
|
||||
expect(keyName("KeyQ")).toBe("Q");
|
||||
expect(keyName("Digit4")).toBe("4");
|
||||
expect(keyName("ArrowLeft")).toBe("←");
|
||||
expect(keyName("Semicolon")).toBe("Semicolon");
|
||||
});
|
||||
|
||||
it("moves a key to its new button and refuses reserved keys", () => {
|
||||
let preferences = rebind(defaultPreferences(), "a", "KeyZ");
|
||||
expect(preferences.keys.a).toEqual(["KeyZ"]);
|
||||
expect(preferences.keys.b).toEqual([]);
|
||||
expect(buttonKeys(preferences, "b")).toBe("None");
|
||||
preferences = rebind(preferences, "start", "Tab");
|
||||
expect(preferences.keys.start).toEqual(["Enter"]);
|
||||
});
|
||||
|
||||
it("round-trips through storage and rejects invalid entries", () => {
|
||||
const store = storage();
|
||||
vi.stubGlobal("localStorage", store);
|
||||
const saved = { ...rebind(defaultPreferences(), "start", "KeyQ"), textScale: 1.5 as const };
|
||||
savePreferences(saved);
|
||||
expect(loadPreferences()).toEqual(saved);
|
||||
// A duplicated key or an unknown scale falls back to the defaults.
|
||||
store.values.set(
|
||||
"crystal-preferences-v1",
|
||||
JSON.stringify({ textScale: 3, keys: { ...saved.keys, b: ["KeyQ"] } }),
|
||||
);
|
||||
expect(loadPreferences()).toEqual(defaultPreferences());
|
||||
store.values.set("crystal-preferences-v1", "{not json");
|
||||
expect(loadPreferences()).toEqual(defaultPreferences());
|
||||
});
|
||||
|
||||
it("falls back to the defaults without storage", () => {
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
});
|
||||
expect(loadPreferences()).toEqual(defaultPreferences());
|
||||
expect(() => savePreferences(defaultPreferences())).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user