mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-05 15:58:19 +00:00
Reject explicit NFC settings before WebApp services start and when settings are loaded or saved. Preserve Legacy interoperability, and clarify host support and the Commonlib-first publication prerequisite.
275 lines
14 KiB
TypeScript
275 lines
14 KiB
TypeScript
import { assertEquals } from "@std/assert";
|
|
import { resolve } from "@std/path";
|
|
import { chromium } from "playwright";
|
|
|
|
import { observePageFailures, startStaticServer, waitFor } from "../helpers/browser.ts";
|
|
|
|
const webAppDist = resolve(import.meta.dirname!, "../../../src/apps/webapp/dist");
|
|
|
|
Deno.test({
|
|
name: "WebApp: production bundle selects a Vault and preserves the main remote while saving P2P settings",
|
|
sanitizeOps: false,
|
|
sanitizeResources: false,
|
|
async fn() {
|
|
const server = await startStaticServer(webAppDist);
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const cancellationPage = await browser.newPage();
|
|
const assertNoCancellationFailures = observePageFailures(cancellationPage);
|
|
await cancellationPage.addInitScript(`
|
|
Object.defineProperty(globalThis, "showDirectoryPicker", {
|
|
configurable: true,
|
|
value: async () => {
|
|
throw new DOMException("Cancelled by WebApp browser test", "AbortError");
|
|
},
|
|
});
|
|
`);
|
|
await cancellationPage.goto(new URL("webapp.html", server.baseUrl).href);
|
|
await cancellationPage.locator("#status").filter({ hasText: "Select a vault folder" }).waitFor();
|
|
const picker = cancellationPage.getByRole("button", { name: "Choose new vault folder" });
|
|
await picker.click();
|
|
await cancellationPage
|
|
.locator("#status.warning")
|
|
.filter({ hasText: "Vault selection was cancelled" })
|
|
.waitFor();
|
|
assertEquals(await picker.isEnabled(), true);
|
|
assertEquals(await cancellationPage.locator("#vault-selector").isVisible(), true);
|
|
assertNoCancellationFailures();
|
|
await cancellationPage.close();
|
|
|
|
const runtimePage = await browser.newPage();
|
|
const assertNoRuntimeFailures = observePageFailures(runtimePage);
|
|
await runtimePage.addInitScript(`
|
|
Object.defineProperty(globalThis, "showDirectoryPicker", {
|
|
configurable: true,
|
|
value: async () => {
|
|
const originRoot = await navigator.storage.getDirectory();
|
|
try {
|
|
await originRoot.removeEntry("livesync-webapp-smoke", { recursive: true });
|
|
} catch (error) {
|
|
if (error?.name !== "NotFoundError") throw error;
|
|
}
|
|
return await originRoot.getDirectoryHandle("livesync-webapp-smoke", { create: true });
|
|
},
|
|
});
|
|
`);
|
|
await runtimePage.goto(new URL("webapp.html", server.baseUrl).href);
|
|
await runtimePage.getByRole("button", { name: "Choose new vault folder" }).click();
|
|
await runtimePage.locator("#vault-selector").waitFor({ state: "hidden", timeout: 30_000 });
|
|
await runtimePage.waitForFunction("globalThis.livesyncApp?.getRuntime() != null");
|
|
await runtimePage.locator("#status.warning").filter({ hasText: "Please configure CouchDB" }).waitFor();
|
|
|
|
const state = await runtimePage.evaluate(async () => {
|
|
const app = (
|
|
globalThis as typeof globalThis & {
|
|
livesyncApp?: {
|
|
getRuntime(): unknown;
|
|
historyStore: {
|
|
getVaultHistory(): Promise<Array<{ name: string }>>;
|
|
};
|
|
};
|
|
}
|
|
).livesyncApp;
|
|
return {
|
|
historyNames: (await app!.historyStore.getVaultHistory()).map((item) => item.name),
|
|
runtimeStarted: app!.getRuntime() != null,
|
|
};
|
|
});
|
|
assertEquals(state, {
|
|
historyNames: ["livesync-webapp-smoke"],
|
|
runtimeStarted: true,
|
|
});
|
|
|
|
await runtimePage.getByRole("heading", { name: "Peer to Peer Replicator", exact: true }).waitFor();
|
|
await runtimePage.evaluate(() => {
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
livesyncApp?: {
|
|
getRuntime(): {
|
|
events: {
|
|
emitEvent(name: string, payload: unknown): void;
|
|
};
|
|
} | null;
|
|
};
|
|
}
|
|
).livesyncApp?.getRuntime();
|
|
runtime?.events.emitEvent("p2p-server-status", {
|
|
isConnected: true,
|
|
knownAdvertisements: [
|
|
{
|
|
isAccepted: true,
|
|
name: "Peer without WebApp menu",
|
|
peerId: "peer-without-webapp-menu",
|
|
},
|
|
],
|
|
serverPeerId: "webapp-browser-smoke",
|
|
});
|
|
});
|
|
await runtimePage.getByText("Peer without WebApp menu", { exact: true }).waitFor();
|
|
assertEquals(
|
|
await runtimePage.getByRole("button", { name: "...", exact: true }).count(),
|
|
0,
|
|
"WebApp must not render a peer-menu action without a menu capability"
|
|
);
|
|
await runtimePage.locator(".p2p-control input[type='checkbox']").first().check();
|
|
await runtimePage.getByPlaceholder("wss://exp-relay.vrtmrz.net, wss://xxxxx").fill("ws://127.0.0.1:4010/");
|
|
await runtimePage.getByPlaceholder("anything-you-like").fill("browser-e2e-room");
|
|
await runtimePage.getByPlaceholder("password").fill("browser-e2e-passphrase");
|
|
await runtimePage.getByPlaceholder("iphone-16").fill("browser-e2e-webapp");
|
|
const save = runtimePage.getByRole("button", { name: "Save and Apply", exact: true });
|
|
await save.click();
|
|
await waitFor(async () => await save.isDisabled(), "WebApp did not finish applying P2P settings");
|
|
|
|
const primaryRemote = await runtimePage.evaluate(() => {
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
livesyncApp?: {
|
|
getRuntime(): {
|
|
p2pPaneHost: {
|
|
services: {
|
|
setting: {
|
|
currentSettings(): {
|
|
isConfigured: boolean;
|
|
remoteType: string;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
} | null;
|
|
};
|
|
}
|
|
).livesyncApp?.getRuntime();
|
|
const settings = runtime?.p2pPaneHost.services.setting.currentSettings();
|
|
return {
|
|
isConfigured: settings?.isConfigured,
|
|
remoteType: settings?.remoteType,
|
|
};
|
|
});
|
|
assertEquals(primaryRemote, {
|
|
isConfigured: false,
|
|
remoteType: "",
|
|
});
|
|
await runtimePage.getByRole("button", { name: "Scan local files", exact: true }).waitFor();
|
|
assertNoRuntimeFailures();
|
|
} finally {
|
|
await browser.close();
|
|
await server.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
Deno.test({
|
|
name: "WebApp: rejects NFC settings before opening a database, scanning, writing, or synchronising",
|
|
sanitizeOps: false,
|
|
sanitizeResources: false,
|
|
async fn() {
|
|
const server = await startStaticServer(webAppDist);
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const page = await browser.newPage();
|
|
const assertNoFailures = observePageFailures(page);
|
|
await page.addInitScript(`
|
|
Object.defineProperty(globalThis, "showDirectoryPicker", {
|
|
configurable: true,
|
|
value: async () => {
|
|
const originRoot = await navigator.storage.getDirectory();
|
|
const root = await originRoot.getDirectoryHandle("livesync-webapp-nfc-refusal", { create: true });
|
|
const settingsDirectory = await root.getDirectoryHandle(".livesync", { create: true });
|
|
const settingsText = JSON.stringify({
|
|
pathNormalizationVersion: 1, isConfigured: true, liveSync: true,
|
|
couchDB_URI: "https://couchdb.example.invalid", couchDB_DBNAME: "nfc-refusal-test"
|
|
});
|
|
const settingsWriter = await (await settingsDirectory.getFileHandle("settings.json", { create: true })).createWritable();
|
|
await settingsWriter.write(settingsText);
|
|
await settingsWriter.close();
|
|
const sentinelWriter = await (await root.getFileHandle("Résumé.md".normalize("NFD"), { create: true })).createWritable();
|
|
await sentinelWriter.write("retained native content");
|
|
await sentinelWriter.close();
|
|
const audit = globalThis.webAppIdentityAudit = {
|
|
settingsText, writes: 0, scans: 0, opens: [], remoteRequests: [],
|
|
databaseNamesBefore: (await indexedDB.databases()).map(item => item.name).sort()
|
|
};
|
|
const open = IDBFactory.prototype.open;
|
|
IDBFactory.prototype.open = function(...args) {
|
|
audit.opens.push(String(args[0]));
|
|
return Reflect.apply(open, this, args);
|
|
};
|
|
const createWritable = FileSystemFileHandle.prototype.createWritable;
|
|
FileSystemFileHandle.prototype.createWritable = function(...args) {
|
|
audit.writes++;
|
|
return Reflect.apply(createWritable, this, args);
|
|
};
|
|
for (const key of ["entries", "values", Symbol.asyncIterator]) {
|
|
const original = FileSystemDirectoryHandle.prototype[key];
|
|
if (typeof original !== "function") continue;
|
|
FileSystemDirectoryHandle.prototype[key] = function(...args) {
|
|
audit.scans++;
|
|
return Reflect.apply(original, this, args);
|
|
};
|
|
}
|
|
const fetch = globalThis.fetch;
|
|
globalThis.fetch = function(input, ...args) {
|
|
const url = String(input?.url ?? input);
|
|
if (url.startsWith("https://couchdb.example.invalid")) {
|
|
audit.remoteRequests.push(url);
|
|
return Promise.resolve(new Response("{}", { status: 503 }));
|
|
}
|
|
return Reflect.apply(fetch, this, [input, ...args]);
|
|
};
|
|
return root;
|
|
}
|
|
});
|
|
`);
|
|
await page.goto(new URL("webapp.html", server.baseUrl).href);
|
|
await page.locator("#status").filter({ hasText: "Select a vault folder" }).waitFor();
|
|
await page.getByRole("button", { name: "Choose new vault folder" }).click();
|
|
await page.locator("#status.error").filter({ hasText: "WebApp supports Legacy path identity" }).waitFor();
|
|
const state = await page.evaluate(async () => {
|
|
const globals = globalThis as typeof globalThis & {
|
|
webAppIdentityAudit: {
|
|
settingsText: string;
|
|
writes: number;
|
|
scans: number;
|
|
opens: string[];
|
|
remoteRequests: string[];
|
|
databaseNamesBefore: Array<string | undefined>;
|
|
};
|
|
livesyncApp: { getRuntime(): unknown };
|
|
};
|
|
const audit = globals.webAppIdentityAudit;
|
|
const root = await (
|
|
await navigator.storage.getDirectory()
|
|
).getDirectoryHandle("livesync-webapp-nfc-refusal");
|
|
const settingsDirectory = await root.getDirectoryHandle(".livesync");
|
|
return {
|
|
runtime: globals.livesyncApp.getRuntime(),
|
|
writes: audit.writes,
|
|
scans: audit.scans,
|
|
appDatabaseOpens: audit.opens.filter((name) => name !== "livesync-webapp-handles"),
|
|
remoteRequests: audit.remoteRequests,
|
|
databaseNamesBefore: audit.databaseNamesBefore,
|
|
databaseNamesAfter: (await indexedDB.databases()).map((item) => item.name).sort(),
|
|
settingsUnchanged:
|
|
(await (await (await settingsDirectory.getFileHandle("settings.json")).getFile()).text()) ===
|
|
audit.settingsText,
|
|
sentinel: await (await (await root.getFileHandle("Résumé.md".normalize("NFD"))).getFile()).text(),
|
|
};
|
|
});
|
|
assertEquals(state.runtime, null);
|
|
assertEquals(state.writes, 0);
|
|
assertEquals(state.scans, 0);
|
|
assertEquals(state.appDatabaseOpens, []);
|
|
assertEquals(state.remoteRequests, []);
|
|
assertEquals(state.databaseNamesAfter, state.databaseNamesBefore);
|
|
assertEquals(state.settingsUnchanged, true);
|
|
assertEquals(state.sentinel, "retained native content");
|
|
assertEquals(await page.locator("#vault-selector").isVisible(), true);
|
|
assertEquals(await page.getByRole("button", { name: "Scan local files", exact: true }).count(), 0);
|
|
assertNoFailures();
|
|
} finally {
|
|
await browser.close();
|
|
await server.close();
|
|
}
|
|
},
|
|
});
|