test: cover Commonlib 0.2 consumer workflows

This commit is contained in:
vorotamoroz committed 2026-10-23 17:37:42 +00:00
1 parent b40784e66f
commit 1a54d21e71
8 files changed
+276 -81

No files matched your search

+65 -20
View File
@@ -15,6 +15,7 @@ import {
captureObsidianPage,
obsidianRemoteDebuggingPort,
openLiveSyncSettings,
waitForVisibleObsidianDialogue,
withObsidianPage as withObsidianSessionPage,
} from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts";
@@ -43,6 +44,8 @@ type DialogueMode = "desktop" | "mobile";
type DialogueRunState = {
done: boolean;
deviceNameRead?: boolean;
deviceNameReadPending?: boolean;
releaseDeviceNameRead?: () => void;
error?: string;
expected?: unknown;
kind: string;
@@ -140,11 +143,20 @@ async function openP2PRemoteSelectionDialogueForInspection(): Promise<void> {
kind: "p2p-compatibility-settings",
done: false,
deviceNameRead: false,
deviceNameReadPending: false,
};
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
const deviceNameReadGate = new Promise<void>((resolve) => {
state.releaseDeviceNameRead = resolve;
});
config.getValue = async (key, defaultValue) => {
const value = await originalGetValue.call(config, key, defaultValue);
if (key === deviceNameKey && value === deviceName) state.deviceNameRead = true;
if (key === deviceNameKey) {
state.deviceNameReadPending = true;
await deviceNameReadGate;
state.deviceNameReadPending = false;
if (value === deviceName) state.deviceNameRead = true;
}
return value;
};
manager.onConfirmApplySettingsFromWizard = async (settings: unknown) => {
@@ -156,16 +168,17 @@ async function openP2PRemoteSelectionDialogueForInspection(): Promise<void> {
.then(
(result) => {
state.result = result;
state.done = true;
},
(error: unknown) => {
state.error = error instanceof Error ? error.message : String(error);
state.done = true;
}
)
.finally(() => {
state.releaseDeviceNameRead?.();
state.releaseDeviceNameRead = undefined;
manager.onConfirmApplySettingsFromWizard = originalConfirm;
config.getValue = originalGetValue;
state.done = true;
});
},
{
@@ -177,6 +190,15 @@ async function openP2PRemoteSelectionDialogueForInspection(): Promise<void> {
});
}
async function releasePendingP2PDeviceNameRead(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate((stateKey) => {
const state = (globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey];
state?.releaseDeviceNameRead?.();
}, dialogRunStateKey);
});
}
async function openSetupUriDialogue(): Promise<void> {
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
return await page.evaluate(
@@ -523,9 +545,11 @@ async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise<string
async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
await openP2PRemoteSelectionDialogueForInspection();
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const remoteSelection = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
});
const remoteSelection = await waitForVisibleObsidianDialogue(
page,
"Choose a synchronisation remote",
uiTimeoutMs
);
await remoteSelection
.locator("label")
.filter({ hasText: "Peer-to-Peer (P2P)" })
@@ -541,13 +565,36 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
obsidianRemoteDebuggingPort(),
"setup-p2p-compatibility-dialogue.png",
async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "P2P Configuration" }),
});
const modal = await waitForVisibleObsidianDialogue(page, "P2P Configuration", uiTimeoutMs);
const modalPage = modal.page();
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
const peerName = modal.locator('input[name="p2p-device-peer-id"]');
const peerName = modal.getByLabel("Device Peer ID", { exact: true });
await peerName.waitFor({ state: "visible", timeout: uiTimeoutMs });
await page.waitForFunction(
await withObsidianPage(obsidianRemoteDebuggingPort(), async (statePage) => {
await statePage.waitForFunction(
(stateKey) =>
(globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey]
?.deviceNameReadPending === true,
dialogRunStateKey,
{ timeout: uiTimeoutMs }
);
});
const testAndContinue = modal.getByRole("button", { name: "Test Settings and Continue", exact: true });
const continueAnyway = modal.getByRole("button", { name: "Continue anyway", exact: true });
await testAndContinue.waitFor({ state: "visible", timeout: uiTimeoutMs });
await continueAnyway.waitFor({ state: "visible", timeout: uiTimeoutMs });
if (
!(await peerName.isDisabled()) ||
!(await testAndContinue.isDisabled()) ||
!(await continueAnyway.isDisabled())
) {
throw new Error(
"The P2P device name and setup actions must remain disabled while the saved device name is loading."
);
}
await releasePendingP2PDeviceNameRead();
await modalPage.waitForFunction(
(expectedName) => {
const input = document.querySelector('input[name="p2p-device-peer-id"]');
return input instanceof HTMLInputElement && !input.disabled && input.value === expectedName;
@@ -562,7 +609,7 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
await modal.locator('input[name="p2p-password"]').fill("e2e-passphrase");
const compatibility = modal.locator("details").filter({
has: page.locator("summary").filter({ hasText: "Connection compatibility" }),
has: modalPage.locator("summary").filter({ hasText: "Connection compatibility" }),
});
await compatibility.waitFor({ state: "visible", timeout: uiTimeoutMs });
if (!(await compatibility.evaluate((element) => (element as HTMLDetailsElement).open))) {
@@ -603,7 +650,7 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
}
const advanced = modal.locator("details").filter({
has: page.locator("summary").filter({ hasText: "Advanced Settings" }),
has: modalPage.locator("summary").filter({ hasText: "Advanced Settings" }),
});
if (!(await advanced.evaluate((element) => (element as HTMLDetailsElement).open))) {
await advanced.locator("summary").click({ timeout: uiTimeoutMs });
@@ -615,13 +662,13 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
if (relayOnlyElement === null || connectionPathElement === null) {
throw new Error("P2P connection-path controls were removed whilst validating TURN settings.");
}
await page.waitForFunction((element) => !(element as HTMLOptionElement).disabled, relayOnlyElement, {
await modalPage.waitForFunction((element) => !(element as HTMLOptionElement).disabled, relayOnlyElement, {
timeout: uiTimeoutMs,
});
await connectionPath.selectOption("relay");
await turnServers.fill("");
await page.waitForFunction(
await modalPage.waitForFunction(
(element) => (element as HTMLSelectElement).value === "automatic",
connectionPathElement,
{ timeout: uiTimeoutMs }
@@ -636,7 +683,7 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
.waitFor({ state: "visible", timeout: uiTimeoutMs });
await turnServers.fill("turn:turn.example.com:3478");
await page.waitForFunction((element) => !(element as HTMLOptionElement).disabled, relayOnlyElement, {
await modalPage.waitForFunction((element) => !(element as HTMLOptionElement).disabled, relayOnlyElement, {
timeout: uiTimeoutMs,
});
await connectionPath.selectOption("relay");
@@ -644,12 +691,10 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
await advanced.locator("summary").click({ timeout: uiTimeoutMs });
await compatibility.scrollIntoViewIfNeeded();
}
);
).finally(releasePendingP2PDeviceNameRead);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const modal = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({ hasText: "P2P Configuration" }),
});
const modal = await waitForVisibleObsidianDialogue(page, "P2P Configuration", uiTimeoutMs);
await modal.getByRole("button", { name: "Continue anyway", exact: true }).click({ timeout: uiTimeoutMs });
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
@@ -69,6 +69,7 @@ type RunnerContext = {
cliBinary: string;
couchDb: CouchDbConfig;
dbName: string;
activeSessions: Set<ObsidianLiveSyncSession>;
};
async function writeVaultFile(vaultPath: string, path: string, content: string): Promise<void> {
@@ -348,6 +349,7 @@ async function startConfiguredSession(
pluginData: createE2eCouchDbPluginData(couchDbSettings, hiddenFileSettings),
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
});
context.activeSessions.add(session);
await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv);
await assertE2eCompatibilityMarker(context.cliBinary, session.cliEnv);
await configureCouchDb(context.cliBinary, session.cliEnv, couchDbSettings, hiddenFileSettings);
@@ -355,6 +357,11 @@ async function startConfiguredSession(
return session;
}
async function stopSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise<void> {
await session.app.stop();
context.activeSessions.delete(session);
}
async function uploadHiddenFile(
context: RunnerContext,
session: ObsidianLiveSyncSession,
@@ -430,12 +437,12 @@ async function runCreateRoundTrip(
throw new Error("Remote Rebuild did not declare and encrypt internal Metadata for its accepted writer.");
}
console.log("Remote Rebuild declared the feature and preserved encrypted Hidden File Sync Metadata.");
await session.app.stop();
await stopSession(context, session);
session = await startConfiguredSession(context, vaultB);
await pullAndApplyHiddenFiles(context, session);
const received = await waitForPathContent(vaultB.path, snippetPath, (content) => content === snippetContent);
await session.app.stop();
await stopSession(context, session);
assertEqual(received, snippetContent, "Hidden snippet content did not round-trip to the second vault.");
console.log(`Hidden create round-trip copied ${entry.id} to the second vault.`);
@@ -450,12 +457,12 @@ async function runDeleteRoundTrip(
let session = await startConfiguredSession(context, vaultA);
await scanHiddenStorage(context.cliBinary, session.cliEnv);
await pushLocalChanges(context.cliBinary, session.cliEnv);
await session.app.stop();
await stopSession(context, session);
session = await startConfiguredSession(context, vaultB);
await pullAndApplyHiddenFiles(context, session);
await waitForPathDeleted(vaultB.path, snippetPath);
await session.app.stop();
await stopSession(context, session);
console.log("Hidden delete round-trip removed the snippet from the second vault.");
}
@@ -476,14 +483,14 @@ async function runJsonConflictRoundTrip(
const mergedOnB = await waitForPathContent(vaultB.path, mergeJsonPath, (content) =>
hasJsonValues(content, { fromA: true, fromB: true })
);
await session.app.stop();
await stopSession(context, session);
session = await startConfiguredSession(context, vaultA);
await pullAndApplyHiddenFiles(context, session);
const mergedOnA = await waitForPathContent(vaultA.path, mergeJsonPath, (content) =>
hasJsonValues(content, { fromA: true, fromB: true })
);
await session.app.stop();
await stopSession(context, session);
assertEqual(mergedOnA, mergedOnB, "Merged hidden JSON content was not consistent across both vaults.");
console.log("Hidden JSON conflict was automatically merged and round-tripped.");
@@ -503,7 +510,7 @@ async function runJsonManualConflictResolution(context: RunnerContext, vault: Te
const merged = await waitForPathContent(vault.path, manualMergeJsonPath, (content) =>
hasJsonValues(content, { shared: "right", fromA: true, fromB: true })
);
await session.app.stop();
await stopSession(context, session);
const parsed = JSON.parse(merged);
assertEqual(parsed.shared, "right", "Manual JSON conflict resolution did not apply the selected merged result.");
@@ -517,17 +524,19 @@ async function runTargetMismatch(
vaultA: TemporaryVault,
vaultB: TemporaryVault
): Promise<void> {
const selectors = { syncOnlyRegEx: String.raw`\.(md|pdf)$` };
const targetContent = JSON.stringify({ onlyA: true, targetMismatch: true }, null, 4) + "\n";
await writeVaultFile(vaultA.path, targetPath, targetContent);
let session = await startConfiguredSession(context, vaultA);
let session = await startConfiguredSession(context, vaultA, selectors);
try {
await uploadHiddenFile(context, session, targetPath);
} finally {
await session.app.stop();
await stopSession(context, session);
}
session = await startConfiguredSession(context, vaultB, {
...selectors,
syncInternalFilesTargetPatterns: "snippets",
});
try {
@@ -538,10 +547,11 @@ async function runTargetMismatch(
"Hidden file was applied on a device where it was not a target file."
);
} finally {
await session.app.stop();
await stopSession(context, session);
}
session = await startConfiguredSession(context, vaultB, {
...selectors,
syncInternalFilesTargetPatterns: "",
});
let received = "";
@@ -549,11 +559,17 @@ async function runTargetMismatch(
await pullAndApplyHiddenFiles(context, session, { resolveConflicts: false });
received = await waitForPathContent(vaultB.path, targetPath, (content) => content === targetContent);
} finally {
await session.app.stop();
await stopSession(context, session);
}
assertEqual(received, targetContent, "Hidden file was not applied after it became a target file.");
console.log("Hidden target mismatch respected per-device target patterns, then applied after enabling the target.");
assertEqual(
received,
targetContent,
"The normal file allow-list blocked a selected hidden file from the other device."
);
console.log(
"A normal Markdown/PDF allow-list preserved hidden target exclusion and exact remote restoration after enabling the target."
);
}
async function setHiddenFileNoticeFixtures(port: number, itemIds: string[], includeRestart: boolean): Promise<void> {
@@ -828,7 +844,7 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
`Hidden File Sync showed one progress Notice before settings were saved and retained it throughout initialisation. Screenshot: ${screenshotPath}`
);
} finally {
await session.app.stop();
await stopSession(context, session);
}
}
@@ -889,7 +905,7 @@ async function runConfigurationNoticeGrouping(context: RunnerContext, vault: Tem
} finally {
await clearHiddenFileNoticeFixtures(port).catch(() => undefined);
await setObsidianMobileTestMode(port, false, timeoutMs).catch(() => undefined);
await session.app.stop();
await stopSession(context, session);
}
}
@@ -904,7 +920,7 @@ async function main(): Promise<void> {
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "hidden-roundtrip");
const vaultA = await createTemporaryVault();
const vaultB = await createTemporaryVault();
const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName };
const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName, activeSessions: new Set() };
try {
await assertCouchDbReachable(couchDb);
@@ -923,6 +939,7 @@ async function main(): Promise<void> {
await runInitialisationNoticeGrouping(context, vaultB);
await runConfigurationNoticeGrouping(context, vaultB);
} finally {
for (const session of context.activeSessions) await stopSession(context, session);
await vaultA.dispose();
await vaultB.dispose();
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
+113 -41
View File
@@ -1,3 +1,4 @@
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { DEVICE_ID_PREFERRED, MILESTONE_DOCID, NODEINFO_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types";
@@ -12,12 +13,22 @@ import {
putCouchDbDocument,
type CouchDbDocument,
} from "../runner/couchdb.ts";
import {
createObjectStorageClient,
deleteObjectStoragePrefix,
ensureObjectStorageBucket,
loadObjectStorageConfig,
makeUniqueBucketPrefix,
readObjectStorageJson,
} from "../runner/objectStorage.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import {
assertEqual,
assertE2eCompatibilityUnpaused,
configureCouchDb,
configureObjectStorage,
createE2eCouchDbPluginData,
createE2eObjectStoragePluginData,
prepareRemote,
waitForLiveSyncCoreReady,
} from "../runner/liveSyncWorkflow.ts";
@@ -72,6 +83,73 @@ type DeviceFixture = {
preferredTweaks: Record<string, unknown>;
};
type JoinedDevicesRemote = {
label: string;
pluginData: Record<string, unknown>;
prepare(): Promise<void>;
configure(cliBinary: string, env: NodeJS.ProcessEnv): Promise<unknown>;
read(): Promise<JoinedDevicesMilestone>;
write(milestone: JoinedDevicesMilestone): Promise<void>;
cleanup(): Promise<void>;
};
async function createRemoteFixture(): Promise<JoinedDevicesRemote> {
const args = process.argv.slice(2);
if (args.some((argument) => argument !== "--object-storage")) {
throw new Error("Usage: joined-devices.ts [--object-storage]");
}
if (args.includes("--object-storage")) {
const config = await loadObjectStorageConfig();
const bucketPrefix = makeUniqueBucketPrefix("joined-devices");
const settings = { ...config, bucketPrefix };
const key = `${bucketPrefix}_00000000-milestone.json`;
return {
label: "Object Storage",
pluginData: createE2eObjectStoragePluginData(settings),
prepare: () => ensureObjectStorageBucket(config),
configure: (cliBinary, env) => configureObjectStorage(cliBinary, env, settings),
read: () => readObjectStorageJson<JoinedDevicesMilestone>(config, key),
write: async (milestone) => {
const client = createObjectStorageClient(config);
try {
await client.send(
new PutObjectCommand({
Bucket: config.bucket,
Key: key,
Body: JSON.stringify(milestone),
ContentType: "application/json",
})
);
} finally {
client.destroy();
}
},
cleanup: () => deleteObjectStoragePrefix(config, bucketPrefix),
};
}
const config = await loadCouchDbConfig();
const dbName = makeUniqueDatabaseName(config.dbPrefix, "joined-devices");
const settings = { uri: config.uri, username: config.username, password: config.password, dbName };
return {
label: "CouchDB",
pluginData: createE2eCouchDbPluginData(settings),
prepare: async () => {
await assertCouchDbReachable(config);
await createCouchDbDatabase(config, dbName);
},
configure: (cliBinary, env) => configureCouchDb(cliBinary, env, settings),
read: async () => {
const milestone = await fetchCouchDbDocument(config, dbName, MILESTONE_DOCID);
assert(Boolean(milestone._rev), "The remote milestone did not have a revision.");
return milestone as JoinedDevicesMilestone;
},
write: async (milestone) => {
await putCouchDbDocument(config, dbName, milestone);
},
cleanup: () => deleteCouchDbDatabase(config, dbName),
};
}
function assert(condition: boolean, message: string): void {
if (!condition) throw new Error(message);
}
@@ -228,13 +306,9 @@ async function assertInitialDialogue(dialogue: Locator, self: SelfDeviceInfo, pa
.waitFor({ state: "visible", timeout: uiTimeoutMs });
}
async function refreshRemotePlatform(
page: Page,
config: Awaited<ReturnType<typeof loadCouchDbConfig>>,
dbName: string
): Promise<void> {
const milestone = (await fetchCouchDbDocument(config, dbName, MILESTONE_DOCID)) as JoinedDevicesMilestone;
await putCouchDbDocument(config, dbName, {
async function refreshRemotePlatform(page: Page, remote: JoinedDevicesRemote): Promise<void> {
const milestone = await remote.read();
await remote.write({
...milestone,
node_info: {
...milestone.node_info,
@@ -258,6 +332,15 @@ async function showForgetConfirmation(dialogue: Locator): Promise<void> {
});
}
async function assertCancelledForget(dialogue: Locator, remote: JoinedDevicesRemote): Promise<void> {
const before = await remote.read();
await showForgetConfirmation(dialogue);
const confirmation = deviceCard(dialogue, targetId).locator(".forget-confirmation");
await confirmation.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs });
await confirmation.waitFor({ state: "detached", timeout: uiTimeoutMs });
assertEqual(JSON.stringify(await remote.read()), JSON.stringify(before), "Cancelling Forget changed the remote.");
}
async function confirmForgetTarget(dialogue: Locator): Promise<void> {
const targetCard = deviceCard(dialogue, targetId);
await targetCard.getByRole("button", { name: "Forget device", exact: true }).click({ timeout: uiTimeoutMs });
@@ -268,12 +351,8 @@ async function confirmForgetTarget(dialogue: Locator): Promise<void> {
await targetCard.waitFor({ state: "detached", timeout: uiTimeoutMs });
}
async function assertTargetForgotten(
config: Awaited<ReturnType<typeof loadCouchDbConfig>>,
dbName: string,
fixture: DeviceFixture
): Promise<void> {
const milestone = (await fetchCouchDbDocument(config, dbName, MILESTONE_DOCID)) as JoinedDevicesMilestone;
async function assertTargetForgotten(remote: JoinedDevicesRemote, fixture: DeviceFixture): Promise<void> {
const milestone = await remote.read();
assertEqual(
JSON.stringify(milestone.accepted_nodes),
JSON.stringify([retainedId]),
@@ -371,42 +450,32 @@ async function main(): Promise<void> {
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, "joined-devices");
const remote = await createRemoteFixture();
const vault = await createTemporaryVault();
let session: ObsidianLiveSyncSession | undefined;
try {
await assertCouchDbReachable(couchDb);
await createCouchDbDatabase(couchDb, dbName);
await remote.prepare();
session = await startObsidianLiveSyncSession({
binary,
cliBinary: cli.binary,
vault,
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
pluginData: createE2eCouchDbPluginData({
uri: couchDb.uri,
username: couchDb.username,
password: couchDb.password,
dbName,
}),
pluginData: remote.pluginData,
});
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await assertE2eCompatibilityUnpaused(cli.binary, session.cliEnv, session.remoteDebuggingPort);
await configureCouchDb(cli.binary, session.cliEnv, {
uri: couchDb.uri,
username: couchDb.username,
password: couchDb.password,
dbName,
});
await remote.configure(cli.binary, session.cliEnv);
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await prepareRemote(cli.binary, session.cliEnv);
const self = await readSelfDeviceInfo(cli.binary, session.cliEnv);
const fixture = createFixture(self);
const preparedMilestone = await fetchCouchDbDocument(couchDb, dbName, MILESTONE_DOCID);
assert(Boolean(preparedMilestone._rev), "Remote preparation did not create a revisioned milestone document.");
await putCouchDbDocument(couchDb, dbName, { ...fixture.milestone, _rev: preparedMilestone._rev });
const preparedMilestone = await remote.read();
await remote.write({
...fixture.milestone,
...(preparedMilestone._rev ? { _rev: preparedMilestone._rev } : {}),
});
await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => {
const dialogue = await openFromRemoteSettings(page);
@@ -440,8 +509,9 @@ async function main(): Promise<void> {
session.remoteDebuggingPort,
"joined-devices-forget-confirmation.png",
async (page) => {
await refreshRemotePlatform(page, couchDb, dbName);
await refreshRemotePlatform(page, remote);
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await assertCancelledForget(dialogue, remote);
await showForgetConfirmation(dialogue);
await deviceCard(dialogue, targetId).locator(".forget-confirmation").scrollIntoViewIfNeeded();
return dialogue;
@@ -459,7 +529,7 @@ async function main(): Promise<void> {
const settings = await openLiveSyncSettings(page, uiTimeoutMs);
await settings.close();
});
await assertTargetForgotten(couchDb, dbName, fixture);
await assertTargetForgotten(remote, fixture);
await setObsidianMobileTestMode(session.remoteDebuggingPort, true, uiTimeoutMs);
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
@@ -485,14 +555,16 @@ async function main(): Promise<void> {
return dialogue;
});
console.log(`Joined devices scenario passed against CouchDB database ${dbName}.`);
console.log(`Joined devices scenario passed against ${remote.label}.`);
} finally {
if (session) await session.app.stop();
await vault.dispose();
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => {
console.warn(error instanceof Error ? error.message : error);
});
try {
if (session) await session.app.stop();
} finally {
try {
await vault.dispose();
} finally {
await remote.cleanup();
}
}
}
}
+3
View File
@@ -26,6 +26,7 @@ const testSteps: Step[] = [
{ name: "Review Harness", args: ["run", "test:e2e:obsidian:review-harness"] },
{ name: "P2P status pane", args: ["run", "test:e2e:obsidian:p2p-pane"] },
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
{ name: "joined devices with CouchDB", args: ["run", "test:e2e:obsidian:joined-devices"] },
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
{
name: "manual CouchDB setup workflow",
@@ -35,6 +36,7 @@ const testSteps: Step[] = [
name: "CLI to real Obsidian synchronisation",
args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"],
},
{ name: "joined devices with Object Storage", args: ["run", "test:e2e:obsidian:joined-devices-object-storage"] },
{ name: "Object Storage upload", args: ["run", "test:e2e:obsidian:minio-upload"] },
{
name: "Object Storage Setup URI workflow",
@@ -56,6 +58,7 @@ const testSteps: Step[] = [
{ name: "startup scan", args: ["run", "test:e2e:obsidian:startup-scan"] },
{ name: "provisioned Setup URI workflow", args: ["run", "test:e2e:obsidian:setup-uri-workflow"] },
{ name: "two-vault synchronisation", args: ["run", "test:e2e:obsidian:two-vault-sync"] },
{ name: "hidden file selectors", args: ["run", "test:e2e:obsidian:hidden-file-selectors"] },
{ name: "hidden file snippet synchronisation", args: ["run", "test:e2e:obsidian:hidden-file-snippet-sync"] },
{ name: "Customisation Sync", args: ["run", "test:e2e:obsidian:customisation-sync"] },
{ name: "internal Metadata Doctor", args: ["run", "test:e2e:obsidian:internal-metadata-doctor"] },
+1
View File
@@ -9,6 +9,7 @@ const focusedScenarios = new Set([
"onboarding-invitation",
"dialog-mounts",
"joined-devices",
"joined-devices-object-storage",
"database-diagnostics",
"database-diagnostics-couchdb",
"conflict-dialog-policy",