Prepare the 1.0 compatibility review flow

This commit is contained in:
vorotamoroz
2026-07-19 04:34:08 +00:00
parent 52138bf7a5
commit ed3f81e9f9
42 changed files with 5524 additions and 4119 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ npm run test:e2e:obsidian:local-suite:services
`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises two representative Svelte dialogue routes: remote server selection through `SetupManager`, and Setup URI entry through the registered command. The session pre-seeds a configured, inactive plug-in state so that onboarding and migration prompts do not interfere with the dialogues under test. It requires each dialogue title and its principal controls to be visible, captures desktop and mobile screenshots, closes them through their normal user controls, and verifies that the remote-selection promise settles without an error. This covers the host, context, component, and result boundaries moved during the Commonlib package extraction without applying settings or contacting a remote service.
`test:e2e:obsidian:settings-ui` opens Self-hosted LiveSync's settings in a temporary real Obsidian session and selects the Synchronisation Settings pane through its user-facing control. It 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 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.
+109
View File
@@ -0,0 +1,109 @@
import {
assertLocatorHasMinimumTouchTarget,
assertLocatorWithinSafeArea,
assertLocatorWithinViewport,
assertNoHorizontalOverflow,
} from "@vrtmrz/obsidian-test-session";
import type { Locator, Page } from "playwright";
import { withObsidianPage } from "./ui.ts";
export const mobileViewport = { width: 390, height: 844 } as const;
export const desktopViewport = { width: 1024, height: 768 } as const;
export const iPhoneSafeArea = { top: 47, right: 0, bottom: 34, left: 0 } as const;
type ObsidianTestApp = {
emulateMobile?: (mobile: boolean) => void;
plugins?: { plugins: Record<string, unknown> };
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise<void> {
await withObsidianPage(port, async (page) => {
await page.setViewportSize(enabled ? mobileViewport : desktopViewport);
await page.evaluate((nextEnabled) => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
if (typeof obsidianApp?.emulateMobile !== "function") {
throw new Error("app.emulateMobile is unavailable");
}
obsidianApp.emulateMobile(nextEnabled);
}, enabled);
await page.waitForFunction(
(nextEnabled) => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
return (
document.body.classList.contains("is-mobile") === nextEnabled &&
obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined
);
},
enabled,
{ timeout: timeoutMs }
);
await page.evaluate(
(safeArea) => {
for (const edge of ["top", "right", "bottom", "left"] as const) {
const property = `--safe-area-inset-${edge}`;
if (safeArea === null) document.body.style.removeProperty(property);
else document.body.style.setProperty(property, `${safeArea[edge]}px`);
}
},
enabled ? iPhoneSafeArea : null
);
});
}
export async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise<void> {
const dialogue = container.locator(".modal").last();
const closeButton = dialogue.locator(".modal-close-button");
await assertLocatorWithinViewport(page, dialogue, { label });
await assertNoHorizontalOverflow(page, dialogue, { label });
await assertLocatorWithinSafeArea(page, dialogue, {
label,
safeAreaInsets: iPhoneSafeArea,
});
await assertLocatorWithinSafeArea(page, closeButton, {
label: `${label} close button`,
safeAreaInsets: iPhoneSafeArea,
});
await assertLocatorHasMinimumTouchTarget(page, closeButton, {
label: `${label} close button`,
});
const visibleButtons = dialogue.locator("button:visible");
for (let index = 0; index < (await visibleButtons.count()); index++) {
const button = visibleButtons.nth(index);
const buttonLabel = (await button.innerText()).trim() || `button ${index + 1}`;
await assertLocatorWithinViewport(page, button, { label: `${label}: ${buttonLabel}` });
await assertLocatorWithinSafeArea(page, button, {
label: `${label}: ${buttonLabel}`,
safeAreaInsets: iPhoneSafeArea,
});
await assertNoHorizontalOverflow(page, button, { label: `${label}: ${buttonLabel}` });
await assertLocatorHasMinimumTouchTarget(page, button, { label: `${label}: ${buttonLabel}` });
}
}
export async function assertMobileNoticeLayout(
page: Page,
notice: Locator,
label: string,
reservedRightPx = 56
): Promise<void> {
await assertLocatorWithinViewport(page, notice, { label });
await assertNoHorizontalOverflow(page, notice, { label });
await assertLocatorWithinSafeArea(page, notice, {
label,
safeAreaInsets: iPhoneSafeArea,
});
const box = await notice.boundingBox();
if (box === null) {
throw new Error(`${label} did not expose a measurable viewport rectangle.`);
}
const viewportWidth = await page.evaluate(() => window.innerWidth);
const rightEdge = box.x + box.width;
if (rightEdge > viewportWidth - reservedRightPx) {
throw new Error(
`${label} overlaps the reserved close-control column: right edge ${rightEdge}, limit ${viewportWidth - reservedRightPx}.`
);
}
}
+164 -68
View File
@@ -1,20 +1,12 @@
import {
assertLocatorHasMinimumTouchTarget,
assertLocatorWithinSafeArea,
assertLocatorWithinViewport,
assertNoHorizontalOverflow,
} from "@vrtmrz/obsidian-test-session";
import type { Locator, Page } from "playwright";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
import { assertMobileDialogueLayout, assertMobileNoticeLayout, 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 dialogRunStateKey = "__livesyncE2EDialogMount";
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000);
const mobileViewport = { width: 390, height: 844 } as const;
const iPhoneSafeArea = { top: 47, right: 0, bottom: 34, left: 0 } as const;
type DialogueMode = "desktop" | "mobile";
@@ -39,66 +31,11 @@ type LiveSyncTestPlugin = {
type ObsidianTestApp = {
commands?: { executeCommandById(commandId: string): boolean };
emulateMobile?: (mobile: boolean) => void;
plugins?: { plugins: Record<string, LiveSyncTestPlugin | undefined> };
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
async function setMobileDialogueTestMode(enabled: boolean): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
if (enabled) {
await page.setViewportSize(mobileViewport);
}
await page.evaluate((nextEnabled) => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
if (typeof obsidianApp?.emulateMobile !== "function") {
throw new Error("app.emulateMobile is unavailable");
}
obsidianApp.emulateMobile(nextEnabled);
}, enabled);
await page.waitForFunction(
(nextEnabled) => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
return (
document.body.classList.contains("is-mobile") === nextEnabled &&
obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined
);
},
enabled,
{ timeout: uiTimeoutMs }
);
await page.evaluate(
(safeArea) => {
for (const edge of ["top", "right", "bottom", "left"] as const) {
const property = `--safe-area-inset-${edge}`;
if (safeArea === null) document.body.style.removeProperty(property);
else document.body.style.setProperty(property, `${safeArea[edge]}px`);
}
},
enabled ? iPhoneSafeArea : null
);
});
}
async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise<void> {
const dialogue = container.locator(".modal").last();
const closeButton = dialogue.locator(".modal-close-button");
await assertLocatorWithinViewport(page, dialogue, { label });
await assertNoHorizontalOverflow(page, dialogue, { label });
await assertLocatorWithinSafeArea(page, dialogue, {
label,
safeAreaInsets: iPhoneSafeArea,
});
await assertLocatorWithinSafeArea(page, closeButton, {
label: `${label} close button`,
safeAreaInsets: iPhoneSafeArea,
});
await assertLocatorHasMinimumTouchTarget(page, closeButton, {
label: `${label} close button`,
});
}
async function openRemoteSelectionDialogue(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate((stateKey) => {
@@ -155,6 +92,96 @@ async function assertDialogueRunCompleted(): Promise<void> {
}
}
async function verifyRemoteSizeNoticeAndDialogue(): Promise<{
compatibilityReview: string;
notice: string;
dialogue: string;
}> {
const compatibilityReviewScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"compatibility-review-dialogue.png",
async (page) => {
const compatibilityReview = page.locator(".modal-container").filter({
has: page
.locator(".modal-title")
.filter({ hasText: "Synchronisation paused for compatibility review" }),
});
await compatibilityReview.waitFor({ state: "visible", timeout: uiTimeoutMs });
const actions = compatibilityReview.locator(".vpk-action-dialog__actions--vertical");
await actions.waitFor({ state: "visible", timeout: uiTimeoutMs });
const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection);
if (flexDirection !== "column") {
throw new Error(`Expected vertically stacked compatibility actions, received ${flexDirection}.`);
}
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const compatibilityReview = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review" }),
});
await compatibilityReview
.getByRole("button", { name: "Keep synchronisation paused" })
.click({ timeout: uiTimeoutMs });
await compatibilityReview.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
const noticeScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"remote-size-startup-notice.png",
async (page) => {
const notice = page.locator(".notice").filter({
hasText: "Remote storage size notifications are not configured.",
});
await notice.waitFor({ state: "visible", timeout: uiTimeoutMs });
await notice.getByRole("link", { name: "Review options" }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const notice = page.locator(".notice").filter({
hasText: "Remote storage size notifications are not configured.",
});
await notice.getByRole("link", { name: "Review options" }).click({ timeout: uiTimeoutMs });
await notice.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
const dialogueScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"remote-size-review-dialogue.png",
async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }),
});
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
for (const action of [
"No, never warn please",
"800MB (Cloudant, fly.io)",
"2GB (Standard)",
"Ask me later",
]) {
await modal.getByRole("button", { name: action }).waitFor({ state: "visible", timeout: uiTimeoutMs });
}
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }),
});
await modal.getByRole("button", { name: "Ask me later" }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
return {
compatibilityReview: compatibilityReviewScreenshot,
notice: noticeScreenshot,
dialogue: dialogueScreenshot,
};
}
async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string> {
await openRemoteSelectionDialogue();
const screenshotPath = await captureObsidianDialogue(
@@ -235,6 +262,67 @@ async function verifySetupUriDialogue(mode: DialogueMode): Promise<string> {
return screenshotPath;
}
async function verifyMobileStartupReviews(): Promise<{ compatibilityReview: string; remoteSizeReview: string }> {
const compatibilityReviewScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"compatibility-review-dialogue-mobile.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.locator(".vpk-action-dialog__actions--vertical").waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
await assertMobileDialogueLayout(page, modal, "compatibility review dialogue");
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review" }),
});
await modal.getByRole("button", { name: "Keep synchronisation paused" }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
const compatibilityReminder = page.locator(".livesync-compatibility-review-notice");
await compatibilityReminder.waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertMobileNoticeLayout(page, compatibilityReminder, "compatibility review reminder");
const notice = page.locator(".notice").filter({
hasText: "Remote storage size notifications are not configured.",
});
await notice.getByRole("link", { name: "Review options" }).click({ timeout: uiTimeoutMs });
await notice.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
const remoteSizeReviewScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"remote-size-review-dialogue-mobile.png",
async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }),
});
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertMobileDialogueLayout(page, modal, "remote size review dialogue");
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }),
});
await modal.getByRole("button", { name: "Ask me later" }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
return {
compatibilityReview: compatibilityReviewScreenshot,
remoteSizeReview: remoteSizeReviewScreenshot,
};
}
async function main(): Promise<void> {
const binary = requireObsidianBinary();
const cli = discoverObsidianCli();
@@ -252,9 +340,8 @@ async function main(): Promise<void> {
pluginData: {
doctorProcessedVersion: "0.25.27",
isConfigured: true,
lastReadUpdates: Number.MAX_SAFE_INTEGER,
liveSync: false,
notifyThresholdOfRemoteStorageSize: 0,
notifyThresholdOfRemoteStorageSize: -1,
syncOnStart: false,
syncOnSave: false,
syncOnEditorSave: false,
@@ -265,13 +352,22 @@ async function main(): Promise<void> {
});
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
const remoteSizeScreenshots = await verifyRemoteSizeNoticeAndDialogue();
console.log(
`Compatibility review actions were stacked vertically, and the remote-size startup notice opened an untimed review dialogue successfully. Screenshots: ${remoteSizeScreenshots.compatibilityReview}, ${remoteSizeScreenshots.notice}, ${remoteSizeScreenshots.dialogue}`
);
const remoteScreenshot = await verifyRemoteSelectionDialogue("desktop");
console.log(`Remote selection dialogue mounted and closed successfully. Screenshot: ${remoteScreenshot}`);
const setupUriScreenshot = await verifySetupUriDialogue("desktop");
console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`);
await setMobileDialogueTestMode(true);
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs);
try {
const mobileStartupScreenshots = await verifyMobileStartupReviews();
console.log(
`Mobile compatibility and remote-size reviews passed viewport, safe-area, touch-target, and vertical-action checks. Screenshots: ${mobileStartupScreenshots.compatibilityReview}, ${mobileStartupScreenshots.remoteSizeReview}`
);
const mobileRemoteScreenshot = await verifyRemoteSelectionDialogue("mobile");
console.log(
`Mobile remote selection dialogue passed viewport, safe-area, touch-target, and close-control checks. Screenshot: ${mobileRemoteScreenshot}`
@@ -281,7 +377,7 @@ async function main(): Promise<void> {
`Mobile Setup URI dialogue passed viewport, safe-area, and touch-target checks. Screenshot: ${mobileSetupUriScreenshot}`
);
} finally {
await setMobileDialogueTestMode(false);
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs);
}
} finally {
if (session) {
+162 -4
View File
@@ -1,22 +1,171 @@
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 { obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.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<string, LiveSyncTestPlugin | undefined> };
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
async function verifyCompatibilityReview(): Promise<void> {
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<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate(() => {
@@ -28,8 +177,16 @@ async function verifyDeletionSettings(): Promise<void> {
const liveSyncSettings = page.locator(".sls-setting");
await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs });
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
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" })
@@ -64,8 +221,8 @@ async function main(): Promise<void> {
pluginData: {
doctorProcessedVersion: "0.25.27",
isConfigured: true,
lastReadUpdates: Number.MAX_SAFE_INTEGER,
liveSync: false,
versionUpFlash: compatibilityReviewMessage,
notifyThresholdOfRemoteStorageSize: 0,
syncOnStart: false,
syncOnSave: false,
@@ -77,8 +234,9 @@ async function main(): Promise<void> {
},
});
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await verifyCompatibilityReview();
await verifyDeletionSettings();
console.log("Deletion settings expose only effective user controls.");
console.log("Compatibility review and deletion settings expose only effective user controls.");
} finally {
if (session) {
await session.app.stop();