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 { 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; }; type LiveSyncTestPlugin = { core: { services: { setting: { currentSettings(): { versionUpFlash: string }; getSmallConfig(key: string): string | null; }; }; }; }; type ObsidianTestApp = { setting?: ObsidianSettingsController; plugins?: { plugins: Record }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; async function verifyCompatibilityReview(): Promise { const port = obsidianRemoteDebuggingPort(); const summaryScreenshot = await captureObsidianDialogue(port, "compatibility-review-summary.png", async (page) => { const modal = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review", }), }); await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); await modal .getByText("Your automatic synchronisation preferences have not been changed.", { exact: false }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); await modal .getByRole("button", { name: "Review compatibility details" }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); await modal .getByRole("button", { name: "Resume synchronisation", }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); await modal .getByRole("button", { name: "Keep synchronisation paused" }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); }); await withObsidianPage(port, async (page) => { const markerBeforeAcknowledgement = await page.evaluate(() => { const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable"); return plugin.core.services.setting.getSmallConfig("database-compatibility-version"); }); if (markerBeforeAcknowledgement !== null && markerBeforeAcknowledgement !== "") { throw new Error( `The database version was marked as acknowledged before review: ${markerBeforeAcknowledgement}` ); } }); await setObsidianMobileTestMode(port, true, uiTimeoutMs); const mobileSummaryScreenshot = await captureObsidianDialogue( port, "compatibility-review-summary-mobile.png", async (page) => { const summary = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review", }), }); await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); await assertMobileDialogueLayout(page, summary, "compatibility review summary"); } ); await withObsidianPage(port, async (page) => { const summary = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review", }), }); await summary.getByRole("button", { name: "Review compatibility details" }).click(); }); const detailsScreenshot = await captureObsidianDialogue( port, "compatibility-review-details-mobile.png", async (page) => { const modal = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), }); await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); await modal.getByText("Why synchronisation is paused", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); await modal.getByText("Remote replication is blocked before work begins.", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs, }); await modal .getByRole("button", { name: "Back to compatibility review" }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); if ((await modal.getByRole("button", { name: "Keep synchronisation paused" }).count()) !== 0) { throw new Error("The explanatory details dialogue must not make the pause decision."); } await assertMobileDialogueLayout(page, modal, "compatibility review details"); } ); await withObsidianPage(port, async (page) => { const details = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), }); await details.getByRole("button", { name: "Back to compatibility review" }).click(); const summary = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review", }), }); await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); }); await setObsidianMobileTestMode(port, false, uiTimeoutMs); await withObsidianPage(port, async (page) => { const summary = page.locator(".modal-container").filter({ has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review", }), }); await summary .getByRole("button", { name: "Resume synchronisation", }) .click(); await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs }); await page.waitForFunction( (expectedVersion) => { const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; if (plugin === undefined) return false; const setting = plugin.core.services.setting; return ( setting.getSmallConfig("database-compatibility-version") === expectedVersion && setting.currentSettings().versionUpFlash === "" ); }, `${VER}`, { timeout: uiTimeoutMs } ); }); console.log( `Compatibility review screenshots: ${summaryScreenshot}, ${mobileSummaryScreenshot}, ${detailsScreenshot}` ); } async function verifyDeletionSettings(): Promise { await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { 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 }); await liveSyncSettings.locator('.sls-setting-menu-btn[title="Change Log"]').click(); const removedAcknowledgements = liveSyncSettings.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="Sync Settings"]').click(); const deletionPanel = liveSyncSettings .locator("h4.sls-setting-panel-title") .filter({ hasText: "Deletion Propagation" }) .locator(".."); await deletionPanel .getByText("Keep empty folder", { exact: true }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); const obsoleteToggleCount = await deletionPanel.getByText("Use the trash bin", { exact: true }).count(); if (obsoleteToggleCount !== 0) { throw new Error( `The obsolete LiveSync trash toggle is still present in the settings UI (${obsoleteToggleCount} found).` ); } }); } async function main(): Promise { const binary = requireObsidianBinary(); const cli = discoverObsidianCli(); if (!cli.binary) { throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); } const vault = await createTemporaryVault(); let session: ObsidianLiveSyncSession | undefined; try { session = await startObsidianLiveSyncSession({ binary, cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), pluginData: { doctorProcessedVersion: "0.25.27", isConfigured: true, liveSync: false, versionUpFlash: compatibilityReviewMessage, notifyThresholdOfRemoteStorageSize: 0, syncOnStart: false, syncOnSave: false, syncOnEditorSave: false, syncOnFileOpen: false, syncAfterMerge: false, periodicReplication: false, useAdvancedMode: true, }, }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); await verifyCompatibilityReview(); await verifyDeletionSettings(); console.log("Compatibility review and deletion settings expose only effective user controls."); } finally { if (session) { await session.app.stop(); } await vault.dispose(); } } main().catch((error: unknown) => { console.error(error instanceof Error ? error.stack : error); process.exit(1); });