Complete declarative settings runtime integration

This commit is contained in:
vorotamoroz
2026-08-24 18:11:46 +00:00
parent dd11a753d5
commit 8adc88b8cb
11 changed files with 460 additions and 184 deletions
+168 -50
View File
@@ -1,18 +1,27 @@
import { mkdir } from "node:fs/promises";
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
import { assertMobileDialogueLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
import {
allowPendingObsidianTestVaultOpenAction,
captureObsidianDialogue,
obsidianRemoteDebuggingPort,
openLiveSyncSettings,
preseedTrustedVaultState,
withObsidianPage,
} from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts";
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS ?? 10000);
const compatibilityReviewMessage = "Review the internal database compatibility change before synchronisation resumes.";
type ObsidianSettingsController = {
open(): void;
openTabById(tabId: string): void;
const settingsOnly = process.env.E2E_OBSIDIAN_SETTINGS_ONLY === "true";
const diagnosticsDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e";
const settingsScreenshotOptions = {
animations: "disabled" as const,
style: ".notice-container { visibility: hidden !important; }",
};
const compatibilityReviewMessage = "Review the internal database compatibility change before synchronisation resumes.";
type LiveSyncTestPlugin = {
core: {
@@ -34,12 +43,39 @@ type LiveSyncTestPlugin = {
};
type ObsidianTestApp = {
setting?: ObsidianSettingsController;
plugins?: { plugins: Record<string, LiveSyncTestPlugin | undefined> };
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
const settingsPageNames = [
"Change Log",
"Setup",
"General Settings",
"Remote Configuration",
"Sync Settings",
"Selector",
"Customisation sync",
"Hatch",
"Advanced",
"Power users",
"Patches",
"Maintenance",
] as const;
async function resumePendingCompatibilityReviewForSettings(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const review = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({
hasText: "Synchronisation paused for compatibility review",
}),
});
if (!(await review.isVisible())) return;
await review.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs });
await review.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
}
async function verifyCompatibilityReview(): Promise<void> {
const port = obsidianRemoteDebuggingPort();
const summaryScreenshot = await captureObsidianDialogue(port, "compatibility-review-summary.png", async (page) => {
@@ -223,41 +259,26 @@ async function verifyEffectiveSettings(): Promise<void> {
sleepPreferences.powerUserMode !== false ||
sleepPreferences.edgeCaseMode !== false
) {
throw new Error(
`Unexpected effective sleep preferences: ${JSON.stringify(sleepPreferences)}`
);
throw new Error(`Unexpected effective sleep preferences: ${JSON.stringify(sleepPreferences)}`);
}
await page.evaluate(() => {
const setting = (globalThis as ObsidianTestGlobal).app?.setting;
if (setting === undefined) throw new Error("Obsidian settings are unavailable");
setting.open();
setting.openTabById("obsidian-livesync");
});
const liveSyncSettings = page.locator(".sls-setting");
await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs });
const settingsClass = await liveSyncSettings.getAttribute("class");
for (const modeClass of [
"menu-setting-advanced-disabled",
"menu-setting-poweruser-disabled",
"menu-setting-edgecase-disabled",
]) {
if (!settingsClass?.split(/\s+/u).includes(modeClass)) {
throw new Error(`The settings UI did not disable ${modeClass}.`);
let settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
for (const hiddenPage of ["Selector", "Customisation sync", "Advanced", "Power users", "Patches"]) {
if (await settingsNavigator.isPageListed(hiddenPage)) {
throw new Error(`${hiddenPage} was visible before its feature level was enabled.`);
}
}
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Change Log"]').click();
const removedAcknowledgements = liveSyncSettings.getByRole("button", {
let settingsPage = await settingsNavigator.openPage("Change Log");
const removedAcknowledgements = settingsPage.getByRole("button", {
name: /I got it and updated|OK, I have read everything/u,
});
if ((await removedAcknowledgements.count()) !== 0) {
throw new Error("The Change Log still contains a compatibility or release-note acknowledgement control.");
}
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Remote Configuration"]').click();
const connectionPanel = liveSyncSettings
settingsPage = await settingsNavigator.openPage("Remote Configuration");
const connectionPanel = settingsPage
.locator("h4.sls-setting-panel-title")
.filter({ hasText: "Connection settings" })
.locator("..");
@@ -267,41 +288,74 @@ async function verifyEffectiveSettings(): Promise<void> {
timeout: uiTimeoutMs,
});
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
const generalSleepSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Allow sleep during synchronisation", { exact: true }),
settingsPage = await settingsNavigator.openPage("Sync Settings");
const generalSleepSetting = settingsPage.locator(".setting-item").filter({
has: settingsNavigator.page.getByText("Allow sleep during synchronisation", { exact: true }),
});
await generalSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
if ((await generalSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 0) {
throw new Error("The general sleep preference must be disabled by default.");
}
const desktopSleepSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Allow sleep during synchronisation on the desktop", { exact: true }),
const desktopSleepSetting = settingsPage.locator(".setting-item").filter({
has: settingsNavigator.page.getByText("Allow sleep during synchronisation on the desktop", {
exact: true,
}),
});
await desktopSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
if ((await desktopSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 1) {
throw new Error("The desktop sleep preference must be enabled by default.");
}
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click();
const advancedModeSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Enable advanced features", { exact: true }),
settingsPage = await settingsNavigator.openPage("Setup");
const advancedModeSetting = settingsPage.locator(".setting-item").filter({
has: settingsNavigator.page.getByText("Enable advanced features", { exact: true }),
});
await advancedModeSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
await advancedModeSetting.locator(".checkbox-container").click();
await page.waitForFunction(
() => document.querySelector(".sls-setting")?.classList.contains("menu-setting-advanced-enabled") === true,
() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.services.setting.currentSettings().useAdvancedMode === true;
},
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 }),
if (settingsNavigator.renderer === "declarative") {
for (const mode of [
{ label: "Enable poweruser features", key: "usePowerUserMode" },
{ label: "Enable edge case treatment features", key: "useEdgeCaseMode" },
] as const) {
const modeSetting = settingsPage.locator(".setting-item").filter({
has: settingsNavigator.page.getByText(mode.label, { exact: true }),
});
await modeSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs });
await page.waitForFunction(
(key) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.services.setting.currentSettings()[key] === true;
},
mode.key,
{ timeout: uiTimeoutMs }
);
}
}
settingsPage = await settingsNavigator.openPage("Advanced");
const cacheSizeSetting = settingsPage.locator(".setting-item").filter({
has: settingsNavigator.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");
const cacheSizeInput = cacheSizeSetting.locator('input[type="number"]');
if (settingsNavigator.renderer === "declarative") {
await cacheSizeInput.click();
await cacheSizeInput.press("ControlOrMeta+A");
await cacheSizeInput.pressSequentially("321");
await cacheSizeInput.press("Enter");
} else {
await cacheSizeInput.fill("321");
}
await page.waitForFunction(
() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
@@ -311,9 +365,44 @@ async function verifyEffectiveSettings(): Promise<void> {
{ timeout: uiTimeoutMs }
);
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
if (settingsNavigator.renderer === "declarative") {
await settingsNavigator.returnToCatalogue();
await settingsNavigator.dialogue
.locator(".vertical-tab-content:visible")
.last()
.evaluate((element) => {
element.scrollTop = 0;
});
await settingsNavigator.dialogue.screenshot({
...settingsScreenshotOptions,
path: `${diagnosticsDirectory}/settings-declarative-catalogue.png`,
});
const search = settingsNavigator.dialogue.locator(".setting-search-container input");
await search.fill("Memory cache size (by total items)");
const searchResult = settingsNavigator.dialogue.locator(".setting-search-result-item").filter({
has: settingsNavigator.page.getByText("Memory cache size (by total items)", { exact: true }),
});
await settingsNavigator.dialogue.screenshot({
...settingsScreenshotOptions,
path: `${diagnosticsDirectory}/settings-declarative-search.png`,
});
await searchResult.waitFor({ state: "visible", timeout: uiTimeoutMs });
await search.fill("");
const deletionPanel = liveSyncSettings
for (const pageName of settingsPageNames) {
const pageRoot = await settingsNavigator.openPage(pageName);
await pageRoot.waitFor({ state: "visible", timeout: uiTimeoutMs });
}
settingsPage = await settingsNavigator.openPage("Advanced");
await settingsNavigator.dialogue.screenshot({
...settingsScreenshotOptions,
path: `${diagnosticsDirectory}/settings-declarative-advanced.png`,
});
}
settingsPage = await settingsNavigator.openPage("Sync Settings");
const deletionPanel = settingsPage
.locator("h4.sls-setting-panel-title")
.filter({ hasText: "Deletion Propagation" })
.locator("..");
@@ -328,6 +417,22 @@ async function verifyEffectiveSettings(): Promise<void> {
`The obsolete LiveSync trash toggle is still present in the settings UI (${obsoleteToggleCount} found).`
);
}
if (settingsNavigator.renderer === "declarative") {
await settingsNavigator.close();
settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
settingsPage = await settingsNavigator.openPage("Advanced");
const restoredValue = await settingsPage
.locator(".setting-item")
.filter({
has: settingsNavigator.page.getByText("Memory cache size (by total items)", { exact: true }),
})
.locator('input[type="number"]')
.inputValue();
if (restoredValue !== "321") {
throw new Error(`The declarative Advanced value was not restored after reopening: ${restoredValue}`);
}
}
});
}
@@ -338,6 +443,7 @@ async function main(): Promise<void> {
throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);
}
const vault = await createTemporaryVault();
await mkdir(diagnosticsDirectory, { recursive: true });
let session: ObsidianLiveSyncSession | undefined;
try {
session = await startObsidianLiveSyncSession({
@@ -346,10 +452,10 @@ async function main(): Promise<void> {
vault,
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
pluginData: {
doctorProcessedVersion: "0.25.27",
doctorProcessedVersion: settingsOnly ? "1.0.0" : "0.25.27",
isConfigured: true,
liveSync: false,
versionUpFlash: compatibilityReviewMessage,
versionUpFlash: settingsOnly ? "" : compatibilityReviewMessage,
notifyThresholdOfRemoteStorageSize: 0,
syncOnStart: false,
syncOnSave: false,
@@ -362,10 +468,22 @@ async function main(): Promise<void> {
usePowerUserMode: false,
useEdgeCaseMode: false,
},
lifecycle: settingsOnly
? {
afterLaunch: async ({ remoteDebuggingPort }) => {
await preseedTrustedVaultState(remoteDebuggingPort, vault.id);
await allowPendingObsidianTestVaultOpenAction(remoteDebuggingPort, vault.path, uiTimeoutMs);
},
}
: undefined,
});
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await verifyCompatibilityReview();
await verifyConfigDoctorFollowsCompatibilityReview();
if (settingsOnly) {
await resumePendingCompatibilityReviewForSettings();
} else {
await verifyCompatibilityReview();
await verifyConfigDoctorFollowsCompatibilityReview();
}
await verifyEffectiveSettings();
console.log("Compatibility review and settings expose only effective user controls.");
} finally {