tests/settingsForm.test.ts
Rama main · réplica de lectura del repositorio original.
// The settings form, out of the dialog (3.27.0). SettingsModal.tsx was split
// so the phone shell could host the same tabs as pushed screens; these hold
// the two things the split must not have moved: the form's round trip with
// the server (what the panel loads, it can save back unchanged, and an edit
// becomes a patch of exactly that key which the server accepts), and one
// switch that draws every tab the rail lists, in both shells.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { after, before, describe, it } from "node:test";
import { buildPatch, formFrom, validate } from "../client/components/settings/form.ts";
import { POCKET_HIDDEN_TABS, TABS, tabIntro } from "../client/components/settings/tabs.ts";
import { patchSettings, settingsResponse } from "../server/settings.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { makeDir, makeVault, removeVault } from "./helpers/vault.ts";
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const read = (p: string): string => readFileSync(path.join(repo, p), "utf8");
const data = makeDir();
const root = makeVault({ "Home.md": "# Home\n", "Templates/Daily.md": "# {{date}}\n" });
before(() => {
initSite({ ASTROLABE_DATA: data });
initVault(root);
});
after(() => {
removeVault(root);
removeVault(data);
});
describe("the settings form, as both hosts hold it", () => {
it("loads clean: no errors and an empty patch", () => {
const form = formFrom(settingsResponse());
assert.deepEqual(validate(form), {});
assert.deepEqual(buildPatch(form, form), {});
});
it("an edit is a patch of that key, and the server takes it back as the form", () => {
const initial = formFrom(settingsResponse());
const edited = { ...initial, tagline: "Notes from the canopy", syncInterval: "60", language: "ar" };
assert.deepEqual(validate(edited), {});
const patch = buildPatch(initial, edited);
assert.deepEqual(Object.keys(patch).sort(), ["gitSync", "language", "tagline"]);
assert.equal(patch.tagline, "Notes from the canopy");
const after = formFrom(patchSettings(patch as Record<string, unknown>));
assert.equal(after.tagline, "Notes from the canopy");
assert.equal(after.language, "ar");
assert.equal(after.syncInterval, "60");
// Saved, the form is clean again — the Save button goes quiet.
assert.deepEqual(buildPatch(after, after), {});
assert.deepEqual(buildPatch(edited, after), {});
});
it("every switch saves on its own — Share buttons without Comments beside it", () => {
// The settings walk found it: "Share buttons" rode inside the comments'
// condition in buildPatch, so switched alone it raised the Save bar and
// saved nothing. Each two-state row must patch its own key, alone.
const initial = formFrom(settingsResponse());
const off = { ...initial, share: "off" };
const patch = buildPatch(initial, off);
assert.deepEqual(Object.keys(patch), ["shareButtons"]);
assert.equal(patch.shareButtons, false);
const after = formFrom(patchSettings(patch as Record<string, unknown>));
assert.equal(after.share, "off");
for (const key of ["ambient", "externalVideo", "languageToggle", "speakPublic", "emptyPropsCard", "propsCard", "wmAccept", "wmSend", "fediEnabled", "feedsFetch", "voiceKeepAudio", "publicFoldersOn", "publicFoldersHome", "publicFoldersNav", "libraryOn", "libraryNav", "libraryHome", "syncPullFirst"] as const) {
const flipped = { ...initial, [key]: initial[key] === "on" ? "off" : "on" };
assert.ok(Object.keys(buildPatch(initial, flipped)).length > 0, `${key} switched alone saves nothing`);
}
});
it("the Discord page patches only what moved, and a default goes back as null", () => {
const initial = formFrom(settingsResponse());
assert.equal(initial.presenceMode, "off", "presence is off until the owner says so");
const on = { ...initial, presenceMode: "title", presenceClientId: " 123456789012345678 ", presenceIdleMinutes: "30", presenceServer: "on" };
assert.deepEqual(validate(on), {});
assert.deepEqual(buildPatch(initial, on), { presence: { mode: "title", clientId: "123456789012345678", idleMinutes: 30, server: true } });
const back = { ...on, presenceMode: "off", presenceIdleMinutes: "10", presenceServer: "off", presenceClientId: "" };
assert.deepEqual(buildPatch(on, back).presence, { mode: null, clientId: null, idleMinutes: null, server: null });
for (const id of ["1234567890123456", "123456789012345678901", "ID 123456789012345678"]) {
assert.ok("presenceClientId" in validate({ ...initial, presenceClientId: id }), `${id} passed as an application id`);
}
});
it("the server takes the Discord page's patch back as the form", () => {
const initial = formFrom(settingsResponse());
const on = { ...initial, presenceMode: "activity", presenceClientId: "123456789012345678", presenceElapsed: "session", presenceAssets: "uploaded" };
const after = formFrom(patchSettings(buildPatch(initial, on) as Record<string, unknown>));
assert.equal(after.presenceMode, "activity");
assert.equal(after.presenceClientId, "123456789012345678");
assert.equal(after.presenceElapsed, "session");
assert.equal(after.presenceAssets, "uploaded");
assert.deepEqual(buildPatch(after, after), {});
// And back off: the defaults are stored as nothing.
const off = formFrom(patchSettings(buildPatch(after, { ...after, presenceMode: "off", presenceClientId: "", presenceElapsed: "subject", presenceAssets: "site" }) as Record<string, unknown>));
assert.equal(off.presenceMode, "off");
assert.equal(off.presenceClientId, "");
});
it("a field the rules refuse is named, and a refused form is not clean", () => {
const initial = formFrom(settingsResponse());
const bad = { ...initial, blogLocale: "not a locale!!" };
assert.ok("blogLocale" in validate(bad));
});
});
describe("one switch draws every tab", () => {
const body = read("client/components/settings/TabBody.tsx");
it("every tab the rail lists has a body line in TabBody.tsx", () => {
for (const tab of TABS) assert.match(body, new RegExp(`\\{tab === "${tab.id}" &&`), tab.id);
});
it("the tabs a pocket vault hides are instance-only in the switch", () => {
for (const id of POCKET_HIDDEN_TABS) assert.match(body, new RegExp(`\\{tab === "${id}" && !pocket &&`), id);
});
it("the dialog no longer carries a tab body of its own", () => {
const modal = read("client/components/SettingsModal.tsx");
assert.doesNotMatch(modal, /\{tab === "[a-z]+" &&/);
assert.match(modal, /<TabBody tab=\{tab\} \/>/);
});
it("the pocket's Backup & sync says what the phone does", () => {
assert.equal(tabIntro("sync", true), "pocketSyncNote");
assert.equal(tabIntro("sync", false), "syncNote");
});
});