mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-25 02:23:22 +00:00
Adopt Commonlib 0.2.0 with injected device storage
This commit is contained in:
1 parent
e56098eb57
commit
96ad2737a7
60 files changed
+1499
-541
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { LOG_LEVEL_INFO, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { LiveSyncBrowserServiceHubOptions } from "@/apps/browser/createLiveSyncBrowserServiceHub";
|
||||
@@ -168,7 +169,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
|
||||
it("owns core start, directory scan, status reporting, and shutdown", async () => {
|
||||
const reportStatus = vi.fn();
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus });
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
|
||||
|
||||
await runtime.start();
|
||||
|
||||
@@ -193,7 +194,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
});
|
||||
|
||||
it("finalises application readiness after importing local files for optional P2P", async () => {
|
||||
const runtime = new WebAppRuntime(createRootHandle());
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
vi.clearAllMocks();
|
||||
runtimeMocks.currentSettings.mockReturnValue(unconfiguredSettings);
|
||||
@@ -239,7 +240,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not publish readiness after a partial local-file import", async () => {
|
||||
const runtime = new WebAppRuntime(createRootHandle());
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
vi.clearAllMocks();
|
||||
runtimeMocks.collectFilesOnStorage.mockResolvedValue({
|
||||
@@ -269,7 +270,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not publish readiness when database initialisation rejects manual preparation", async () => {
|
||||
const runtime = new WebAppRuntime(createRootHandle());
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
vi.clearAllMocks();
|
||||
runtimeMocks.onDatabaseInitialised.mockResolvedValue(false);
|
||||
@@ -282,7 +283,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not publish readiness when pending file events cannot be committed", async () => {
|
||||
const runtime = new WebAppRuntime(createRootHandle());
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
vi.clearAllMocks();
|
||||
runtimeMocks.commitPendingFileEvents.mockResolvedValue(false);
|
||||
@@ -295,7 +296,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not repeat readiness finalisation after a successful manual scan", async () => {
|
||||
const runtime = new WebAppRuntime(createRootHandle());
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -311,7 +312,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
it("rejects a failed core start after cleaning up the partial runtime", async () => {
|
||||
runtimeMocks.onLoad.mockResolvedValue(false);
|
||||
const reportStatus = vi.fn();
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus });
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
|
||||
|
||||
await expect(runtime.start()).rejects.toThrow("Failed to initialise LiveSync");
|
||||
|
||||
@@ -325,7 +326,7 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
|
||||
it("shuts down once before scheduling a host reload", async () => {
|
||||
const scheduleReload = vi.fn();
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { scheduleReload });
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { scheduleReload, deviceValues: new MemoryValueStore() });
|
||||
await runtime.start();
|
||||
|
||||
runtimeMocks.options?.restart?.schedule();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
@@ -27,6 +28,7 @@ describe("WebPeer runtime composition", () => {
|
||||
translate: (key) => `webpeer:${key}`,
|
||||
});
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
context,
|
||||
store: createMemoryStore(),
|
||||
});
|
||||
@@ -43,6 +45,7 @@ describe("WebPeer runtime composition", () => {
|
||||
|
||||
it("loads settings and opens the local database before announcing layout readiness", async () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
store: createMemoryStore(),
|
||||
});
|
||||
const loadSettings = vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
|
||||
@@ -68,6 +71,7 @@ describe("WebPeer runtime composition", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
store: createMemoryStore(),
|
||||
});
|
||||
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
|
||||
@@ -77,9 +81,7 @@ describe("WebPeer runtime composition", () => {
|
||||
P2P_Enabled: true,
|
||||
});
|
||||
vi.spyOn(runtime.services.database, "openDatabase").mockResolvedValue(true);
|
||||
const onResumed = vi
|
||||
.spyOn(runtime.services.appLifecycle, "onResumed")
|
||||
.mockResolvedValue(true);
|
||||
const onResumed = vi.spyOn(runtime.services.appLifecycle, "onResumed").mockResolvedValue(true);
|
||||
const open = vi.spyOn(runtime.p2p.transportLifecycle, "connect").mockResolvedValue(undefined);
|
||||
|
||||
await runtime.start();
|
||||
@@ -94,9 +96,11 @@ describe("WebPeer runtime composition", () => {
|
||||
|
||||
it("rejects start when the browser-local database cannot be opened", async () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
store: createMemoryStore(),
|
||||
});
|
||||
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
|
||||
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({ ...DEFAULT_SETTINGS });
|
||||
vi.spyOn(runtime.services.database, "openDatabase").mockResolvedValue(false);
|
||||
|
||||
await expect(runtime.start()).rejects.toThrow("WebPeer local database could not be opened");
|
||||
@@ -104,6 +108,7 @@ describe("WebPeer runtime composition", () => {
|
||||
|
||||
it("isolates a specialised runtime and applies its device name before opening the database", async () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
store: createMemoryStore(),
|
||||
deviceName: " p2p-check-browser-desktop-abc ",
|
||||
systemVaultName: "p2p-check-vault",
|
||||
@@ -114,15 +119,39 @@ describe("WebPeer runtime composition", () => {
|
||||
P2P_AutoStart: false,
|
||||
P2P_Enabled: false,
|
||||
});
|
||||
const setSmallConfig = vi.spyOn(runtime.services.config, "setSmallConfig").mockImplementation(() => undefined);
|
||||
let finishWrite!: () => void;
|
||||
const setValue = vi.spyOn(runtime.services.config, "setValue").mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finishWrite = resolve;
|
||||
})
|
||||
);
|
||||
const openDatabase = vi.spyOn(runtime.services.database, "openDatabase").mockImplementation(async () => {
|
||||
expect(setSmallConfig).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
|
||||
expect(setValue).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
|
||||
return true;
|
||||
});
|
||||
|
||||
await runtime.start();
|
||||
const starting = runtime.start();
|
||||
await vi.waitFor(() => expect(setValue).toHaveBeenCalledOnce());
|
||||
expect(openDatabase).not.toHaveBeenCalled();
|
||||
finishWrite();
|
||||
await starting;
|
||||
|
||||
expect(runtime.services.API.getSystemVaultName()).toBe("p2p-check-vault");
|
||||
expect(openDatabase).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reads the current device name after the pane updates the store", async () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
deviceValues: new MemoryValueStore(),
|
||||
store: createMemoryStore(),
|
||||
systemVaultName: "webpeer-vault",
|
||||
});
|
||||
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({ ...DEFAULT_SETTINGS });
|
||||
expect(await runtime.getDeviceName()).toBe("webpeer-vault");
|
||||
await runtime.services.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, "first-name");
|
||||
expect(await runtime.getDeviceName()).toBe("first-name");
|
||||
await runtime.services.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, "renamed-device");
|
||||
expect(await runtime.getDeviceName()).toBe("renamed-device");
|
||||
});
|
||||
});
|
||||
@@ -17,13 +17,12 @@ export const SERVICE_CONTEXT_MEMBERS = [
|
||||
"vault",
|
||||
"test",
|
||||
"UI",
|
||||
"config",
|
||||
"keyValueDB",
|
||||
"control",
|
||||
] as const satisfies readonly Exclude<keyof ServiceHub, "context">[];
|
||||
] as const satisfies readonly Exclude<keyof ServiceHub, "context" | "config">[];
|
||||
|
||||
export type ServiceContextMember = (typeof SERVICE_CONTEXT_MEMBERS)[number];
|
||||
type MissingServiceContextMember = Exclude<Exclude<keyof ServiceHub, "context">, ServiceContextMember>;
|
||||
type MissingServiceContextMember = Exclude<Exclude<keyof ServiceHub, "context" | "config">, ServiceContextMember>;
|
||||
const serviceContextMembersAreExhaustive: [MissingServiceContextMember] extends [never] ? true : never = true;
|
||||
void serviceContextMembersAreExhaustive;
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export async function readE2eCompatibilityMarker(
|
||||
return await evalObsidianJson<CompatibilityMarkerState>(
|
||||
cliBinary,
|
||||
[
|
||||
"(()=>{",
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const setting=core.services.setting;",
|
||||
"const settings=setting.currentSettings();",
|
||||
@@ -122,7 +122,7 @@ export async function readE2eCompatibilityMarker(
|
||||
"return JSON.stringify({",
|
||||
"vaultName,additionalSuffix,expectedStorageKey,",
|
||||
"rawStorageValue:localStorage.getItem(expectedStorageKey),",
|
||||
"serviceValue:setting.getSmallConfig(markerKey),",
|
||||
"serviceValue:await setting.config.getValue(markerKey,''),",
|
||||
"versionUpFlash:settings.versionUpFlash,",
|
||||
"});",
|
||||
"})()",
|
||||
|
||||
@@ -288,7 +288,7 @@ export async function readRuntimeUpgradeState(
|
||||
"localDatabaseDocumentCount:databaseInfo.doc_count,",
|
||||
"nodeId:nodeInfo?.nodeid??replicator?.nodeid??'',",
|
||||
"legacyCompatibilityMarker:localStorage.getItem(`obsidian-live-sync-ver${vaultName}`),",
|
||||
"compatibilityMarker:setting.getSmallConfig('database-compatibility-version')??'',",
|
||||
"compatibilityMarker:await (setting.config?.getValue?.('database-compatibility-version','')??Promise.resolve(setting.getSmallConfig?.('database-compatibility-version')??'')),",
|
||||
"compatibilityStorageEntries:Object.fromEntries(Array.from({length:localStorage.length},(_,index)=>localStorage.key(index))",
|
||||
".filter((key)=>key!==null)",
|
||||
".filter(key=>key.startsWith('obsidian-live-sync-ver')||key.endsWith('-database-compatibility-version'))",
|
||||
@@ -335,7 +335,7 @@ export async function readRuntimeSettingsUpgradeState(
|
||||
"const migrationState=setting.getSettingsMigrationState?.();",
|
||||
"return JSON.stringify({",
|
||||
"pluginVersion:app.plugins.manifests['obsidian-livesync']?.version??'unknown',",
|
||||
"compatibilityMarker:setting.getSmallConfig?.('database-compatibility-version')??'',",
|
||||
"compatibilityMarker:await (setting.config?.getValue?.('database-compatibility-version','')??Promise.resolve(setting.getSmallConfig?.('database-compatibility-version')??'')),",
|
||||
"migrationState,",
|
||||
"settings:{",
|
||||
"isConfigured:settings.isConfigured,settingVersion:settings.settingVersion,",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/setup-uri";
|
||||
import { decryptString } from "@vrtmrz/livesync-commonlib/compat/encryption/stringEncryption";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { $msg } from "../../../src/common/translation.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||
@@ -13,18 +15,34 @@ import {
|
||||
captureObsidianPage,
|
||||
obsidianRemoteDebuggingPort,
|
||||
openLiveSyncSettings,
|
||||
withObsidianPage,
|
||||
withObsidianPage as withObsidianSessionPage,
|
||||
} from "../runner/ui.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
const dialogRunStateKey = "__livesyncE2EDialogMount";
|
||||
const savedP2PDeviceName = "e2e-p2p-saved-device";
|
||||
const repairRunStateKey = "__livesyncE2ETroubleshootingRepair";
|
||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000);
|
||||
|
||||
async function withObsidianPage<T>(port: number, operation: (page: Page) => Promise<T>): Promise<T> {
|
||||
return await withObsidianSessionPage(port, async (page) => {
|
||||
for (const candidate of page.context().pages()) {
|
||||
const hasPlugin = await candidate
|
||||
.evaluate(
|
||||
() => (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"] !== undefined
|
||||
)
|
||||
.catch(() => false);
|
||||
if (hasPlugin) return await operation(candidate);
|
||||
}
|
||||
throw new Error("The LiveSync Vault renderer is unavailable.");
|
||||
});
|
||||
}
|
||||
|
||||
type DialogueMode = "desktop" | "mobile";
|
||||
|
||||
type DialogueRunState = {
|
||||
done: boolean;
|
||||
deviceNameRead?: boolean;
|
||||
error?: string;
|
||||
expected?: unknown;
|
||||
kind: string;
|
||||
@@ -46,6 +64,14 @@ type LiveSyncTestPlugin = {
|
||||
createAllChunks(force: boolean): Promise<unknown>;
|
||||
};
|
||||
modules: SetupManagerHandle[];
|
||||
services: {
|
||||
setting: {
|
||||
config: {
|
||||
getValue(key: string, defaultValue?: string): Promise<string>;
|
||||
setValue(key: string, value: string): Promise<void>;
|
||||
};
|
||||
};
|
||||
};
|
||||
settings: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
@@ -91,39 +117,63 @@ async function openRemoteSelectionDialogue(): Promise<void> {
|
||||
|
||||
async function openP2PRemoteSelectionDialogueForInspection(): Promise<void> {
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
await page.evaluate((stateKey) => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
|
||||
const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager");
|
||||
if (
|
||||
typeof manager?.onSelectServer !== "function" ||
|
||||
typeof manager.onConfirmApplySettingsFromWizard !== "function"
|
||||
) {
|
||||
throw new Error("Could not find the P2P setup workflow");
|
||||
await page.evaluate(
|
||||
async ({ stateKey, deviceNameKey, deviceName }) => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
|
||||
const initialPeerName = plugin.core.settings.P2P_DevicePeerName;
|
||||
if (typeof initialPeerName === "string" && initialPeerName.trim() !== "") {
|
||||
throw new Error("The P2P setup fixture unexpectedly has an initial device name.");
|
||||
}
|
||||
const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager");
|
||||
if (
|
||||
typeof manager?.onSelectServer !== "function" ||
|
||||
typeof manager.onConfirmApplySettingsFromWizard !== "function"
|
||||
) {
|
||||
throw new Error("Could not find the P2P setup workflow");
|
||||
}
|
||||
const config = plugin.core.services.setting.config;
|
||||
await config.setValue(deviceNameKey, deviceName);
|
||||
const originalGetValue = config.getValue;
|
||||
const originalConfirm = manager.onConfirmApplySettingsFromWizard;
|
||||
const state: DialogueRunState = {
|
||||
kind: "p2p-compatibility-settings",
|
||||
done: false,
|
||||
deviceNameRead: false,
|
||||
};
|
||||
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
|
||||
config.getValue = async (key, defaultValue) => {
|
||||
const value = await originalGetValue.call(config, key, defaultValue);
|
||||
if (key === deviceNameKey && value === deviceName) state.deviceNameRead = true;
|
||||
return value;
|
||||
};
|
||||
manager.onConfirmApplySettingsFromWizard = async (settings: unknown) => {
|
||||
state.expected = settings;
|
||||
return true;
|
||||
};
|
||||
void manager
|
||||
.onSelectServer(plugin.core.settings, "unknown")
|
||||
.then(
|
||||
(result) => {
|
||||
state.result = result;
|
||||
state.done = true;
|
||||
},
|
||||
(error: unknown) => {
|
||||
state.error = error instanceof Error ? error.message : String(error);
|
||||
state.done = true;
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
manager.onConfirmApplySettingsFromWizard = originalConfirm;
|
||||
config.getValue = originalGetValue;
|
||||
});
|
||||
},
|
||||
{
|
||||
stateKey: dialogRunStateKey,
|
||||
deviceNameKey: SETTING_KEY_P2P_DEVICE_NAME,
|
||||
deviceName: savedP2PDeviceName,
|
||||
}
|
||||
const originalConfirm = manager.onConfirmApplySettingsFromWizard;
|
||||
const state: DialogueRunState = { kind: "p2p-compatibility-settings", done: false };
|
||||
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
|
||||
manager.onConfirmApplySettingsFromWizard = async (settings: unknown) => {
|
||||
state.expected = settings;
|
||||
return true;
|
||||
};
|
||||
void manager
|
||||
.onSelectServer(plugin.core.settings, "unknown")
|
||||
.then(
|
||||
(result) => {
|
||||
state.result = result;
|
||||
state.done = true;
|
||||
},
|
||||
(error: unknown) => {
|
||||
state.error = error instanceof Error ? error.message : String(error);
|
||||
state.done = true;
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
manager.onConfirmApplySettingsFromWizard = originalConfirm;
|
||||
});
|
||||
}, dialogRunStateKey);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,7 +438,7 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
||||
if (previousDialogue === null) {
|
||||
throw new Error("The remote selection dialogue did not expose a close control.");
|
||||
}
|
||||
await modal.locator(".modal-close-button").click({ timeout: uiTimeoutMs });
|
||||
await modal.locator(".modal-close-button, .modal-header-button").last().click({ timeout: uiTimeoutMs });
|
||||
await page.waitForFunction((element) => !element.isConnected, previousDialogue, { timeout: uiTimeoutMs });
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
}
|
||||
@@ -495,9 +545,21 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
|
||||
has: page.locator(".modal-title").filter({ hasText: "P2P Configuration" }),
|
||||
});
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const peerName = modal.locator('input[name="p2p-device-peer-id"]');
|
||||
await peerName.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
(expectedName) => {
|
||||
const input = document.querySelector('input[name="p2p-device-peer-id"]');
|
||||
return input instanceof HTMLInputElement && !input.disabled && input.value === expectedName;
|
||||
},
|
||||
savedP2PDeviceName,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
if ((await peerName.inputValue()) !== savedP2PDeviceName) {
|
||||
throw new Error("The saved P2P device name was not loaded before submitting the setup dialogue.");
|
||||
}
|
||||
await modal.locator('input[name="p2p-room-id"]').fill("e2e-p2p-compatibility");
|
||||
await modal.locator('input[name="p2p-password"]').fill("e2e-passphrase");
|
||||
await modal.locator('input[name="p2p-device-peer-id"]').fill("e2e-device");
|
||||
|
||||
const compatibility = modal.locator("details").filter({
|
||||
has: page.locator("summary").filter({ hasText: "Connection compatibility" }),
|
||||
@@ -565,8 +627,7 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
const resetNotice = compatibility.locator(".sls-info-note-notice").filter({
|
||||
hasText:
|
||||
"TURN relay only requires TURN configuration. Connection path has been restored to Automatic.",
|
||||
hasText: "TURN relay only requires TURN configuration. Connection path has been restored to Automatic.",
|
||||
});
|
||||
await resetNotice.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await resetNotice
|
||||
@@ -594,10 +655,16 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
|
||||
});
|
||||
|
||||
const state = await assertDialogueRunCompleted();
|
||||
if (state.deviceNameRead !== true) {
|
||||
throw new Error("The saved P2P device name was not read through setting.config.");
|
||||
}
|
||||
if (state.result !== true || typeof state.expected !== "object" || state.expected === null) {
|
||||
throw new Error("The P2P compatibility settings were not passed to the setup workflow.");
|
||||
}
|
||||
const settings = state.expected as Record<string, unknown>;
|
||||
if (settings.P2P_DevicePeerName !== savedP2PDeviceName) {
|
||||
throw new Error("The saved P2P device name was not passed through the setup workflow.");
|
||||
}
|
||||
if (settings.P2P_maxWirePayloadBytes !== 800 || settings.P2P_connectionPath !== "relay") {
|
||||
throw new Error(
|
||||
`The P2P compatibility choices did not round-trip through the setup dialogue: ${JSON.stringify({
|
||||
@@ -672,15 +739,14 @@ async function verifyGenerateSetupUriDialogue(mode: DialogueMode): Promise<strin
|
||||
const app = (globalThis as ObsidianTestGlobal).app;
|
||||
return Boolean(
|
||||
app?.plugins?.plugins["obsidian-livesync"]?.core?.settings.isConfigured &&
|
||||
app.commands?.commands?.[commandId]
|
||||
app.commands?.commands?.[commandId]
|
||||
);
|
||||
},
|
||||
"obsidian-livesync:livesync-copysetupuri",
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
return await page.evaluate(
|
||||
(commandId) =>
|
||||
(globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true,
|
||||
(commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true,
|
||||
"obsidian-livesync:livesync-copysetupuri"
|
||||
);
|
||||
});
|
||||
@@ -714,7 +780,8 @@ async function verifyGenerateSetupUriDialogue(mode: DialogueMode): Promise<strin
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await choice.getByRole("button", { name: "Time-bound", exact: true }).waitFor({
|
||||
state: "visible", timeout: uiTimeoutMs,
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
if (mode === "mobile") await assertMobileDialogueLayout(page, choice, "Setup URI availability dialogue");
|
||||
await choice.getByRole("button", { name: selected, exact: true }).click({ timeout: uiTimeoutMs });
|
||||
@@ -866,6 +933,7 @@ async function verifyCompatibleAlignmentSettingDefault(): Promise<void> {
|
||||
if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) {
|
||||
throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined.");
|
||||
}
|
||||
await settingsNavigator.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1094,9 +1162,10 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
|
||||
);
|
||||
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const liveSyncSettings = page.locator(".sls-setting");
|
||||
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||
const liveSyncSettings = await settingsNavigator.openPage("Hatch");
|
||||
const logSetting = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: page.getByText("Write logs into the file", { exact: true }),
|
||||
has: settingsNavigator.page.getByText("Write logs into the file", { exact: true }),
|
||||
});
|
||||
await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
@@ -1130,10 +1199,10 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
|
||||
|
||||
// Saving a toggle refreshes the settings pane. Resolve the visible control again so the
|
||||
// second action does not target the detached pre-save element.
|
||||
const refreshedLogToggle = page
|
||||
.locator(".sls-setting:visible .setting-item:visible")
|
||||
const refreshedLogToggle = liveSyncSettings
|
||||
.locator(".setting-item:visible")
|
||||
.filter({
|
||||
has: page.getByText("Write logs into the file", { exact: true }),
|
||||
has: settingsNavigator.page.getByText("Write logs into the file", { exact: true }),
|
||||
})
|
||||
.locator(".checkbox-container:visible")
|
||||
.last();
|
||||
@@ -1179,13 +1248,9 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
|
||||
}
|
||||
};
|
||||
}, repairRunStateKey);
|
||||
await page
|
||||
.locator(".sls-setting:visible")
|
||||
.last()
|
||||
.getByRole("button", { name: "Recreate current chunks", exact: true })
|
||||
.click({
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await liveSyncSettings.getByRole("button", { name: "Recreate current chunks", exact: true }).click({
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
(stateKey) =>
|
||||
(globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey]?.done === true,
|
||||
@@ -1200,13 +1265,9 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
|
||||
throw new Error(`Recreate missing chunks failed: ${repairState.error}`);
|
||||
}
|
||||
|
||||
await page
|
||||
.locator(".sls-setting:visible")
|
||||
.last()
|
||||
.getByRole("button", { name: "Begin inspection", exact: true })
|
||||
.click({
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await liveSyncSettings.getByRole("button", { name: "Begin inspection", exact: true }).click({
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await page
|
||||
.locator(".notice")
|
||||
.filter({ hasText: /^done$/u })
|
||||
@@ -1214,6 +1275,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await settingsNavigator.close();
|
||||
});
|
||||
|
||||
return screenshotPath;
|
||||
|
||||
@@ -372,13 +372,21 @@ async function restartAndResumeHarness(): Promise<string> {
|
||||
await harness
|
||||
.locator('[data-testid="review-harness-resumed"]')
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const continuationRemoved = await page.evaluate((stateKey) => {
|
||||
const continuationRemoved = await page.evaluate(async (stateKey) => {
|
||||
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
|
||||
throw new Error("Self-hosted LiveSync is unavailable after restart.");
|
||||
}
|
||||
const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } }).core;
|
||||
return core.services.setting.getSmallConfig(stateKey) === "";
|
||||
const core = (
|
||||
plugin as {
|
||||
core: {
|
||||
services: {
|
||||
setting: { config: { getValue(key: string, defaultValue: string): Promise<string> } };
|
||||
};
|
||||
};
|
||||
}
|
||||
).core;
|
||||
return (await core.services.setting.config.getValue(stateKey, "")) === "";
|
||||
}, REVIEW_HARNESS_STATE_KEY);
|
||||
if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use.");
|
||||
await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" });
|
||||
|
||||
@@ -58,8 +58,9 @@ type LiveSyncTestPlugin = {
|
||||
usePowerUserMode: boolean;
|
||||
useEdgeCaseMode: boolean;
|
||||
hashCacheMaxCount: number;
|
||||
customChunkSize: number;
|
||||
};
|
||||
getSmallConfig(key: string): string | null;
|
||||
config: { getValue(key: string, defaultValue: string): Promise<string> };
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -484,12 +485,12 @@ async function verifyCompatibilityReview(): Promise<void> {
|
||||
});
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const markerBeforeAcknowledgement = await page.evaluate(() => {
|
||||
const markerBeforeAcknowledgement = await page.evaluate(async () => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
|
||||
return plugin.core.services.setting.getSmallConfig("database-compatibility-version");
|
||||
return await plugin.core.services.setting.config.getValue("database-compatibility-version", "");
|
||||
});
|
||||
if (markerBeforeAcknowledgement !== null && markerBeforeAcknowledgement !== "") {
|
||||
if (markerBeforeAcknowledgement !== "") {
|
||||
throw new Error(
|
||||
`The database version was marked as acknowledged before review: ${markerBeforeAcknowledgement}`
|
||||
);
|
||||
@@ -579,12 +580,12 @@ async function verifyCompatibilityReview(): Promise<void> {
|
||||
.click();
|
||||
await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
(expectedVersion) => {
|
||||
async (expectedVersion) => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (plugin === undefined) return false;
|
||||
const setting = plugin.core.services.setting;
|
||||
return (
|
||||
setting.getSmallConfig("database-compatibility-version") === expectedVersion &&
|
||||
(await setting.config.getValue("database-compatibility-version", "")) === expectedVersion &&
|
||||
setting.currentSettings().versionUpFlash === ""
|
||||
);
|
||||
},
|
||||
@@ -611,10 +612,16 @@ async function verifyConfigDoctorFollowsCompatibilityReview(): Promise<string> {
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
await doctor.getByText("Enhance chunk size", { exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
const chunkSize = await page.evaluate(() => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
return plugin?.core.services.setting.currentSettings().customChunkSize;
|
||||
});
|
||||
if (chunkSize !== 0) {
|
||||
throw new Error("Compatibility review must retain the fixture's zero chunk-size value.");
|
||||
}
|
||||
if ((await doctor.getByText("Enhance chunk size", { exact: true }).count()) !== 0) {
|
||||
throw new Error("Config Doctor must accept the recommended zero chunk-size value.");
|
||||
}
|
||||
if ((await doctor.getByText("Data Compression", { exact: true }).count()) !== 0) {
|
||||
throw new Error("Config Doctor still treats supported Data Compression as a problem.");
|
||||
}
|
||||
@@ -978,6 +985,7 @@ function createSettingsPluginData(settingsOnlyRun: boolean): Record<string, unkn
|
||||
liveSync: false,
|
||||
versionUpFlash: settingsOnlyRun ? "" : compatibilityReviewMessage,
|
||||
notifyThresholdOfRemoteStorageSize: 0,
|
||||
customChunkSize: 0,
|
||||
syncOnStart: false,
|
||||
syncOnSave: false,
|
||||
syncOnEditorSave: false,
|
||||
|
||||
Reference in new issue
Block a user