import { describe, expect, it } from "vitest";
import {
LANG_STORAGE_KEY,
PREFS_INLINE_SCRIPT,
THEME_STORAGE_KEY,
parseSavedLang,
parseSavedTheme,
} from "@/lib/ui-prefs";
import type { Lang } from "@/lib/i18n";
import type { ThemeName } from "@/lib/theme";
/**
* 表示設定(ダーク/ライト・JP/EN)の保存(ユーザー決定 2026-10-19 U1。指示書BC 段階3。D-183)。
* ★許す値は "dark" / "light" と "ja" / "en" だけ★。それ以外は null(既定で描く)。
*/
describe("parseSavedTheme — 保存値をテーマとして読む", () => {
const cases: Array<[string, string | null, ThemeName | null]> = [
["T1 dark", "dark", "dark"],
["T2 light", "light", "light"],
["T3 null(保存なし)", null, null],
["T4 大文字違いは許さない", "Light", null],
["T5 空文字", "", null],
];
it.each(cases)("%s", (_name, input, expected) => {
expect(parseSavedTheme(input)).toBe(expected);
});
});
describe("parseSavedLang — 保存値を言語として読む", () => {
const cases: Array<[string, string | null, Lang | null]> = [
["L1 ja", "ja", "ja"],
["L2 en", "en", "en"],
["L3 null(保存なし)", null, null],
["L4 jp(違う綴り)", "jp", null],
];
it.each(cases)("%s", (_name, input, expected) => {
expect(parseSavedLang(input)).toBe(expected);
});
});
/**
* `<head>` で描く前に動かすスクリプト。★`vitest.config.ts` の環境は node なので DOM は使わず★、
* 偽の `document`(`documentElement.setAttribute` を Map に記録する)と偽の `localStorage` を渡して呼ぶ。
* 偽の `<html>` は最初 `data-theme=dark`・`lang=ja`(`app/layout.tsx` の既定と同じ)。
*/
describe("PREFS_INLINE_SCRIPT — 描く前に <html> の属性を保存値に合わせる", () => {
function run(getItem: (key: string) => string | null): Map<string, string> {
const attrs = new Map<string, string>([
["data-theme", "dark"],
["lang", "ja"],
]);
const fakeDocument = {
documentElement: {
setAttribute: (name: string, value: string) => {
attrs.set(name, value);
},
},
};
const fakeStorage = { getItem };
const script = new Function("document", "localStorage", PREFS_INLINE_SCRIPT);
script(fakeDocument, fakeStorage);
return attrs;
}
function storageOf(values: Record<string, string>): (key: string) => string | null {
return (key) => (key in values ? values[key] : null);
}
it("S1 保存 light / en → data-theme=light・lang=en", () => {
const attrs = run(storageOf({ [THEME_STORAGE_KEY]: "light", [LANG_STORAGE_KEY]: "en" }));
expect(attrs.get("data-theme")).toBe("light");
expect(attrs.get("lang")).toBe("en");
});
it("S2 許さない値(blue / fr)→ 変えない(dark / ja のまま)", () => {
const attrs = run(storageOf({ [THEME_STORAGE_KEY]: "blue", [LANG_STORAGE_KEY]: "fr" }));
expect(attrs.get("data-theme")).toBe("dark");
expect(attrs.get("lang")).toBe("ja");
});
it("S3 getItem が例外を投げる → 例外が外に出ない・変えない", () => {
// 例外が外に出たら attrs は空のまま(下の2行も落ちる)
let attrs = new Map<string, string>();
expect(() => {
attrs = run(() => {
throw new Error("SecurityError");
});
}).not.toThrow();
expect(attrs.get("data-theme")).toBe("dark");
expect(attrs.get("lang")).toBe("ja");
});
it("S4 何も保存されていない → 変えない", () => {
const attrs = run(() => null);
expect(attrs.get("data-theme")).toBe("dark");
expect(attrs.get("lang")).toBe("ja");
});
});