mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-25 05:53:00 +00:00
Improve troubleshooting and compatible setting handling
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user