Clarify device review guidance and follow dialogue windows

This commit is contained in:
vorotamoroz committed 2026-10-23 16:07:35 +00:00
1 parent 76808878da
commit 39ef429d7b
7 files changed
+81 -40

No files matched your search

+1 -1
View File
@@ -236,7 +236,7 @@ The active connection type. This is automatically projected to the legacy config
### Joined devices ### Joined devices
Open **Joined devices** to read the device records on the active CouchDB or Object Storage connection. The same dialogue is available from the command palette and version Notices. **Refresh** reads the remote again. This device appears first, including when its record is absent from the remote. Opening or refreshing this dialogue does not create a missing CouchDB database. Choose **Review devices** under **Joined devices** to read the device records on the active CouchDB or Object Storage connection. The same **Review devices** action is available from the command palette and version Notices. **Refresh** reads the remote again. This device appears first, including when its record is absent from the remote. Opening or refreshing this dialogue does not create a missing CouchDB database.
The list shows each device ID, recorded platform, plug-in version, app version, and last recorded connection time. Older clients may have incomplete information. These records do not establish whether a device is online. On a locked remote, a device outside the accepted list is shown as **Locked out**. The list shows each device ID, recorded platform, plug-in version, app version, and last recorded connection time. Older clients may have incomplete information. These records do not establish whether a device is online. On a locked remote, a device outside the accepted list is shown as **Locked out**.
@@ -26,8 +26,8 @@ export const liveSyncProvisionalEnglishMessages = {
"This remote does not currently have a record of joined devices.", "This remote does not currently have a record of joined devices.",
"This list shows device information recorded by the remote. It does not show whether a device is online now.": "This list shows device information recorded by the remote. It does not show whether a device is online now.":
"This list shows device information recorded by the remote. It does not show whether a device is online now.", "This list shows device information recorded by the remote. It does not show whether a device is online now.",
"This device is locked out. Use the existing recovery process to join again.": "This device has been locked out. Use the existing recovery process to join again.":
"This device is locked out. Use the existing recovery process to join again.", "This device has been locked out. Use the existing recovery process to join again.",
"This device": "This device", "This device": "This device",
"This remote has been cleaned. Use the existing recovery process to join devices again as needed.": "This remote has been cleaned. Use the existing recovery process to join devices again as needed.":
"This remote has been cleaned. Use the existing recovery process to join devices again as needed.", "This remote has been cleaned. Use the existing recovery process to join devices again as needed.",
@@ -42,10 +42,10 @@ export const liveSyncProvisionalEnglishMessages = {
Refresh: "Refresh", Refresh: "Refresh",
"Plug-in version": "Plug-in version", "Plug-in version": "Plug-in version",
Platform: "Platform", Platform: "Platform",
"Other joined devices last reported an older plug-in version: ${versions}.": "An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.":
"Other joined devices last reported an older plug-in version: ${versions}.", "An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.",
"Other joined devices last reported a newer plug-in version: ${versions}.": "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.":
"Other joined devices last reported a newer plug-in version: ${versions}.", "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.",
Open: "Open", Open: "Open",
"Not recorded": "Not recorded", "Not recorded": "Not recorded",
"Not accepted": "Not accepted", "Not accepted": "Not accepted",
@@ -58,6 +58,7 @@ export const liveSyncProvisionalEnglishMessages = {
"Joined devices are available after database startup with an active CouchDB or Object Storage connection.": "Joined devices are available after database startup with an active CouchDB or Object Storage connection.":
"Joined devices are available after database startup with an active CouchDB or Object Storage connection.", "Joined devices are available after database startup with an active CouchDB or Object Storage connection.",
"Joined devices": "Joined devices", "Joined devices": "Joined devices",
"Review devices": "Review devices",
"Forgetting…": "Forgetting…", "Forgetting…": "Forgetting…",
"Forgetting the record does not delete Vault files or revoke credentials.": "Forgetting the record does not delete Vault files or revoke credentials.":
"Forgetting the record does not delete Vault files or revoke credentials.", "Forgetting the record does not delete Vault files or revoke credentials.",
@@ -106,7 +106,7 @@ export function paneRemoteConfig(
.setName($msg("Joined devices")) .setName($msg("Joined devices"))
.setDesc($msg("Review device records for the active CouchDB or Object Storage connection.")) .setDesc($msg("Review device records for the active CouchDB or Object Storage connection."))
.addButton((button) => .addButton((button) =>
button.setButtonText($msg("Open")).onClick(async () => { button.setButtonText($msg("Review devices")).onClick(async () => {
const result = await this.services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined); const result = await this.services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined);
if (result.status === "unavailable" || result.value === "unavailable") { if (result.status === "unavailable" || result.value === "unavailable") {
this.services.API.addLog( this.services.API.addLog(
@@ -176,7 +176,7 @@
{#if device.self && device.lockedOut} {#if device.self && device.lockedOut}
<p class="state-message"> <p class="state-message">
{translate( {translate(
"This device is locked out. Use the existing recovery process to join again." "This device has been locked out. Use the existing recovery process to join again."
)} )}
</p> </p>
{/if} {/if}
+14 -8
View File
@@ -62,14 +62,20 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi
notices.setItem(noticeGroup, direction, { notices.setItem(noticeGroup, direction, {
message: message:
direction === "newer" direction === "newer"
? $msg("Other joined devices last reported a newer plug-in version: ${versions}.", { ? $msg(
versions: versions.join(", "), "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.",
}) {
: $msg("Other joined devices last reported an older plug-in version: ${versions}.", { versions: versions.join(", "),
versions: versions.join(", "), }
}), )
: $msg(
"An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.",
{
versions: versions.join(", "),
}
),
action: { action: {
label: $msg("Joined devices"), label: $msg("Review devices"),
onSelect: () => { onSelect: () => {
if (!disposed && identity === remoteIdentity(services.setting.currentSettings())) { if (!disposed && identity === remoteIdentity(services.setting.currentSettings())) {
void services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined); void services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined);
@@ -190,7 +196,7 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi
services.appLifecycle.onInitialise.addHandler(() => { services.appLifecycle.onInitialise.addHandler(() => {
services.API.addCommand({ services.API.addCommand({
id: "joined-devices", id: "joined-devices",
name: $msg("Joined devices"), name: $msg("Review devices"),
icon: "monitor-smartphone", icon: "monitor-smartphone",
checkCallback: (checking) => { checkCallback: (checking) => {
if (!available()) return false; if (!available()) return false;
+55 -21
View File
@@ -1,3 +1,5 @@
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"; import { DEVICE_ID_PREFERRED, MILESTONE_DOCID, NODEINFO_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { evalObsidianJson } from "../runner/cli.ts"; import { evalObsidianJson } from "../runner/cli.ts";
import { import {
@@ -27,12 +29,7 @@ import {
} from "@vrtmrz/obsidian-test-session"; } from "@vrtmrz/obsidian-test-session";
import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { import { openLiveSyncSettings, waitForVisibleObsidianDialogue, withObsidianPage } from "../runner/ui.ts";
captureObsidianDialogue,
openLiveSyncSettings,
waitForVisibleObsidianDialogue,
withObsidianPage,
} from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts"; import { createTemporaryVault } from "../runner/vault.ts";
import type { Locator, Page } from "playwright"; import type { Locator, Page } from "playwright";
@@ -161,7 +158,9 @@ function createFixture(self: SelfDeviceInfo): DeviceFixture {
async function openFromRemoteSettings(page: Page): Promise<Locator> { async function openFromRemoteSettings(page: Page): Promise<Locator> {
const settings = await openLiveSyncSettings(page, uiTimeoutMs); const settings = await openLiveSyncSettings(page, uiTimeoutMs);
const remoteConfiguration = await settings.openPage("Remote Configuration"); const remoteConfiguration = await settings.openPage("Remote Configuration");
await remoteConfiguration.getByRole("button", { name: "Open", exact: true }).click({ timeout: uiTimeoutMs }); await remoteConfiguration
.getByRole("button", { name: "Review devices", exact: true })
.click({ timeout: uiTimeoutMs });
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await dialogue.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); await dialogue.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
return dialogue; return dialogue;
@@ -185,7 +184,7 @@ async function assertInitialDialogue(dialogue: Locator, self: SelfDeviceInfo, pa
await selfCard.getByText("Locked out", { 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("Not accepted", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
await selfCard await selfCard
.getByText("This device is locked out.", { exact: false }) .getByText("This device has been locked out.", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs }); .waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertDetail(selfCard, "Platform", self.platform); await assertDetail(selfCard, "Platform", self.platform);
await assertDetail(selfCard, "Plug-in version", self.pluginVersion); await assertDetail(selfCard, "Plug-in version", self.pluginVersion);
@@ -339,6 +338,27 @@ async function dismissNotices(page: Page): Promise<void> {
assertEqual(await notices.count(), 0, "Notices still obscured the dialogue screenshot."); assertEqual(await notices.count(), 0, "Notices still obscured the dialogue screenshot.");
} }
async function captureJoinedDevices(
port: number,
filename: string,
assertReady: (page: Page) => Promise<Locator>
): Promise<void> {
const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e";
await mkdir(outputDirectory, { recursive: true });
await withObsidianPage(port, async (page) => {
try {
const dialogue = await assertReady(page);
await dialogue.page().screenshot({ path: join(outputDirectory, filename), fullPage: true });
} catch (error) {
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", 500).catch(() => undefined);
const failurePath = join(outputDirectory, filename.replace(/\.png$/u, ".failure.png"));
await (dialogue?.page() ?? page).screenshot({ path: failurePath, fullPage: true }).catch(() => undefined);
console.error(`Joined devices UI failure screenshot: ${failurePath}`);
throw error;
}
});
}
async function main(): Promise<void> { async function main(): Promise<void> {
const binary = requireObsidianBinary(); const binary = requireObsidianBinary();
const cli = discoverObsidianCli(); const cli = discoverObsidianCli();
@@ -381,24 +401,35 @@ async function main(): Promise<void> {
assert(Boolean(preparedMilestone._rev), "Remote preparation did not create a revisioned milestone document."); assert(Boolean(preparedMilestone._rev), "Remote preparation did not create a revisioned milestone document.");
await putCouchDbDocument(couchDb, dbName, { ...fixture.milestone, _rev: preparedMilestone._rev }); await putCouchDbDocument(couchDb, dbName, { ...fixture.milestone, _rev: preparedMilestone._rev });
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => { await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => {
await page.setViewportSize({ width: 1280, height: 1100 });
const dialogue = await openFromRemoteSettings(page); const dialogue = await openFromRemoteSettings(page);
await assertInitialDialogue(dialogue, self, page); const dialoguePage = dialogue.page();
await page await dialoguePage.setViewportSize({ width: 1280, height: 1100 });
await assertInitialDialogue(dialogue, self, dialoguePage);
await dialoguePage
.locator(".notice") .locator(".notice")
.getByText("Other joined devices last reported a newer plug-in version: 999.0.0.", { exact: true }) .getByText(
"A newer plug-in version was detected in other device records: 999.0.0. We recommend checking whether this device needs updating.",
{ exact: true }
)
.waitFor({ state: "visible", timeout: uiTimeoutMs }); .waitFor({ state: "visible", timeout: uiTimeoutMs });
return dialogue;
}); });
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-desktop.png", async (page) => { await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-desktop.png", async (page) => {
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await dialogue.getByRole("button", { name: "Close", exact: true }).click({ timeout: 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(); await dialogue
.page()
.locator(".notice")
.getByRole("button", { name: "Review devices", exact: true })
.first()
.click();
const reopened = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const reopened = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await reopened.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); await reopened.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
await dismissNotices(page); await dismissNotices(reopened.page());
return reopened;
}); });
await captureObsidianDialogue( await captureJoinedDevices(
session.remoteDebuggingPort, session.remoteDebuggingPort,
"joined-devices-forget-confirmation.png", "joined-devices-forget-confirmation.png",
async (page) => { async (page) => {
@@ -406,12 +437,14 @@ async function main(): Promise<void> {
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await showForgetConfirmation(dialogue); await showForgetConfirmation(dialogue);
await deviceCard(dialogue, targetId).locator(".forget-confirmation").scrollIntoViewIfNeeded(); await deviceCard(dialogue, targetId).locator(".forget-confirmation").scrollIntoViewIfNeeded();
return dialogue;
} }
); );
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-after-forget.png", async (page) => { await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-after-forget.png", async (page) => {
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await confirmForgetTarget(dialogue); await confirmForgetTarget(dialogue);
await dialogue.locator(".modal-content").evaluate((element) => element.scrollTo(0, 0)); await dialogue.locator(".modal-content").evaluate((element) => element.scrollTo(0, 0));
return dialogue;
}); });
await withObsidianPage(session.remoteDebuggingPort, async (page) => { await withObsidianPage(session.remoteDebuggingPort, async (page) => {
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
@@ -423,7 +456,7 @@ async function main(): Promise<void> {
await setObsidianMobileTestMode(session.remoteDebuggingPort, true, uiTimeoutMs); await setObsidianMobileTestMode(session.remoteDebuggingPort, true, uiTimeoutMs);
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-mobile.png", async (page) => { await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-mobile.png", async (page) => {
const opened = await page.evaluate((id) => { const opened = await page.evaluate((id) => {
const app = ( const app = (
window as typeof window & { window as typeof window & {
@@ -435,13 +468,14 @@ async function main(): Promise<void> {
assert(opened, "The Joined devices command did not open the dialogue."); assert(opened, "The Joined devices command did not open the dialogue.");
const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs);
await dialogue.getByText(retainedId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); await dialogue.getByText(retainedId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertScrollableMobileDialogue(page, dialogue); await assertScrollableMobileDialogue(dialogue.page(), dialogue);
assertEqual( assertEqual(
await dialogue.locator(".device-card").count(), await dialogue.locator(".device-card").count(),
2, 2,
"The refreshed mobile list did not retain both remaining devices." "The refreshed mobile list did not retain both remaining devices."
); );
await dismissNotices(page); await dismissNotices(dialogue.page());
return dialogue;
}); });
console.log(`Joined devices scenario passed against CouchDB database ${dbName}.`); console.log(`Joined devices scenario passed against CouchDB database ${dbName}.`);
+2 -2
View File
@@ -27,9 +27,9 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
#### Added #### Added
- We can review participating devices through **Joined devices** in Remote Configuration, the command palette, or a version Notice. - We can choose **Review devices** in Remote Configuration, the command palette, or a version Notice to review participating devices.
- Our device appears first, with its lockout state. Other records show the platform, plug-in version, app version, and last recorded connection time when available. - Our device appears first, with its lockout state. Other records show the platform, plug-in version, app version, and last recorded connection time when available.
- Advisory Notices help us find devices which last reported newer or older plug-in versions. - Advisory Notices highlight newer or older plug-in versions detected in device records and help us check which devices may need updating.
- We can forget records belonging to lost or retired devices while preserving the remote lock state and other devices. - We can forget records belonging to lost or retired devices while preserving the remote lock state and other devices.
- Forgetting a record does not delete Vault files or revoke credentials. A returning device can register again on an unlocked remote. - Forgetting a record does not delete Vault files or revoke credentials. A returning device can register again on an unlocked remote.