mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-25 14:02:58 +00:00
517 lines
24 KiB
TypeScript
517 lines
24 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
|
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
|
import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
|
import {
|
|
captureObsidianDialogue,
|
|
captureObsidianPage,
|
|
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);
|
|
|
|
type DialogueMode = "desktop" | "mobile";
|
|
|
|
type DialogueRunState = {
|
|
done: boolean;
|
|
error?: string;
|
|
kind: string;
|
|
result?: unknown;
|
|
};
|
|
|
|
type SetupManagerHandle = {
|
|
constructor: { name: string };
|
|
onSelectServer?: (settings: unknown, remoteType: string) => Promise<unknown>;
|
|
};
|
|
|
|
type LiveSyncTestPlugin = {
|
|
core: {
|
|
modules: SetupManagerHandle[];
|
|
settings: unknown;
|
|
};
|
|
};
|
|
|
|
type ObsidianTestApp = {
|
|
commands?: { executeCommandById(commandId: string): boolean };
|
|
plugins?: { plugins: Record<string, LiveSyncTestPlugin | undefined> };
|
|
};
|
|
|
|
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
|
|
|
|
async function openRemoteSelectionDialogue(): 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 manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager");
|
|
if (typeof manager?.onSelectServer !== "function") throw new Error("Could not find SetupManager");
|
|
const state: DialogueRunState = { kind: "remote-selection", done: false };
|
|
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
|
|
void manager.onSelectServer(plugin.core.settings, "unknown").then(
|
|
(result) => {
|
|
state.result = result;
|
|
state.done = true;
|
|
},
|
|
(error: unknown) => {
|
|
state.error = error instanceof Error ? error.message : String(error);
|
|
state.done = true;
|
|
}
|
|
);
|
|
}, dialogRunStateKey);
|
|
});
|
|
}
|
|
|
|
async function openSetupUriDialogue(): Promise<void> {
|
|
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
return await page.evaluate(
|
|
(commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true,
|
|
"obsidian-livesync:livesync-opensetupuri"
|
|
);
|
|
});
|
|
if (!opened) {
|
|
throw new Error("The Setup URI command was not registered or could not be executed.");
|
|
}
|
|
}
|
|
|
|
async function assertDialogueRunCompleted(): Promise<void> {
|
|
const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
await page.waitForFunction(
|
|
(stateKey) =>
|
|
(globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey]?.done === true,
|
|
dialogRunStateKey,
|
|
{ timeout: uiTimeoutMs }
|
|
);
|
|
return await page.evaluate(
|
|
(stateKey) => (globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey],
|
|
dialogRunStateKey
|
|
);
|
|
});
|
|
if (!state) {
|
|
throw new Error("The remote selection dialogue did not record its completion state.");
|
|
}
|
|
if (state.error) {
|
|
throw new Error(`The remote selection dialogue failed: ${state.error}`);
|
|
}
|
|
}
|
|
|
|
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(
|
|
obsidianRemoteDebuggingPort(),
|
|
`setup-remote-selection-dialogue${mode === "mobile" ? "-mobile" : ""}.png`,
|
|
async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
|
});
|
|
const dialogue = modal.locator(".dialog-host");
|
|
await modal.waitFor({
|
|
state: "visible",
|
|
timeout: uiTimeoutMs,
|
|
});
|
|
for (const label of ["CouchDB", "S3-compatible Object Storage", "Peer-to-Peer (P2P)"]) {
|
|
await dialogue.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
}
|
|
await dialogue
|
|
.getByText(
|
|
"No central data-storage server is required, but a signalling relay is required for peer discovery.",
|
|
{ exact: false }
|
|
)
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
await modal
|
|
.getByRole("button", { name: "No, please take me back" })
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
if (mode === "mobile") {
|
|
await assertMobileDialogueLayout(page, modal, "remote selection dialogue");
|
|
}
|
|
}
|
|
);
|
|
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
|
});
|
|
if (mode === "mobile") {
|
|
const previousDialogue = await modal.locator(".modal").last().elementHandle();
|
|
if (previousDialogue === null) {
|
|
throw new Error("The remote selection dialogue did not expose a close control.");
|
|
}
|
|
await modal.locator(".modal-close-button").click({ timeout: uiTimeoutMs });
|
|
await page.waitForFunction((element) => !element.isConnected, previousDialogue, { timeout: uiTimeoutMs });
|
|
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
}
|
|
await modal.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs });
|
|
});
|
|
await assertDialogueRunCompleted();
|
|
return screenshotPath;
|
|
}
|
|
|
|
async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise<string> {
|
|
await openRemoteSelectionDialogue();
|
|
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
const remoteSelection = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
|
});
|
|
await remoteSelection
|
|
.locator("label")
|
|
.filter({ hasText: "CouchDB" })
|
|
.locator('input[type="radio"]')
|
|
.first()
|
|
.check({ timeout: uiTimeoutMs });
|
|
await remoteSelection
|
|
.getByRole("button", { name: "Continue to CouchDB setup", exact: true })
|
|
.click({ timeout: uiTimeoutMs });
|
|
});
|
|
const screenshotPath = await captureObsidianDialogue(
|
|
obsidianRemoteDebuggingPort(),
|
|
`setup-couchdb-dialogue${mode === "mobile" ? "-mobile" : ""}.png`,
|
|
async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "CouchDB Configuration" }),
|
|
});
|
|
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
for (const label of [
|
|
"Check server requirements",
|
|
"Test connection and save",
|
|
"Save without connecting",
|
|
"Cancel",
|
|
]) {
|
|
await modal.getByRole("button", { name: label, exact: true }).waitFor({
|
|
state: "visible",
|
|
timeout: uiTimeoutMs,
|
|
});
|
|
}
|
|
await modal
|
|
.getByText(
|
|
"This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server.",
|
|
{ exact: false }
|
|
)
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
await modal
|
|
.getByText("CouchDB validates the database name when you connect.", { exact: false })
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
if ((await modal.getByRole("button", { name: "Continue anyway", exact: true }).count()) !== 0) {
|
|
throw new Error("CouchDB onboarding still exposes the ambiguous Continue anyway action.");
|
|
}
|
|
const buttonGroups = modal.locator(".button-group");
|
|
for (let index = 0; index < (await buttonGroups.count()); index++) {
|
|
const flexDirection = await buttonGroups.nth(index).evaluate((element) => {
|
|
return getComputedStyle(element).flexDirection;
|
|
});
|
|
if (flexDirection !== "column") {
|
|
throw new Error(`Expected vertical CouchDB actions, received ${flexDirection}.`);
|
|
}
|
|
}
|
|
if (mode === "mobile") {
|
|
await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue");
|
|
}
|
|
}
|
|
);
|
|
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "CouchDB Configuration" }),
|
|
});
|
|
await modal.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs });
|
|
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
|
});
|
|
await assertDialogueRunCompleted();
|
|
return screenshotPath;
|
|
}
|
|
|
|
async function verifySetupUriDialogue(mode: DialogueMode): Promise<string> {
|
|
await openSetupUriDialogue();
|
|
const screenshotPath = await captureObsidianDialogue(
|
|
obsidianRemoteDebuggingPort(),
|
|
`setup-uri-dialogue${mode === "mobile" ? "-mobile" : ""}.png`,
|
|
async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "Enter Setup URI" }),
|
|
});
|
|
await modal.waitFor({
|
|
state: "visible",
|
|
timeout: uiTimeoutMs,
|
|
});
|
|
await modal
|
|
.locator('input[placeholder^="obsidian://setuplivesync"]')
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
await modal.locator('input[name="password"]').waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
await modal
|
|
.getByRole("button", { name: "Test Settings and Continue" })
|
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
await modal.getByRole("button", { name: "Cancel" }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
if (mode === "mobile") {
|
|
await assertMobileDialogueLayout(page, modal, "Setup URI dialogue");
|
|
}
|
|
}
|
|
);
|
|
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
|
const modal = page.locator(".modal-container").filter({
|
|
has: page.locator(".modal-title").filter({ hasText: "Enter Setup URI" }),
|
|
});
|
|
await modal.getByRole("button", { name: "Cancel" }).click({ timeout: uiTimeoutMs });
|
|
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
|
});
|
|
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();
|
|
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: createE2eCouchDbPluginData(
|
|
{
|
|
uri: "http://127.0.0.1:5984",
|
|
username: "",
|
|
password: "",
|
|
dbName: "dialog-mounts-ui-only",
|
|
},
|
|
{
|
|
notifyThresholdOfRemoteStorageSize: -1,
|
|
syncOnStart: false,
|
|
syncOnSave: false,
|
|
syncOnEditorSave: false,
|
|
syncOnFileOpen: false,
|
|
syncAfterMerge: false,
|
|
periodicReplication: false,
|
|
}
|
|
),
|
|
});
|
|
try {
|
|
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
|
} catch (error) {
|
|
const screenshot = await captureObsidianPage(
|
|
obsidianRemoteDebuggingPort(),
|
|
"dialog-mounts-core-not-ready.png",
|
|
async () => undefined
|
|
);
|
|
console.error(`Core readiness diagnostic screenshot: ${screenshot}`);
|
|
const persistedSettings = JSON.parse(
|
|
await readFile(join(session.install.pluginDir, "data.json"), "utf8")
|
|
) as Record<string, unknown>;
|
|
console.error(
|
|
`Persisted readiness settings: ${JSON.stringify({
|
|
isConfigured: persistedSettings.isConfigured,
|
|
remoteType: persistedSettings.remoteType,
|
|
settingVersion: persistedSettings.settingVersion,
|
|
couchDB_URI: persistedSettings.couchDB_URI,
|
|
couchDB_DBNAME: persistedSettings.couchDB_DBNAME,
|
|
remoteConfigurationCount: Object.keys(
|
|
(persistedSettings.remoteConfigurations as Record<string, unknown> | undefined) ?? {}
|
|
).length,
|
|
})}`
|
|
);
|
|
throw error;
|
|
}
|
|
|
|
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 couchDBScreenshot = await verifyCouchDBSettingsDialogue("desktop");
|
|
console.log(
|
|
`CouchDB settings mode exposed explicit connection, unverified-save, and server-check actions. Screenshot: ${couchDBScreenshot}`
|
|
);
|
|
const setupUriScreenshot = await verifySetupUriDialogue("desktop");
|
|
console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`);
|
|
|
|
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}`
|
|
);
|
|
const mobileCouchDBScreenshot = await verifyCouchDBSettingsDialogue("mobile");
|
|
console.log(
|
|
`Mobile CouchDB settings dialogue passed viewport, touch-target, and vertical-action checks. Screenshot: ${mobileCouchDBScreenshot}`
|
|
);
|
|
const mobileSetupUriScreenshot = await verifySetupUriDialogue("mobile");
|
|
console.log(
|
|
`Mobile Setup URI dialogue passed viewport, safe-area, and touch-target checks. Screenshot: ${mobileSetupUriScreenshot}`
|
|
);
|
|
} finally {
|
|
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs);
|
|
}
|
|
} 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);
|
|
});
|