Improve troubleshooting and compatible setting handling

This commit is contained in:
vorotamoroz
2026-07-24 08:51:30 +00:00
parent 058da1f34c
commit d489452583
15 changed files with 868 additions and 50 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ The underlying `test:e2e:obsidian:<scenario>` scripts remain available for an im
`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then uses the permanent command to reopen the wizard on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios.
`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, and the Setup URI controls. It captures the representative dialogues on desktop and mobile, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that the remote-selection promise settles without an error. These UI-only checks do not apply a remote configuration or contact a remote service.
`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.
+542 -11
View File
@@ -6,6 +6,7 @@ import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobile
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import {
captureObsidianDialogue,
captureObsidianElement,
captureObsidianPage,
obsidianRemoteDebuggingPort,
withObsidianPage,
@@ -13,6 +14,7 @@ import {
import { createTemporaryVault } from "../runner/vault.ts";
const dialogRunStateKey = "__livesyncE2EDialogMount";
const repairRunStateKey = "__livesyncE2ETroubleshootingRepair";
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000);
type DialogueMode = "desktop" | "mobile";
@@ -20,6 +22,7 @@ type DialogueMode = "desktop" | "mobile";
type DialogueRunState = {
done: boolean;
error?: string;
expected?: unknown;
kind: string;
result?: unknown;
};
@@ -27,18 +30,39 @@ type DialogueRunState = {
type SetupManagerHandle = {
constructor: { name: string };
onSelectServer?: (settings: unknown, remoteType: string) => Promise<unknown>;
_askUseRemoteConfiguration?: (settings: unknown, preferred: unknown) => Promise<unknown>;
_checkAndAskResolvingMismatchedTweaks?: (preferred: unknown) => Promise<unknown>;
__addLog?: (message: string) => void;
};
type LiveSyncTestPlugin = {
core: {
fileHandler: {
createAllChunks(force: boolean): Promise<unknown>;
};
modules: SetupManagerHandle[];
settings: unknown;
settings: Record<string, unknown>;
};
};
type ObsidianSettingsController = {
open(): void;
openTabById(tabId: string): void;
};
type ObsidianVaultFile = {
path: string;
};
type ObsidianTestApp = {
commands?: { executeCommandById(commandId: string): boolean };
plugins?: { plugins: Record<string, LiveSyncTestPlugin | undefined> };
setting?: ObsidianSettingsController;
vault?: {
delete(file: ObsidianVaultFile, force: boolean): Promise<void>;
getFiles(): ObsidianVaultFile[];
read(file: ObsidianVaultFile): Promise<string>;
};
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
@@ -78,7 +102,62 @@ async function openSetupUriDialogue(): Promise<void> {
}
}
async function assertDialogueRunCompleted(): Promise<void> {
async function openConfigurationMismatchDialogue(
kind: "connected" | "connected-rebuild-recommended" | "remote-configuration"
): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate(
({ stateKey, kind }) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const resolver = plugin.core.modules.find(
(module) => module.constructor.name === "ModuleResolvingMismatchedTweaks"
);
if (resolver === undefined) throw new Error("Could not find ModuleResolvingMismatchedTweaks");
if (kind === "connected-rebuild-recommended") {
plugin.core.settings.autoAcceptCompatibleTweak = false;
}
const preferred =
kind === "connected-rebuild-recommended"
? {
...plugin.core.settings,
hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32",
}
: {
...plugin.core.settings,
enableCompression: !Boolean(plugin.core.settings.enableCompression),
};
const state: DialogueRunState = {
kind: `configuration-mismatch-${kind}`,
done: false,
expected: preferred,
};
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
const operation =
kind === "remote-configuration"
? resolver._askUseRemoteConfiguration?.(plugin.core.settings, preferred)
: resolver._checkAndAskResolvingMismatchedTweaks?.(preferred);
if (operation === undefined) {
throw new Error(`The configuration mismatch resolver does not support ${kind}.`);
}
void operation.then(
(result) => {
state.result = result;
state.done = true;
},
(error: unknown) => {
state.error = error instanceof Error ? error.message : String(error);
state.done = true;
}
);
},
{ stateKey: dialogRunStateKey, kind }
);
});
}
async function assertDialogueRunCompleted(): Promise<DialogueRunState> {
const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.waitForFunction(
(stateKey) =>
@@ -92,11 +171,12 @@ async function assertDialogueRunCompleted(): Promise<void> {
);
});
if (!state) {
throw new Error("The remote selection dialogue did not record its completion state.");
throw new Error("The mounted dialogue did not record its completion state.");
}
if (state.error) {
throw new Error(`The remote selection dialogue failed: ${state.error}`);
throw new Error(`The mounted dialogue failed: ${state.error}`);
}
return state;
}
async function verifyRemoteSizeNoticeAndDialogue(): Promise<{
@@ -347,6 +427,442 @@ async function verifySetupUriDialogue(mode: DialogueMode): Promise<string> {
return screenshotPath;
}
async function verifyCompatibleMismatchAutoAdjustment(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate((stateKey) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const resolver = plugin.core.modules.find(
(module) => module.constructor.name === "ModuleResolvingMismatchedTweaks"
);
if (typeof resolver?._checkAndAskResolvingMismatchedTweaks !== "function") {
throw new Error("Could not find the configuration mismatch resolver");
}
plugin.core.settings.autoAcceptCompatibleTweak = undefined;
const currentModified =
typeof plugin.core.settings.tweakModified === "number" ? plugin.core.settings.tweakModified : 0;
const preferred = {
...plugin.core.settings,
hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32",
tweakModified: currentModified + 1,
};
const state: DialogueRunState = {
kind: "configuration-mismatch-compatible-auto-adjustment",
done: false,
expected: preferred,
};
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
void resolver._checkAndAskResolvingMismatchedTweaks(preferred).then(
(result) => {
state.result = result;
state.done = true;
},
(error: unknown) => {
state.error = error instanceof Error ? error.message : String(error);
state.done = true;
}
);
}, dialogRunStateKey);
});
const state = await assertDialogueRunCompleted();
if (!Array.isArray(state.result) || state.result.length !== 2) {
throw new Error("The compatible mismatch did not return its settings and rebuild decision.");
}
const [appliedSettings, shouldRebuild] = state.result;
const expectedSettings = state.expected;
if (
typeof appliedSettings !== "object" ||
appliedSettings === null ||
typeof expectedSettings !== "object" ||
expectedSettings === null ||
!("hashAlg" in appliedSettings) ||
!("hashAlg" in expectedSettings) ||
appliedSettings.hashAlg !== expectedSettings.hashAlg ||
shouldRebuild !== false
) {
throw new Error("The compatible mismatch was not adjusted to the newer setting without a rebuild.");
}
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const autoAcceptEnabled = await page.evaluate(() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.settings.autoAcceptCompatibleTweak;
});
if (autoAcceptEnabled !== true) {
throw new Error("Compatible mismatch auto-adjustment was not persisted as the default.");
}
for (const title of ["Auto-Accept Available", "Configuration Mismatch Detected"]) {
const dialogue = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: title }),
});
if ((await dialogue.count()) !== 0) {
throw new Error(`Compatible mismatch auto-adjustment unexpectedly opened '${title}'.`);
}
}
});
}
async function verifyCompatibleAlignmentSettingDefault(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const persistedValue = await page.evaluate(() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
return plugin.core.settings.autoAcceptCompatibleTweak;
});
if (persistedValue !== undefined) {
throw new Error(
`The default-display fixture expected an undefined preference, received ${persistedValue}.`
);
}
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="Advanced"]').click({ timeout: uiTimeoutMs });
const settingItem = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Auto-accept compatible tweak mismatches", { exact: true }),
});
await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs });
const toggle = settingItem.locator(".checkbox-container");
if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) {
throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined.");
}
});
}
async function verifyConfigurationMismatchDialogues(): Promise<{ general: string; fetch: string }> {
await verifyCompatibleMismatchAutoAdjustment();
await openConfigurationMismatchDialogue("remote-configuration");
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Use Remote Configuration" }),
});
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
await modal
.getByRole("button", { name: "Use configured settings", exact: true })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
await modal.getByRole("button", { name: "Dismiss", exact: true }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
await assertDialogueRunCompleted();
await openConfigurationMismatchDialogue("connected");
const generalScreenshotPath = await captureObsidianElement(
obsidianRemoteDebuggingPort(),
"troubleshooting-configuration-mismatch-dialogue.png",
async (page) => {
const container = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }),
});
const modal = container.locator(".modal").last();
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
for (const action of ["Apply settings to this device", "Update remote database settings"]) {
await modal
.getByRole("button", { name: action, exact: true })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
}
await modal.getByRole("button", { name: /Dismiss$/u }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
for (const retiredAction of ["Use configured", "Update with mine"]) {
if ((await modal.getByRole("button", { name: retiredAction, exact: true }).count()) !== 0) {
throw new Error(`The mismatch dialogue still exposes the retired action '${retiredAction}'.`);
}
}
const actions = modal.locator(".setting-item-control").last();
const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection);
if (flexDirection !== "column") {
throw new Error(`Expected vertically stacked mismatch actions, received ${flexDirection}.`);
}
return modal;
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }),
});
await modal.getByRole("button", { name: /Dismiss$/u }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
await assertDialogueRunCompleted();
await openConfigurationMismatchDialogue("connected-rebuild-recommended");
const fetchScreenshotPath = await captureObsidianElement(
obsidianRemoteDebuggingPort(),
"troubleshooting-configuration-mismatch-fetch-dialogue.png",
async (page) => {
const container = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }),
});
const modal = container.locator(".modal").last();
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
await modal
.getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
return modal;
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }),
});
await modal
.getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true })
.click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
const fetchResult = await assertDialogueRunCompleted();
if (!Array.isArray(fetchResult.result) || fetchResult.result.length !== 2) {
throw new Error("The configuration-mismatch Fetch action did not return its settings and Fetch decision.");
}
const [appliedSettings, shouldFetch] = fetchResult.result;
const expectedSettings = fetchResult.expected;
if (
typeof appliedSettings !== "object" ||
appliedSettings === null ||
typeof expectedSettings !== "object" ||
expectedSettings === null ||
!("hashAlg" in appliedSettings) ||
!("hashAlg" in expectedSettings) ||
appliedSettings.hashAlg !== expectedSettings.hashAlg ||
shouldFetch !== true
) {
throw new Error("The configuration-mismatch Fetch action did not apply the remote setting before Fetch.");
}
return { general: generalScreenshotPath, fetch: fetchScreenshotPath };
}
async function executeRegisteredCommand(commandId: string): Promise<void> {
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
return await page.evaluate(
(id) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(id) === true,
commandId
);
});
if (!opened) {
throw new Error(`The command was not registered or could not be executed: ${commandId}`);
}
}
async function verifyLogAndReportSurfaces(): Promise<{ log: string; report: string }> {
await executeRegisteredCommand("obsidian-livesync:view-log");
const logScreenshot = await captureObsidianElement(
obsidianRemoteDebuggingPort(),
"troubleshooting-show-log.png",
async (page) => {
const logPane = page.locator(".logpane");
await logPane.waitFor({ state: "visible", timeout: uiTimeoutMs });
for (const label of ["Wrap", "Auto scroll", "Pause"]) {
await logPane.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
}
await logPane.getByRole("button", { name: "Close", exact: true }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
return logPane;
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const logPane = page.locator(".logpane");
await logPane.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs });
await logPane.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
await executeRegisteredCommand("obsidian-livesync:dump-debug-info");
const reportScreenshot = await captureObsidianElement(
obsidianRemoteDebuggingPort(),
"troubleshooting-full-report.png",
async (page) => {
const modal = page.locator(".modal-container").filter({
hasText: "Your Debug info is ready to be copied",
});
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
const report = await modal.locator("textarea").inputValue({ timeout: uiTimeoutMs });
if (!report.includes("# ---- Debug Info Dump ----")) {
throw new Error("The full-report dialogue did not contain the generated debug report.");
}
await modal.getByRole("button", { name: "OK", exact: true }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
return modal.locator(".modal").last();
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
hasText: "Your Debug info is ready to be copied",
});
await modal.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
return { log: logScreenshot, report: reportScreenshot };
}
async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
const screenshotPath = await captureObsidianElement(
obsidianRemoteDebuggingPort(),
"troubleshooting-hatch.png",
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="Hatch"]').click({ timeout: uiTimeoutMs });
for (const label of [
"Write logs into the file",
"Recreate missing chunks for all files",
"Verify and repair all files",
]) {
await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
}
await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
await liveSyncSettings
.locator(".setting-item-name", { hasText: "Recreate missing chunks for all files" })
.scrollIntoViewIfNeeded();
return liveSyncSettings;
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const liveSyncSettings = page.locator(".sls-setting");
const logSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Write logs into the file", { exact: true }),
});
await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs });
await page.waitForFunction(
() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.settings.writeLogToTheFile === true;
},
undefined,
{ timeout: uiTimeoutMs }
);
const persistentLogMarker = "E2E persistent troubleshooting log";
await page.evaluate((marker) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const module = plugin.core.modules.find((candidate) => candidate.constructor.name === "ModuleLog");
if (typeof module?.__addLog !== "function") throw new Error("Could not find ModuleLog");
module.__addLog(marker);
}, persistentLogMarker);
await page.waitForFunction(
async (marker) => {
const vault = (globalThis as ObsidianTestGlobal).app?.vault;
if (vault === undefined) return false;
const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_"));
if (logFile === undefined) return false;
return (await vault.read(logFile)).includes(marker);
},
persistentLogMarker,
{ timeout: uiTimeoutMs }
);
// Saving a toggle refreshes the settings pane. Resolve the visible control again so the
// second action does not target the detached pre-save element.
const refreshedLogToggle = page
.locator(".sls-setting:visible .setting-item:visible")
.filter({
has: page.getByText("Write logs into the file", { exact: true }),
})
.locator(".checkbox-container:visible")
.last();
await refreshedLogToggle.click({ timeout: uiTimeoutMs });
await page.waitForFunction(
() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.settings.writeLogToTheFile === false;
},
undefined,
{ timeout: uiTimeoutMs }
);
await page.evaluate(async () => {
const vault = (globalThis as ObsidianTestGlobal).app?.vault;
if (vault === undefined) throw new Error("Obsidian Vault is unavailable");
const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_"));
if (logFile === undefined) throw new Error("The persistent troubleshooting log was not created");
await vault.delete(logFile, true);
});
await page.waitForFunction(
() =>
!(globalThis as ObsidianTestGlobal).app?.vault
?.getFiles()
.some((file) => file.path.startsWith("livesync_log_")),
undefined,
{ timeout: uiTimeoutMs }
);
await page.evaluate((stateKey) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const original = plugin.core.fileHandler.createAllChunks.bind(plugin.core.fileHandler);
const state: DialogueRunState = { kind: "recreate-missing-chunks", done: false };
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
plugin.core.fileHandler.createAllChunks = async (force) => {
try {
state.result = await original(force);
} catch (error) {
state.error = error instanceof Error ? error.message : String(error);
} finally {
state.done = true;
plugin.core.fileHandler.createAllChunks = original;
}
};
}, repairRunStateKey);
await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).click({
timeout: uiTimeoutMs,
});
await page.waitForFunction(
(stateKey) =>
(globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey]?.done === true,
repairRunStateKey,
{ timeout: uiTimeoutMs }
);
const repairState = await page.evaluate(
(stateKey) => (globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey],
repairRunStateKey
);
if (repairState?.error) {
throw new Error(`Recreate missing chunks failed: ${repairState.error}`);
}
await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).click({
timeout: uiTimeoutMs,
});
await page
.locator(".notice")
.filter({ hasText: /^done$/u })
.waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
});
return screenshotPath;
}
async function verifyMobileStartupReviews(): Promise<{ compatibilityReview: string; remoteSizeReview: string }> {
const compatibilityReviewScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
@@ -430,13 +946,14 @@ async function main(): Promise<void> {
dbName: "dialog-mounts-ui-only",
},
{
notifyThresholdOfRemoteStorageSize: -1,
syncOnStart: false,
syncOnSave: false,
syncOnEditorSave: false,
syncOnFileOpen: false,
syncAfterMerge: false,
periodicReplication: false,
notifyThresholdOfRemoteStorageSize: -1,
syncOnStart: false,
syncOnSave: false,
syncOnEditorSave: false,
syncOnFileOpen: false,
syncAfterMerge: false,
periodicReplication: false,
useAdvancedMode: true,
}
),
});
@@ -480,6 +997,20 @@ async function main(): Promise<void> {
);
const setupUriScreenshot = await verifySetupUriDialogue("desktop");
console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`);
await verifyCompatibleAlignmentSettingDefault();
console.log("The undefined compatible-setting preference is displayed with its effective enabled default.");
const mismatchScreenshots = await verifyConfigurationMismatchDialogues();
console.log(
`A mismatch limited to compatible chunk settings was adjusted without a dialogue, current manual mismatch actions mounted successfully, and the Fetch action applied the remote setting before scheduling Fetch. Screenshots: ${mismatchScreenshots.general}, ${mismatchScreenshots.fetch}`
);
const troubleshootingScreenshots = await verifyLogAndReportSurfaces();
console.log(
`Show log and the generated full-report dialogue were reached through their registered commands. Screenshots: ${troubleshootingScreenshots.log}, ${troubleshootingScreenshots.report}`
);
const hatchScreenshot = await verifyHatchSurfacesAndSafeActions();
console.log(
`Hatch repair controls were reachable, safe empty-fixture runs completed, and persistent logging was enabled, verified, disabled, and removed. Screenshot: ${hatchScreenshot}`
);
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs);
try {