Introduce shared setting specifications for Advanced settings

This commit is contained in:
vorotamoroz
2026-08-24 10:36:39 +00:00
parent ebaf89f822
commit d8f7762d01
8 changed files with 618 additions and 50 deletions
@@ -172,7 +172,7 @@ every complex pane.
properties:
- one explicit persisted setting key, excluding keys from `OnDialogSettings`;
- one standard toggle, text, textarea, number, or dropdown control;
- one standard toggle, number, or dropdown control in the first proof page;
- a value which is read from the current editing buffer;
- a change which can be persisted immediately through the existing
`saveSettings([key])` path; and
@@ -196,15 +196,13 @@ type SettingSpecBase<K extends PersistedSettingKey, C> = {
type SettingSpec =
| SettingSpecBase<PersistedBooleanSettingKey, { type: "toggle"; defaultValue?: boolean }>
| SettingSpecBase<PersistedStringSettingKey, { type: "text"; placeholder?: string }>
| SettingSpecBase<PersistedStringSettingKey, { type: "textarea"; placeholder?: string; rows?: number }>
| SettingSpecBase<
PersistedNumericSettingKey,
{
type: "number";
min?: number;
max?: number;
validate?: (value: number) => string | void;
allowZero?: boolean;
}
>
| SettingSpecBase<
@@ -216,16 +214,25 @@ type SettingSpec =
>;
```
Names, descriptions, maturity labels, placeholders, and configuration levels
come from the translated Commonlib setting metadata by default. The native
renderer appends the existing maturity marker to `name`, maps the description
and placeholder directly, and combines the metadata level with the
specification's `visible` predicate. The specification may override a label
only where the current interface already uses a deliberate product-specific
label. Options remain LiveSync owned because they can depend on the active
remote, platform, or language. A control which needs the current obsolete-row
styling remains custom because the native definition does not provide an
equivalent per-row class contract.
The initial union contains only the three control types used by the Advanced
proof page. Text and textarea controls will be added when a migrated page
provides a concrete use for them. Number validation is derived from `min`,
`max`, and `allowZero`, so the native and imperative renderers enforce the same
constraints without introducing an arbitrary validation language.
Names, descriptions, maturity labels, and placeholders come from the translated
Commonlib setting metadata by default. The native renderer appends the existing
maturity marker to `name`, and maps the description and supported placeholder
directly. Configuration level remains a page and renderer concern: the legacy
renderer retains its existing DOM classes, while the native page catalogue owns
page-level visibility. A mixed-level native group must provide an explicit
visibility predicate at that boundary rather than inferring one in the pure
control converter. The specification may override a label only where the
current interface already uses a deliberate product-specific label. Options
remain LiveSync owned because they can depend on the active remote, platform,
or language. A control which needs the current obsolete-row styling remains
custom because the native definition does not provide an equivalent per-row
class contract.
The catalogue explicitly lists each exposed key. It does not enumerate
`SettingInformation` automatically.
@@ -0,0 +1,50 @@
import { ChunkAlgorithmNames } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { SettingSpecGroup } from "./SettingSpec.ts";
export type AdvancedSettingSpecContext = {
isCouchDB: () => boolean;
};
/** Build the explicitly exposed standard controls for the existing Advanced page. */
export function createAdvancedSettingSpecGroups({
isCouchDB,
}: AdvancedSettingSpecContext): readonly SettingSpecGroup[] {
return [
{
heading: "Memory cache",
items: [{ key: "hashCacheMaxCount", control: { type: "number", min: 10 } }],
},
{
heading: "Local Database Tweak",
items: [
{
key: "chunkSplitterVersion",
control: { type: "dropdown", options: () => ChunkAlgorithmNames },
},
{ key: "customChunkSize", control: { type: "number", min: 0, allowZero: true } },
],
},
{
heading: "Transfer Tweak",
items: [
{ key: "readChunksOnline", control: { type: "toggle" }, visible: isCouchDB },
{ key: "useOnlyLocalChunk", control: { type: "toggle" }, visible: isCouchDB },
{
key: "concurrencyOfReadChunksOnline",
control: { type: "number", min: 10 },
visible: isCouchDB,
},
{
key: "minimumIntervalOfReadChunksOnline",
control: { type: "number", min: 10 },
visible: isCouchDB,
},
{ key: "autoAcceptCompatibleTweak", control: { type: "toggle", defaultValue: true } },
],
},
{
heading: "Remote Database Tweak",
items: [{ key: "enableCompression", control: { type: "toggle" } }],
},
];
}
@@ -1,43 +1,18 @@
import { ChunkAlgorithmNames } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import type { PageFunctions } from "./SettingPane.ts";
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
import { renderLegacySettingSpec } from "./SettingSpec.ts";
export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
void addPanel(paneEl, "Memory cache").then((paneEl) => {
new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 });
// new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 });
const groups = createAdvancedSettingSpecGroups({
isCouchDB: () => this.onlyOnCouchDB().visibility !== false,
});
void addPanel(paneEl, "Local Database Tweak").then((paneEl) => {
const items = ChunkAlgorithmNames;
new Setting(paneEl).autoWireDropDown("chunkSplitterVersion", {
options: items,
for (const group of groups) {
void addPanel(paneEl, group.heading).then((panelEl) => {
for (const spec of group.items) {
renderLegacySettingSpec(new Setting(panelEl), spec);
}
});
new Setting(paneEl).autoWireNumeric("customChunkSize", { clampMin: 0, acceptZero: true });
});
void addPanel(paneEl, "Transfer Tweak").then((paneEl) => {
new Setting(paneEl).autoWireToggle("readChunksOnline", { onUpdate: this.onlyOnCouchDB });
new Setting(paneEl).autoWireToggle("useOnlyLocalChunk", { onUpdate: this.onlyOnCouchDB });
new Setting(paneEl).autoWireNumeric("concurrencyOfReadChunksOnline", {
clampMin: 10,
onUpdate: this.onlyOnCouchDB,
});
new Setting(paneEl).autoWireNumeric("minimumIntervalOfReadChunksOnline", {
clampMin: 10,
onUpdate: this.onlyOnCouchDB,
});
new Setting(paneEl).autoWireToggle("autoAcceptCompatibleTweak", { defaultToggleValue: true });
// new Setting(paneEl)
// .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB })
// new Setting(paneEl)
// .autoWireNumeric("sendChunksBulkMaxSize", {
// clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB
// })
});
void addPanel(paneEl, "Remote Database Tweak").then((paneEl) => {
new Setting(paneEl).autoWireToggle("enableCompression");
});
}
}
@@ -0,0 +1,83 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { paneAdvanced } from "./PaneAdvanced.ts";
const settingHarness = vi.hoisted(() => ({
createdIn: [] as HTMLElement[],
rendered: [] as unknown[],
}));
vi.mock("./LiveSyncSetting.ts", () => ({
LiveSyncSetting: class LiveSyncSetting {
constructor(containerEl: HTMLElement) {
settingHarness.createdIn.push(containerEl);
}
},
}));
vi.mock("./SettingSpec.ts", async (importOriginal) => {
const original = await importOriginal<typeof import("./SettingSpec.ts")>();
return {
...original,
renderLegacySettingSpec: vi.fn((_renderer: unknown, spec: unknown) => {
settingHarness.rendered.push(spec);
}),
};
});
afterEach(() => {
settingHarness.createdIn.length = 0;
settingHarness.rendered.length = 0;
vi.clearAllMocks();
});
describe("paneAdvanced", () => {
it("renders the shared specifications into the four existing panels", async () => {
const panelElements = new Map<string, HTMLElement>();
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => {
const panel = { heading } as unknown as HTMLElement;
panelElements.set(heading, panel);
return Promise.resolve(panel);
});
const host = {
onlyOnCouchDB: vi.fn(() => ({ visibility: false })),
};
paneAdvanced.call(host as never, {} as HTMLElement, { addPanel } as never);
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(9));
expect(addPanel.mock.calls.map(([, heading]) => heading)).toEqual([
"Memory cache",
"Local Database Tweak",
"Transfer Tweak",
"Remote Database Tweak",
]);
expect(settingHarness.createdIn).toEqual([
panelElements.get("Memory cache"),
panelElements.get("Local Database Tweak"),
panelElements.get("Local Database Tweak"),
panelElements.get("Transfer Tweak"),
panelElements.get("Transfer Tweak"),
panelElements.get("Transfer Tweak"),
panelElements.get("Transfer Tweak"),
panelElements.get("Transfer Tweak"),
panelElements.get("Remote Database Tweak"),
]);
expect(settingHarness.rendered.map((spec) => (spec as { key: string }).key)).toEqual([
"hashCacheMaxCount",
"chunkSplitterVersion",
"customChunkSize",
"readChunksOnline",
"useOnlyLocalChunk",
"concurrencyOfReadChunksOnline",
"minimumIntervalOfReadChunksOnline",
"autoAcceptCompatibleTweak",
"enableCompression",
]);
const readChunksOnline = settingHarness.rendered.find(
(spec) => (spec as { key: string }).key === "readChunksOnline"
) as { visible: () => boolean };
expect(readChunksOnline.visible()).toBe(false);
expect(host.onlyOnCouchDB).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,237 @@
import { statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { SettingControl, SettingDefinitionControl } from "obsidian";
import type { AutoWireOption, OnUpdateResult } from "./SettingPane.ts";
import type { AllBooleanItemKey, AllNumericItemKey, AllStringItemKey, OnDialogSettings } from "./settingConstants.ts";
export type PersistedBooleanSettingKey = Exclude<AllBooleanItemKey, keyof OnDialogSettings>;
export type PersistedStringSettingKey = Exclude<AllStringItemKey, keyof OnDialogSettings>;
export type PersistedNumericSettingKey = Exclude<AllNumericItemKey, keyof OnDialogSettings>;
export type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey;
type SettingSpecBase<K extends PersistedSettingKey, C> = {
key: K;
control: C;
visible?: () => boolean;
disabled?: () => boolean;
aliases?: string[];
};
type ToggleSettingSpec = SettingSpecBase<
PersistedBooleanSettingKey,
{
type: "toggle";
defaultValue?: boolean;
}
>;
type NumberSettingSpec = SettingSpecBase<
PersistedNumericSettingKey,
{
type: "number";
min?: number;
max?: number;
allowZero?: boolean;
}
>;
type DropdownSettingSpec = SettingSpecBase<
PersistedStringSettingKey,
{
type: "dropdown";
options: () => Record<string, string>;
}
>;
export type SettingSpec = ToggleSettingSpec | NumberSettingSpec | DropdownSettingSpec;
/** An existing Advanced-page panel and the standard controls rendered within it. */
export type SettingSpecGroup = {
heading: string;
items: readonly SettingSpec[];
};
export type SettingSpecMessages = {
valueShouldBeInRange: (range: { min?: number; max?: number }) => string;
};
type LegacySettingOptions = Pick<AutoWireOption, "onUpdate">;
export type LegacySettingBinding =
| {
type: "toggle";
key: PersistedBooleanSettingKey;
options: LegacySettingOptions & Pick<AutoWireOption, "defaultToggleValue">;
}
| {
type: "number";
key: PersistedNumericSettingKey;
options: LegacySettingOptions & {
clampMin?: number;
clampMax?: number;
acceptZero?: boolean;
};
}
| {
type: "dropdown";
key: PersistedStringSettingKey;
options: LegacySettingOptions & { options: Record<string, string> };
};
export interface LegacySettingSpecRenderer {
autoWireToggle(key: AllBooleanItemKey, options?: AutoWireOption): unknown;
autoWireNumeric(
key: AllNumericItemKey,
options: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean }
): unknown;
autoWireDropDown(key: AllStringItemKey, options: AutoWireOption & { options: Record<string, string> }): unknown;
}
function isToggleSettingSpec(spec: SettingSpec): spec is ToggleSettingSpec {
return spec.control.type === "toggle";
}
function isNumberSettingSpec(spec: SettingSpec): spec is NumberSettingSpec {
return spec.control.type === "number";
}
function toLegacyUpdateOptions(spec: SettingSpec): LegacySettingOptions {
if (!spec.visible && !spec.disabled) {
return {};
}
return {
onUpdate: (): OnUpdateResult => ({
...(spec.visible ? { visibility: spec.visible() } : {}),
...(spec.disabled ? { disabled: spec.disabled() } : {}),
}),
};
}
/** Convert a shared specification to the arguments accepted by the current AutoWire renderer. */
export function toLegacySettingBinding(spec: SettingSpec): LegacySettingBinding {
const updateOptions = toLegacyUpdateOptions(spec);
if (isToggleSettingSpec(spec)) {
return {
type: "toggle",
key: spec.key,
options: {
...updateOptions,
...(spec.control.defaultValue === undefined ? {} : { defaultToggleValue: spec.control.defaultValue }),
},
};
}
if (isNumberSettingSpec(spec)) {
return {
type: "number",
key: spec.key,
options: {
...updateOptions,
...(spec.control.min === undefined ? {} : { clampMin: spec.control.min }),
...(spec.control.max === undefined ? {} : { clampMax: spec.control.max }),
...(spec.control.allowZero === undefined ? {} : { acceptZero: spec.control.allowZero }),
},
};
}
return {
type: "dropdown",
key: spec.key,
options: {
...updateOptions,
options: spec.control.options(),
},
};
}
/** Render one shared specification without moving value or persistence ownership out of the settings tab. */
export function renderLegacySettingSpec(renderer: LegacySettingSpecRenderer, spec: SettingSpec): void {
const binding = toLegacySettingBinding(spec);
switch (binding.type) {
case "toggle":
renderer.autoWireToggle(binding.key, binding.options);
return;
case "number":
renderer.autoWireNumeric(binding.key, binding.options);
return;
case "dropdown":
renderer.autoWireDropDown(binding.key, binding.options);
}
}
function numberIsOutOfRange(value: number, control: NumberSettingSpec["control"]) {
if (!Number.isFinite(value)) {
return true;
}
if (control.max !== undefined && value > control.max) {
return true;
}
if (control.allowZero && value === 0) {
return false;
}
return control.min !== undefined && value < control.min;
}
/**
* Convert one shared specification to a declarative Obsidian control.
*
* This function returns a plain object and only refers to Obsidian through erased types, so Stage B does not load or
* activate the 1.13 runtime API. Translated metadata and validation messages are explicit inputs to keep conversion
* side-effect free.
*/
export function toObsidianSettingDefinition(
spec: SettingSpec,
metadata: Pick<ConfigurationItem, "name" | "desc" | "placeHolder" | "status">,
messages: SettingSpecMessages
): SettingDefinitionControl<PersistedSettingKey> {
let control: SettingControl<PersistedSettingKey>;
switch (spec.control.type) {
case "toggle":
control = {
type: "toggle",
key: spec.key,
...(spec.control.defaultValue === undefined ? {} : { defaultValue: spec.control.defaultValue }),
...(spec.disabled ? { disabled: spec.disabled } : {}),
};
break;
case "number": {
const numericControl = spec.control;
const hasRange = numericControl.min !== undefined || numericControl.max !== undefined;
control = {
type: "number",
key: spec.key,
...(numericControl.min === undefined
? {}
: { min: numericControl.allowZero ? Math.min(0, numericControl.min) : numericControl.min }),
...(numericControl.max === undefined ? {} : { max: numericControl.max }),
...(metadata.placeHolder ? { placeholder: metadata.placeHolder } : {}),
...(hasRange
? {
validate: (value: number) =>
numberIsOutOfRange(value, numericControl)
? messages.valueShouldBeInRange({
min: numericControl.min,
max: numericControl.max,
})
: undefined,
}
: {}),
...(spec.disabled ? { disabled: spec.disabled } : {}),
};
break;
}
case "dropdown":
control = {
type: "dropdown",
key: spec.key,
options: spec.control.options(),
...(spec.disabled ? { disabled: spec.disabled } : {}),
};
break;
}
return {
name: `${metadata.name}${statusDisplay(metadata.status)}`,
...(metadata.desc ? { desc: metadata.desc } : {}),
...(spec.aliases ? { aliases: spec.aliases } : {}),
...(spec.visible ? { visible: spec.visible } : {}),
control,
};
}
@@ -0,0 +1,199 @@
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";
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<keyof OnDialogSettings, PersistedSettingKey>;
expectTypeOf<OnDialogOverlap>().toEqualTypeOf<never>();
});
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("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();
});
});
+1 -1
View File
@@ -106,7 +106,7 @@ The underlying `test:e2e:obsidian:<scenario>` scripts remain available for an im
`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database.
`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour.
`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then enables Advanced mode and persists one numeric Advanced setting, covering the imperative settings fallback used by the `SettingSpec` proof. Finally, it selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour.
The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixel viewport, and explicit iPhone-style safe-area insets of 47 pixels at the top and 34 pixels at the bottom. The public `@vrtmrz/obsidian-test-session` layout assertions require each modal to remain within the viewport and safe area without horizontal overflow. They also require the Obsidian Close control to remain within the safe area and provide at least a 44 by 44 CSS-pixel touch target. The runner clicks that control to verify actionability, then completes the explicit cancellation path. These simulated checks cover deterministic layout and interaction boundaries; they do not claim to reproduce a native operating-system overlay.
+17
View File
@@ -25,6 +25,7 @@ type LiveSyncTestPlugin = {
useAdvancedMode: boolean;
usePowerUserMode: boolean;
useEdgeCaseMode: boolean;
hashCacheMaxCount: number;
};
getSmallConfig(key: string): string | null;
};
@@ -294,6 +295,22 @@ async function verifyEffectiveSettings(): Promise<void> {
undefined,
{ timeout: uiTimeoutMs }
);
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Advanced"]').click();
const cacheSizeSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Memory cache size (by total items)", { exact: true }),
});
await cacheSizeSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
await cacheSizeSetting.locator('input[type="number"]').fill("321");
await page.waitForFunction(
() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.services.setting.currentSettings().hashCacheMaxCount === 321;
},
undefined,
{ timeout: uiTimeoutMs }
);
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
const deletionPanel = liveSyncSettings