// 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)); 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)); 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)); 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)); 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, //); }); it("the pocket's Backup & sync says what the phone does", () => { assert.equal(tabIntro("sync", true), "pocketSyncNote"); assert.equal(tabIntro("sync", false), "syncNote"); }); });