mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-25 02:23:22 +00:00
Add Joined devices dialogue and version notices
This commit is contained in:
1 parent
ce0524d49f
commit
404d4279c0
23 files changed
+1589
-6
No files matched your search
@@ -125,6 +125,8 @@ The underlying `test:e2e:obsidian:<scenario>` scripts remain available for an im
|
||||
|
||||
`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database.
|
||||
|
||||
`test:e2e:obsidian:joined-devices` uses a temporary Vault and isolated CouchDB database to open Joined devices from Remote Configuration, a version Notice, and its registered command. It checks that the current device appears first with current metadata, reviews locked-out and other-device records, confirms a manual refresh reads an updated remote record, and forgets one device through the inline confirmation. It verifies the raw milestone retains the other device, remote state, and preferred-ID tweaks, then checks mobile scrolling, safe-area bounds, and touch targets. It captures the version Notice, desktop list, forget confirmation, completed removal, and mobile dialogue; unrelated Notices are dismissed before the list captures.
|
||||
|
||||
`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then enables Advanced mode and persists one numeric Advanced setting. On Obsidian before 1.13, this covers the imperative settings fallback used by the `SettingSpec` proof. On Obsidian 1.13 or later, it verifies the task-oriented landing order, the separate Synchronisation group containing Remote Configuration and Sync Settings, and the General Settings group containing Appearance, Logging, and Extra menus. It reopens and cancels onboarding from Quick Setup, opens all 14 nested settings pages, searches globally for the Advanced control, captures the landing page, search result, Advanced page, and mobile landing page, and verifies that the value remains after the settings dialogue is closed and reopened. The mobile landing check confirms that Remote Configuration is reachable without initial scrolling. Finally, it selects the Synchronisation Settings page and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour.
|
||||
|
||||
The default runner uses the maintained pre-1.13 Obsidian fixture and therefore exercises the complete compatibility-review, mobile-layout, and imperative-settings path. To exercise only the native settings contract against an additional Obsidian 1.13-or-later installation, supply its executable and companion CLI explicitly:
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
import { DEVICE_ID_PREFERRED, MILESTONE_DOCID, NODEINFO_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import {
|
||||
assertCouchDbReachable,
|
||||
createCouchDbDatabase,
|
||||
deleteCouchDbDatabase,
|
||||
fetchCouchDbDocument,
|
||||
loadCouchDbConfig,
|
||||
makeUniqueDatabaseName,
|
||||
putCouchDbDocument,
|
||||
type CouchDbDocument,
|
||||
} from "../runner/couchdb.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import {
|
||||
assertEqual,
|
||||
assertE2eCompatibilityUnpaused,
|
||||
configureCouchDb,
|
||||
createE2eCouchDbPluginData,
|
||||
prepareRemote,
|
||||
waitForLiveSyncCoreReady,
|
||||
} from "../runner/liveSyncWorkflow.ts";
|
||||
import {
|
||||
assertLocatorHasMinimumTouchTarget,
|
||||
assertLocatorWithinSafeArea,
|
||||
assertLocatorWithinViewport,
|
||||
assertNoHorizontalOverflow,
|
||||
} from "@vrtmrz/obsidian-test-session";
|
||||
import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import {
|
||||
captureObsidianDialogue,
|
||||
openLiveSyncSettings,
|
||||
waitForVisibleObsidianDialogue,
|
||||
withObsidianPage,
|
||||
} from "../runner/ui.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
import type { Locator, Page } from "playwright";
|
||||
|
||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_JOINED_DEVICES_TIMEOUT_MS ?? 10000);
|
||||
const commandId = "obsidian-livesync:joined-devices";
|
||||
const targetId = "joined-devices-target";
|
||||
const retainedId = "joined-devices-retained";
|
||||
const initialPlatform = "E2E Android tablet";
|
||||
const refreshedPlatform = "E2E refreshed platform";
|
||||
const connectedAt = Date.UTC(2026, 0, 12, 9, 30, 0);
|
||||
|
||||
type SelfDeviceInfo = {
|
||||
id: string;
|
||||
platform: string;
|
||||
pluginVersion: string;
|
||||
appVersion: string;
|
||||
};
|
||||
|
||||
type DeviceInfo = {
|
||||
device_name: string;
|
||||
vault_name: string;
|
||||
app_version: string;
|
||||
plugin_version: string;
|
||||
platform: string;
|
||||
progress: string;
|
||||
last_connected: number;
|
||||
};
|
||||
|
||||
type JoinedDevicesMilestone = CouchDbDocument & {
|
||||
accepted_nodes: string[];
|
||||
node_info: Record<string, DeviceInfo>;
|
||||
locked: boolean;
|
||||
cleaned: boolean;
|
||||
node_chunk_info: Record<string, { min: number; max: number; current: number }>;
|
||||
tweak_values: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
|
||||
type DeviceFixture = {
|
||||
milestone: JoinedDevicesMilestone;
|
||||
preferredTweaks: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function assert(condition: boolean, message: string): void {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function deviceCard(dialogue: Locator, id: string): Locator {
|
||||
return dialogue.locator(".device-card").filter({ hasText: id });
|
||||
}
|
||||
|
||||
async function assertDetail(card: Locator, label: string, expected: string): Promise<void> {
|
||||
const rows = card.locator(".device-details > div");
|
||||
for (let index = 0; index < (await rows.count()); index++) {
|
||||
const row = rows.nth(index);
|
||||
if ((await row.locator("dt").innerText()).trim() === label) {
|
||||
assertEqual((await row.locator("dd").innerText()).trim(), expected, `${label} did not match.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(`The device details did not contain '${label}'.`);
|
||||
}
|
||||
|
||||
async function readSelfDeviceInfo(cliBinary: string, env: NodeJS.ProcessEnv): Promise<SelfDeviceInfo> {
|
||||
return await evalObsidianJson<SelfDeviceInfo>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
`const nodeInfo=await core.services.database.localDatabase.localDatabase.get(${JSON.stringify(NODEINFO_DOCID)});`,
|
||||
"return JSON.stringify({",
|
||||
"id:nodeInfo.nodeid,",
|
||||
"platform:core.services.API.getPlatform(),",
|
||||
"pluginVersion:core.services.API.getPluginVersion(),",
|
||||
"appVersion:core.services.API.getAppVersion(),",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
function createFixture(self: SelfDeviceInfo): DeviceFixture {
|
||||
const preferredTweaks = { customChunkSize: 60, chunkSplitterVersion: "v3-rabin-karp" };
|
||||
const milestone: JoinedDevicesMilestone = {
|
||||
_id: MILESTONE_DOCID,
|
||||
type: "milestoneinfo",
|
||||
created: Date.now(),
|
||||
// The current device is deliberately absent: it must still be listed first and identified as locked out.
|
||||
accepted_nodes: [targetId, retainedId],
|
||||
node_info: {
|
||||
[targetId]: {
|
||||
device_name: "E2E target device",
|
||||
vault_name: "Joined devices target fixture",
|
||||
app_version: "1.99.0-test",
|
||||
plugin_version: "999.0.0",
|
||||
platform: initialPlatform,
|
||||
progress: "ready",
|
||||
last_connected: connectedAt,
|
||||
},
|
||||
[retainedId]: {
|
||||
device_name: "E2E retained device",
|
||||
vault_name: "Joined devices retained fixture",
|
||||
app_version: "1.88.0-test",
|
||||
plugin_version: "0.1.0",
|
||||
platform: "E2E Linux desktop",
|
||||
progress: "ready",
|
||||
last_connected: connectedAt - 86400000,
|
||||
},
|
||||
},
|
||||
locked: true,
|
||||
cleaned: true,
|
||||
node_chunk_info: {
|
||||
[targetId]: { min: 1, max: 3, current: 3 },
|
||||
[retainedId]: { min: 1, max: 2, current: 2 },
|
||||
},
|
||||
tweak_values: {
|
||||
[targetId]: { customChunkSize: 48 },
|
||||
[retainedId]: { customChunkSize: 60 },
|
||||
[DEVICE_ID_PREFERRED]: preferredTweaks,
|
||||
},
|
||||
};
|
||||
assert(self.id !== targetId && self.id !== retainedId, "The local device identity collided with a fixture ID.");
|
||||
return { milestone, preferredTweaks };
|
||||
}
|
||||
|
||||
async function openFromRemoteSettings(page: Page): Promise<Locator> {
|
||||
const settings = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||
const remoteConfiguration = await settings.openPage("Remote Configuration");
|
||||
await remoteConfiguration.getByRole("button", { name: "Open", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await dialogue.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
return dialogue;
|
||||
}
|
||||
|
||||
async function assertInitialDialogue(dialogue: Locator, self: SelfDeviceInfo, page: Page): Promise<void> {
|
||||
const cards = dialogue.locator(".device-card");
|
||||
assertEqual(
|
||||
await cards.count(),
|
||||
3,
|
||||
"The joined devices dialogue did not show the self, target, and retained records."
|
||||
);
|
||||
const selfCard = cards.nth(0);
|
||||
assert(Boolean(self.id), "The current device identity was not read from the local NODEINFO_DOCID record.");
|
||||
assertEqual(
|
||||
(await selfCard.locator("code.device-id").innerText()).trim(),
|
||||
self.id,
|
||||
"The current device was not listed first."
|
||||
);
|
||||
await selfCard.getByText("This device", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await selfCard.getByText("Locked out", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await selfCard.getByText("Not accepted", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await selfCard
|
||||
.getByText("This device is locked out.", { exact: false })
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await assertDetail(selfCard, "Platform", self.platform);
|
||||
await assertDetail(selfCard, "Plug-in version", self.pluginVersion);
|
||||
await assertDetail(selfCard, "App version", self.appVersion);
|
||||
assertEqual(
|
||||
await selfCard.getByRole("button", { name: "Forget device", exact: true }).count(),
|
||||
0,
|
||||
"The current device exposed a forget control."
|
||||
);
|
||||
|
||||
const targetCard = deviceCard(dialogue, targetId);
|
||||
await assertDetail(targetCard, "Platform", initialPlatform);
|
||||
await assertDetail(targetCard, "Plug-in version", "999.0.0");
|
||||
await assertDetail(targetCard, "App version", "1.99.0-test");
|
||||
const targetDate = await page.evaluate((timestamp) => new Date(timestamp).toLocaleString(), connectedAt);
|
||||
await assertDetail(targetCard, "Last recorded", targetDate);
|
||||
await targetCard.getByText("Accepted", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
|
||||
const retainedCard = deviceCard(dialogue, retainedId);
|
||||
await assertDetail(retainedCard, "Platform", "E2E Linux desktop");
|
||||
await assertDetail(retainedCard, "Plug-in version", "0.1.0");
|
||||
await assertDetail(retainedCard, "App version", "1.88.0-test");
|
||||
await dialogue
|
||||
.getByText(
|
||||
"This list shows device information recorded by the remote. It does not show whether a device is online now.",
|
||||
{ exact: true }
|
||||
)
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await dialogue
|
||||
.getByText("This remote is locked.", { exact: false })
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await dialogue
|
||||
.getByText("This remote has been cleaned.", { exact: false })
|
||||
.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, {
|
||||
...milestone,
|
||||
node_info: {
|
||||
...milestone.node_info,
|
||||
[targetId]: { ...milestone.node_info[targetId], platform: refreshedPlatform },
|
||||
},
|
||||
});
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await dialogue.getByRole("button", { name: "Refresh", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await deviceCard(dialogue, targetId).getByText(refreshedPlatform, { exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
async function showForgetConfirmation(dialogue: Locator): Promise<void> {
|
||||
const targetCard = deviceCard(dialogue, targetId);
|
||||
await targetCard.getByRole("button", { name: "Forget device", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await targetCard.getByText("Forget this device record?", { exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
async function confirmForgetTarget(dialogue: Locator): Promise<void> {
|
||||
const targetCard = deviceCard(dialogue, targetId);
|
||||
await targetCard.getByRole("button", { name: "Forget device", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await dialogue.getByText("The device record was forgotten.", { exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
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;
|
||||
assertEqual(
|
||||
JSON.stringify(milestone.accepted_nodes),
|
||||
JSON.stringify([retainedId]),
|
||||
"Forgetting changed the wrong accepted device records."
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(Object.keys(milestone.node_info)),
|
||||
JSON.stringify([retainedId]),
|
||||
"Forgetting changed the wrong device metadata records."
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(Object.keys(milestone.node_chunk_info)),
|
||||
JSON.stringify([retainedId]),
|
||||
"Forgetting changed the wrong chunk metadata records."
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(Object.keys(milestone.tweak_values).sort()),
|
||||
JSON.stringify([DEVICE_ID_PREFERRED, retainedId].sort()),
|
||||
"Forgetting changed the wrong tweak metadata records."
|
||||
);
|
||||
assertEqual(milestone.locked, true, "Forgetting changed the remote lock state.");
|
||||
assertEqual(milestone.cleaned, true, "Forgetting changed the remote cleaned state.");
|
||||
assertEqual(
|
||||
JSON.stringify(milestone.tweak_values[DEVICE_ID_PREFERRED]),
|
||||
JSON.stringify(fixture.preferredTweaks),
|
||||
"Forgetting changed the preferred-ID tweak record."
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(milestone.node_info[retainedId]),
|
||||
JSON.stringify(fixture.milestone.node_info[retainedId]),
|
||||
"Forgetting changed the retained device metadata."
|
||||
);
|
||||
}
|
||||
|
||||
async function assertScrollableMobileDialogue(page: Page, container: Locator): Promise<void> {
|
||||
const dialogue = container.locator(".modal").last();
|
||||
const closeButton = container.locator(".modal-close-button, .modal-header-button").last();
|
||||
await assertLocatorWithinViewport(page, dialogue, { label: "Joined devices dialogue" });
|
||||
await assertNoHorizontalOverflow(page, dialogue, { label: "Joined devices dialogue" });
|
||||
await assertLocatorWithinSafeArea(page, dialogue, {
|
||||
label: "Joined devices dialogue",
|
||||
safeAreaInsets: iPhoneSafeArea,
|
||||
});
|
||||
const buttons = [closeButton, ...(await dialogue.locator("button:visible").all())];
|
||||
for (const button of buttons) {
|
||||
// Device records form a scrollable list; every control must be reachable after scrolling.
|
||||
await button.scrollIntoViewIfNeeded();
|
||||
const label = (await button.innerText()).trim() || "Close dialogue";
|
||||
await assertLocatorWithinViewport(page, button, { label });
|
||||
await assertLocatorWithinSafeArea(page, button, { label, safeAreaInsets: iPhoneSafeArea });
|
||||
await assertNoHorizontalOverflow(page, button, { label });
|
||||
await assertLocatorHasMinimumTouchTarget(page, button, { label });
|
||||
}
|
||||
await container.locator(".modal-content").evaluate((element) => element.scrollTo(0, 0));
|
||||
}
|
||||
|
||||
async function dismissNotices(page: Page): Promise<void> {
|
||||
const notices = page.locator(".notice:visible");
|
||||
for (let remaining = 20; remaining > 0 && (await notices.count()) > 0; remaining--) {
|
||||
const notice = await notices.first().elementHandle();
|
||||
if (!notice) continue;
|
||||
try {
|
||||
await notice.click({ position: { x: 20, y: 20 }, timeout: uiTimeoutMs });
|
||||
await notice.waitForElementState("hidden", { timeout: uiTimeoutMs });
|
||||
} finally {
|
||||
await notice.dispose();
|
||||
}
|
||||
}
|
||||
assertEqual(await notices.count(), 0, "Notices still obscured the dialogue screenshot.");
|
||||
}
|
||||
|
||||
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, "joined-devices");
|
||||
const vault = await createTemporaryVault();
|
||||
let session: ObsidianLiveSyncSession | undefined;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
await createCouchDbDatabase(couchDb, dbName);
|
||||
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,
|
||||
}),
|
||||
});
|
||||
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 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 });
|
||||
|
||||
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => {
|
||||
await page.setViewportSize({ width: 1280, height: 1100 });
|
||||
const dialogue = await openFromRemoteSettings(page);
|
||||
await assertInitialDialogue(dialogue, self, page);
|
||||
await page
|
||||
.locator(".notice")
|
||||
.getByText("Other joined devices last reported a newer plug-in version: 999.0.0.", { exact: true })
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
});
|
||||
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-desktop.png", async (page) => {
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await dialogue.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await page.locator(".notice").getByRole("button", { name: "Joined devices", exact: true }).first().click();
|
||||
const reopened = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await reopened.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await dismissNotices(page);
|
||||
});
|
||||
await captureObsidianDialogue(
|
||||
session.remoteDebuggingPort,
|
||||
"joined-devices-forget-confirmation.png",
|
||||
async (page) => {
|
||||
await refreshRemotePlatform(page, couchDb, dbName);
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await showForgetConfirmation(dialogue);
|
||||
await deviceCard(dialogue, targetId).locator(".forget-confirmation").scrollIntoViewIfNeeded();
|
||||
}
|
||||
);
|
||||
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-after-forget.png", async (page) => {
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await confirmForgetTarget(dialogue);
|
||||
await dialogue.locator(".modal-content").evaluate((element) => element.scrollTo(0, 0));
|
||||
});
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await dialogue.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
const settings = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||
await settings.close();
|
||||
});
|
||||
await assertTargetForgotten(couchDb, dbName, fixture);
|
||||
|
||||
await setObsidianMobileTestMode(session.remoteDebuggingPort, true, uiTimeoutMs);
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-mobile.png", async (page) => {
|
||||
const opened = await page.evaluate((id) => {
|
||||
const app = (
|
||||
window as typeof window & {
|
||||
app?: { commands?: { executeCommandById?(commandId: string): boolean } };
|
||||
}
|
||||
).app;
|
||||
return app?.commands?.executeCommandById?.(id) === true;
|
||||
}, commandId);
|
||||
assert(opened, "The Joined devices command did not open the dialogue.");
|
||||
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
|
||||
await dialogue.getByText(retainedId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await assertScrollableMobileDialogue(page, dialogue);
|
||||
assertEqual(
|
||||
await dialogue.locator(".device-card").count(),
|
||||
2,
|
||||
"The refreshed mobile list did not retain both remaining devices."
|
||||
);
|
||||
await dismissNotices(page);
|
||||
});
|
||||
|
||||
console.log(`Joined devices scenario passed against CouchDB database ${dbName}.`);
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ const focusedScenarios = new Set([
|
||||
"smoke",
|
||||
"onboarding-invitation",
|
||||
"dialog-mounts",
|
||||
"joined-devices",
|
||||
"database-diagnostics",
|
||||
"database-diagnostics-couchdb",
|
||||
"conflict-dialog-policy",
|
||||
|
||||
Reference in new issue
Block a user