import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { getConfig, type OnDialogSettings } from "./settingConstants.ts"; import { renderLegacySettingSpec, toLegacySettingBinding, toObsidianSettingDefinition, type PersistedSettingKey, type SettingSpec, } from "./SettingSpec.ts"; import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts"; import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts"; const rangeMessages = { valueShouldBeInRange: ({ min, max }: { min?: number; max?: number }) => `${min ?? "~"}..${max ?? "~"}`, }; describe("Advanced setting specifications", () => { it("lists only the nine controls currently exposed by the Advanced page", () => { const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => true }); expect(groups.map(({ heading }) => heading)).toEqual([ "Memory cache", "Local Database Tweak", "Transfer Tweak", "Remote Database Tweak", ]); expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([ "hashCacheMaxCount", "chunkSplitterVersion", "customChunkSize", "readChunksOnline", "useOnlyLocalChunk", "concurrencyOfReadChunksOnline", "minimumIntervalOfReadChunksOnline", "autoAcceptCompatibleTweak", "enableCompression", ]); }); it("keeps CouchDB visibility on the four remote chunk controls", () => { let couchDB = false; const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => couchDB }); const conditional = groups.flatMap(({ items }) => items).filter(({ visible }) => visible !== undefined); expect(conditional.map(({ key }) => key)).toEqual([ "readChunksOnline", "useOnlyLocalChunk", "concurrencyOfReadChunksOnline", "minimumIntervalOfReadChunksOnline", ]); expect(conditional.every(({ visible }) => visible?.() === false)).toBe(true); couchDB = true; expect(conditional.every(({ visible }) => visible?.() === true)).toBe(true); }); it("excludes transient settings-dialogue keys from the persisted key type", () => { type OnDialogOverlap = Extract; expectTypeOf().toEqualTypeOf(); }); it("resolves translated Commonlib metadata for every explicitly exposed key", () => { const specs = createAdvancedSettingSpecGroups({ isCouchDB: () => true }).flatMap(({ items }) => items); for (const spec of specs) { const metadata = getConfig(spec.key); expect(metadata, spec.key).not.toBe(false); if (!metadata) { throw new Error(`Missing setting metadata for ${spec.key}`); } const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages); expect(definition.name).toBe(`${metadata.name}${statusDisplay(metadata.status)}`); } }); }); describe("General setting specifications", () => { it("shares every General and Logging control between the imperative and declarative renderers", () => { const groups = createGeneralSettingSpecGroups({ showEditorStatusDetails: () => true, showVerboseLog: () => true, }); expect(groups.map(({ heading }) => heading)).toEqual(["Appearance", "Logging"]); expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([ "displayLanguage", "showStatusOnEditor", "showOnlyIconsOnEditor", "showStatusOnStatusbar", "hideFileWarningNotice", "networkWarningStyle", "lessInformationInLog", "showVerboseLog", ]); }); it("keeps the three feature-level controls together under Extra menus", () => { const group = createExtraMenuSettingSpecGroup(); expect(group.heading).toBe("Extra menus"); expect(group.items.map(({ key }) => key)).toEqual(["useAdvancedMode", "usePowerUserMode", "useEdgeCaseMode"]); }); it("retains the two conditional visibility rules", () => { let editorDetails = false; let verboseLog = false; const specs = createGeneralSettingSpecGroups({ showEditorStatusDetails: () => editorDetails, showVerboseLog: () => verboseLog, }).flatMap(({ items }) => items); const editorIcons = specs.find(({ key }) => key === "showOnlyIconsOnEditor"); const verbose = specs.find(({ key }) => key === "showVerboseLog"); expect(editorIcons?.visible?.()).toBe(false); expect(verbose?.visible?.()).toBe(false); editorDetails = true; verboseLog = true; expect(editorIcons?.visible?.()).toBe(true); expect(verbose?.visible?.()).toBe(true); }); }); describe("SettingSpec conversion", () => { it("maps metadata and a toggle to an Obsidian definition without importing the runtime API", () => { const visible = vi.fn(() => true); const disabled = vi.fn(() => false); const spec: SettingSpec = { key: "autoAcceptCompatibleTweak", control: { type: "toggle", defaultValue: true }, aliases: ["compatible tweaks"], visible, disabled, }; const metadata = { name: "Automatic compatibility", desc: "Accept compatible values.", status: "BETA", } satisfies ConfigurationItem; expect(toObsidianSettingDefinition(spec, metadata, rangeMessages)).toEqual({ name: `Automatic compatibility${statusDisplay("BETA")}`, desc: "Accept compatible values.", aliases: ["compatible tweaks"], visible, control: { type: "toggle", key: "autoAcceptCompatibleTweak", defaultValue: true, disabled, }, }); }); it("maps dropdown options to both native and legacy representations", () => { const options = { "v3-rabin-karp": "V3", legacy: "Legacy", }; const spec: SettingSpec = { key: "chunkSplitterVersion", control: { type: "dropdown", options: () => options }, }; const metadata = { name: "Chunk splitter", placeHolder: "Select a splitter", } satisfies ConfigurationItem; expect(toObsidianSettingDefinition(spec, metadata, rangeMessages).control).toEqual({ type: "dropdown", key: "chunkSplitterVersion", options, }); expect(toLegacySettingBinding(spec)).toEqual({ type: "dropdown", key: "chunkSplitterVersion", options: { options }, }); }); it("maps number limits and zero exceptions to equivalent validation", () => { const spec: SettingSpec = { key: "hashCacheMaxCount", control: { type: "number", min: 10, max: 100, allowZero: true }, }; const metadata = { name: "Cache size" } satisfies ConfigurationItem; const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages); const control = definition.control; expect(control).toMatchObject({ type: "number", key: "hashCacheMaxCount", min: 0, max: 100, }); if (control.type !== "number") { throw new Error("Expected a number control"); } expect(control.validate?.(0)).toBeUndefined(); expect(control.validate?.(10)).toBeUndefined(); expect(control.validate?.(5)).toBe("10..100"); expect(control.validate?.(101)).toBe("10..100"); expect(toLegacySettingBinding(spec)).toEqual({ type: "number", key: "hashCacheMaxCount", options: { clampMin: 10, clampMax: 100, acceptZero: true }, }); }); it("checks the maximum before applying a zero exception, as the legacy renderer does", () => { const spec: SettingSpec = { key: "hashCacheMaxCount", control: { type: "number", max: -1, allowZero: true }, }; const definition = toObsidianSettingDefinition(spec, { name: "Cache size" }, rangeMessages); const control = definition.control; if (control.type !== "number") { throw new Error("Expected a number control"); } expect(control.validate?.(0)).toBe("~..-1"); }); it("dispatches a specification through the existing AutoWire renderer", () => { const renderer = { autoWireToggle: vi.fn(), autoWireNumeric: vi.fn(), autoWireDropDown: vi.fn(), }; const visible = vi.fn(() => false); const spec: SettingSpec = { key: "readChunksOnline", control: { type: "toggle" }, visible, }; renderLegacySettingSpec(renderer, spec); expect(renderer.autoWireToggle).toHaveBeenCalledOnce(); const [, options] = renderer.autoWireToggle.mock.calls[0]; expect(options.onUpdate()).toEqual({ visibility: false }); expect(visible).toHaveBeenCalledOnce(); expect(renderer.autoWireNumeric).not.toHaveBeenCalled(); expect(renderer.autoWireDropDown).not.toHaveBeenCalled(); }); });