mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-25 05:53:00 +00:00
Clarify remote setup and verify manual CouchDB onboarding
This commit is contained in:
@@ -93,12 +93,14 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe
|
||||
|
||||
`test:e2e:obsidian:p2p-pane` starts one unconfigured CouchDB-only session and one configured P2P session. It proves that the command remains registered while the retired command, automatic pane, and unconfigured ribbon entry are absent. For the configured profile, it verifies that the ribbon and current status command reach the pane without opening it at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions.
|
||||
|
||||
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||
|
||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, starts from configured plug-in data without the device-local compatibility marker, and verifies the copied-or-restored Vault explanation in the actual compatibility dialogue. It captures the summary and details, resumes explicitly, confirms that the marker was recorded, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||
|
||||
The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation.
|
||||
|
||||
`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible first-device onboarding path without a bootstrap Setup URI. It enters end-to-end encryption and CouchDB details, runs the read-only server-requirements check, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After first-device Rebuild, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked.
|
||||
|
||||
If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, and the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory.
|
||||
|
||||
The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a first-device decision. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode.
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import {
|
||||
assertCouchDbReachable,
|
||||
deleteCouchDbDatabase,
|
||||
loadCouchDbConfig,
|
||||
makeUniqueDatabaseName,
|
||||
waitForCouchDbDocs,
|
||||
type CouchDbConfig,
|
||||
} from "../runner/couchdb.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { assertEqual, pushLocalChanges, waitForLocalDatabaseEntry } from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import {
|
||||
acknowledgeDisabledOptionalFeatures,
|
||||
captureAndStartInitialisation,
|
||||
captureGuideDialogue,
|
||||
confirmFastFetch,
|
||||
confirmRebuild,
|
||||
enterSetupURI,
|
||||
finishInitialisation,
|
||||
generateSetupURIFromDevice,
|
||||
modalByTitle,
|
||||
resumeCompatibilityReviewIfShown,
|
||||
selectRadioOption,
|
||||
skipMissingRemoteConfiguration,
|
||||
type SetupArtifact,
|
||||
} from "../runner/setupUri.ts";
|
||||
import { captureObsidianPage, withObsidianPage } from "../runner/ui.ts";
|
||||
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000";
|
||||
process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000";
|
||||
|
||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000);
|
||||
const notePath = "E2E/manual-couchdb/from-first-device.md";
|
||||
const noteContent = "# Manual CouchDB setup\n\nThis note was sent by the manually configured first device.\n";
|
||||
const returnNotePath = "E2E/manual-couchdb/from-second-device.md";
|
||||
const returnNoteContent =
|
||||
"# Manual CouchDB return journey\n\nThis note returned through a Setup URI generated by the first device.\n";
|
||||
const captures = {
|
||||
scenario: "couchdb-manual-setup-workflow",
|
||||
guide: "couchdb-manual",
|
||||
} as const;
|
||||
|
||||
type RunnerContext = {
|
||||
binary: string;
|
||||
cliBinary: string;
|
||||
couchDb: CouchDbConfig;
|
||||
dbName: string;
|
||||
activeSessions: Set<ObsidianLiveSyncSession>;
|
||||
};
|
||||
|
||||
async function startUnconfiguredSession(
|
||||
context: RunnerContext,
|
||||
vault: TemporaryVault
|
||||
): Promise<ObsidianLiveSyncSession> {
|
||||
const session = await startObsidianLiveSyncSession({
|
||||
binary: context.binary,
|
||||
cliBinary: context.cliBinary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
});
|
||||
context.activeSessions.add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise<void> {
|
||||
if (!context.activeSessions.has(session)) return;
|
||||
await session.app.stop();
|
||||
context.activeSessions.delete(session);
|
||||
}
|
||||
|
||||
async function stopTrackedSessions(context: RunnerContext): Promise<void> {
|
||||
for (const session of [...context.activeSessions]) {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
}
|
||||
|
||||
async function captureFailure(session: ObsidianLiveSyncSession, label: string): Promise<void> {
|
||||
const screenshot = await captureObsidianPage(
|
||||
session.remoteDebuggingPort,
|
||||
`couchdb-manual-${label}-failure.png`,
|
||||
async () => undefined
|
||||
).catch(() => undefined);
|
||||
if (screenshot) {
|
||||
console.error(`Manual CouchDB failure screenshot: ${screenshot}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, dbName: string): Promise<string[]> {
|
||||
const screenshots: string[] = [];
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" });
|
||||
await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs });
|
||||
|
||||
const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync");
|
||||
await intro.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await selectRadioOption(intro, "I am setting this up for the first time");
|
||||
await intro
|
||||
.getByRole("button", { name: "Yes, I want to set up a new synchronisation" })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-connection-method.png", "Connection Method")
|
||||
);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const method = modalByTitle(page, "Connection Method");
|
||||
await selectRadioOption(method, "Configure a remote manually");
|
||||
await method
|
||||
.getByRole("button", { name: "Proceed with manual configuration" })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
|
||||
const encryption = modalByTitle(page, "End-to-End Encryption");
|
||||
await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await encryption
|
||||
.locator("label.row")
|
||||
.filter({ hasText: "End-to-End Encryption" })
|
||||
.locator('input[type="checkbox"]')
|
||||
.first()
|
||||
.check({ timeout: uiTimeoutMs });
|
||||
await encryption
|
||||
.locator("label.row")
|
||||
.filter({ hasText: "Obfuscate Properties" })
|
||||
.locator('input[type="checkbox"]')
|
||||
.first()
|
||||
.check({ timeout: uiTimeoutMs });
|
||||
await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url"));
|
||||
});
|
||||
screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption"));
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const encryption = modalByTitle(page, "End-to-End Encryption");
|
||||
await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-remote-selection.png", "Choose a synchronisation remote")
|
||||
);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const remoteSelection = modalByTitle(page, "Choose a synchronisation remote");
|
||||
await selectRadioOption(remoteSelection, "CouchDB");
|
||||
await remoteSelection
|
||||
.getByRole("button", { name: "Continue to CouchDB setup", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await couchDB.locator('input[name="couchdb-url"]').fill(couchDb.uri);
|
||||
await couchDB.locator('input[name="couchdb-username"]').fill(couchDb.username);
|
||||
await couchDB.locator('input[name="couchdb-password"]').fill(couchDb.password);
|
||||
await couchDB.locator('input[name="couchdb-database"]').fill(dbName);
|
||||
});
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-connection-details.png", "CouchDB Configuration")
|
||||
);
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB
|
||||
.getByRole("button", { name: "Check server requirements", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
const summary = couchDB.locator(".check-results summary");
|
||||
await summary.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await summary
|
||||
.filter({ hasText: /All checks passed successfully!|issue\(s\) detected!/u })
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const errors = couchDB.locator(".check-result.error");
|
||||
if ((await errors.count()) > 0) {
|
||||
const messages = await errors.locator(".message").allTextContents();
|
||||
throw new Error(`The documented CouchDB fixture failed its server requirements: ${messages.join(" | ")}`);
|
||||
}
|
||||
const details = couchDB.locator(".check-results details");
|
||||
if (!(await details.evaluate((element) => (element as HTMLDetailsElement).open))) {
|
||||
await summary.click({ timeout: uiTimeoutMs });
|
||||
}
|
||||
});
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-server-requirements.png", "CouchDB Configuration")
|
||||
);
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB
|
||||
.getByRole("button", { name: "Create or connect to database and continue", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
await modalByTitle(page, "Setup Complete: Preparing to Initialise Server").waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
});
|
||||
return screenshots;
|
||||
}
|
||||
|
||||
async function writeNoteViaObsidian(
|
||||
cliBinary: string,
|
||||
environment: NodeJS.ProcessEnv,
|
||||
path: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const content=${JSON.stringify(content)};`,
|
||||
"const folder=path.split('/').slice(0,-1).join('/');",
|
||||
"if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);",
|
||||
"const existing=app.vault.getAbstractFileByPath(path);",
|
||||
"if(existing) await app.vault.modify(existing,content);",
|
||||
"else await app.vault.create(path,content);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
environment
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVaultFile(
|
||||
vault: TemporaryVault,
|
||||
path: string,
|
||||
expected: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000)
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastContent = "";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
lastContent = await readFile(join(vault.path, path), "utf8");
|
||||
if (lastContent === expected) return;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`);
|
||||
}
|
||||
|
||||
async function waitForRemoteEntry(context: RunnerContext, entry: { id: string; children: string[] }): Promise<void> {
|
||||
await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => {
|
||||
const ids = new Set(docs.map((doc) => doc._id));
|
||||
return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId));
|
||||
});
|
||||
}
|
||||
|
||||
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 couchDb = await loadCouchDbConfig();
|
||||
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "manual-setup");
|
||||
const vaultA = await createTemporaryVault();
|
||||
const vaultB = await createTemporaryVault();
|
||||
const context: RunnerContext = {
|
||||
binary,
|
||||
cliBinary: cli.binary,
|
||||
couchDb,
|
||||
dbName,
|
||||
activeSessions: new Set(),
|
||||
};
|
||||
const screenshots: string[] = [];
|
||||
let secondDeviceArtifact: SetupArtifact | undefined;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
console.log(`Using Obsidian executable: ${binary}`);
|
||||
console.log(`Temporary Vault A: ${vaultA.path}`);
|
||||
console.log(`Temporary Vault B: ${vaultB.path}`);
|
||||
console.log(`CouchDB database to be created by the onboarding dialogue: ${dbName}`);
|
||||
|
||||
let session = await startUnconfiguredSession(context, vaultA);
|
||||
try {
|
||||
screenshots.push(...(await enterManualCouchDBSettings(session.remoteDebuggingPort, couchDb, dbName)));
|
||||
screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new", captures));
|
||||
screenshots.push(await confirmRebuild(session.remoteDebuggingPort, captures));
|
||||
screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort, captures));
|
||||
screenshots.push(await acknowledgeDisabledOptionalFeatures(session.remoteDebuggingPort, captures));
|
||||
const state = await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
assertEqual(state.activeConfigurationId !== "", true, "Manual CouchDB setup did not activate a profile.");
|
||||
assertEqual(
|
||||
state.remoteConfigurationCount,
|
||||
1,
|
||||
"Manual CouchDB setup did not persist exactly one remote profile."
|
||||
);
|
||||
|
||||
await writeNoteViaObsidian(context.cliBinary, session.cliEnv, notePath, noteContent);
|
||||
const entry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForRemoteEntry(context, entry);
|
||||
|
||||
const generated = await generateSetupURIFromDevice(
|
||||
session.remoteDebuggingPort,
|
||||
randomBytes(24).toString("base64url"),
|
||||
captures
|
||||
);
|
||||
secondDeviceArtifact = generated.artifact;
|
||||
screenshots.push(...generated.screenshots);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "first-device");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
session = await startUnconfiguredSession(context, vaultB);
|
||||
try {
|
||||
if (!secondDeviceArtifact) {
|
||||
throw new Error("The manually configured first device did not generate a Setup URI.");
|
||||
}
|
||||
screenshots.push(
|
||||
await enterSetupURI(session.remoteDebuggingPort, "existing", secondDeviceArtifact, captures)
|
||||
);
|
||||
screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "existing", captures));
|
||||
screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort, captures)));
|
||||
await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForVaultFile(vaultB, notePath, noteContent);
|
||||
|
||||
await writeNoteViaObsidian(context.cliBinary, session.cliEnv, returnNotePath, returnNoteContent);
|
||||
const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, returnNotePath);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForRemoteEntry(context, returnEntry);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "second-device");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
session = await startUnconfiguredSession(context, vaultA);
|
||||
try {
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForVaultFile(vaultA, returnNotePath, returnNoteContent);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "return-journey");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Manual CouchDB onboarding created and tested its database, generated a second-device Setup URI, and completed a bidirectional note round-trip. Screenshots: ${screenshots.join(", ")}`
|
||||
);
|
||||
} finally {
|
||||
await stopTrackedSessions(context).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
});
|
||||
await vaultA.dispose();
|
||||
await vaultB.dispose();
|
||||
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
|
||||
await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -196,14 +196,14 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
||||
`setup-remote-selection-dialogue${mode === "mobile" ? "-mobile" : ""}.png`,
|
||||
async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Enter Server Information" }),
|
||||
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/MinIO/R2 Object Storage", "Peer-to-Peer only"]) {
|
||||
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
|
||||
@@ -222,7 +222,7 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
||||
);
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Enter Server Information" }),
|
||||
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
||||
});
|
||||
if (mode === "mobile") {
|
||||
const previousDialogue = await modal.locator(".modal").last().elementHandle();
|
||||
@@ -243,7 +243,7 @@ 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: "Enter Server Information" }),
|
||||
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
||||
});
|
||||
await remoteSelection
|
||||
.locator("label")
|
||||
|
||||
@@ -20,6 +20,10 @@ const testSteps: Step[] = [
|
||||
{ name: "P2P status pane", args: ["run", "test:e2e:obsidian:p2p-pane"] },
|
||||
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
|
||||
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
|
||||
{
|
||||
name: "manual CouchDB setup workflow",
|
||||
args: ["run", "test:e2e:obsidian:couchdb-manual-setup-workflow"],
|
||||
},
|
||||
{
|
||||
name: "CLI to real Obsidian synchronisation",
|
||||
args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"],
|
||||
|
||||
@@ -13,6 +13,7 @@ const focusedScenarios = new Set([
|
||||
"p2p-pane",
|
||||
"vault-reflection",
|
||||
"couchdb-upload",
|
||||
"couchdb-manual-setup-workflow",
|
||||
"cli-to-obsidian-sync",
|
||||
"minio-upload",
|
||||
"object-storage-setup-uri-workflow",
|
||||
|
||||
Reference in New Issue
Block a user