diff --git a/web/tests/integration/clips-browser.html b/web/tests/integration/clips-browser.html
new file mode 100644
index 0000000..2c39c3e
--- /dev/null
+++ b/web/tests/integration/clips-browser.html
@@ -0,0 +1 @@
+
Encrypted Clips test harnessTest harness — synthetic account session; real local WASM encryption and IndexedDB. No account-server qualification.
\ No newline at end of file
diff --git a/web/tests/integration/clips-browser.mjs b/web/tests/integration/clips-browser.mjs
new file mode 100644
index 0000000..316d71a
--- /dev/null
+++ b/web/tests/integration/clips-browser.mjs
@@ -0,0 +1,20 @@
+import { chromium, expect } from '@playwright/test';
+import { mkdir,readFile,writeFile } from 'node:fs/promises';
+import { unzipSync,strFromU8 } from 'fflate';
+const output=process.env.NOTES_UI_OUTPUT ?? 'test-results/clips-browser';await mkdir(output,{recursive:true});
+const browser=await chromium.launch({channel:'chrome',headless:true});const context=await browser.newContext({viewport:{width:1160,height:800},acceptDownloads:true});const page=await context.newPage();const errors=[];page.on('pageerror',error=>errors.push(error.message));
+const status= text=>page.getByRole('status').filter({hasText:text});
+const url='https://example.test/a#first';const pasted='\nActual pasted text, not a fetched page.';
+async function openForm(){await page.getByRole('button',{name:'Save a link',exact:true}).first().click();return page.getByRole('dialog',{name:'Save a link',exact:true});}
+try{
+await page.goto(`${process.env.NOTES_PREVIEW_URL ?? 'http://127.0.0.1:5179'}/tests/integration/clips-browser.html`);await page.getByRole('button',{name:'Clips',exact:true}).click();await expect(page.getByRole('heading',{name:'No clips yet',exact:true})).toBeVisible();
+let dialog=await openForm();await dialog.getByLabel('Clip URL').fill(url);await dialog.getByRole('button',{name:'Save link',exact:true}).click();await expect(status('Link saved')).toBeVisible();await status('Link saved').getByRole('button',{name:'Open clip',exact:true}).click();await expect(page.getByText('Only the link is saved. Open the original page to read it.',{exact:true})).toBeVisible();await expect(page.getByRole('link',{name:'Open original',exact:true})).toHaveAttribute('href',url);await expect(page.getByRole('button',{name:'Mark read',exact:true})).toBeVisible();await page.getByRole('button',{name:'Read later',exact:true}).click();await expect(status('Added to read later')).toBeVisible();await page.getByRole('button',{name:'Mark read',exact:true}).click();await expect(status('Marked read')).toBeVisible();await page.getByRole('button',{name:'← Back to clips',exact:true}).click();await page.getByRole('button',{name:/^Unread/}).click();await expect(page.getByRole('heading',{name:'No unread clips',exact:true})).toBeVisible();
+await page.getByRole('group',{name:'Filter clips'}).getByRole('button',{name:/^All/}).click();
+dialog=await openForm();await dialog.getByLabel('Clip URL').fill('https://sample.test/pasted');await dialog.getByLabel('Title (optional)').fill('Pasted local article');await dialog.getByLabel('Selected text (optional)').fill(pasted);await context.setOffline(true);await dialog.getByRole('button',{name:'Save link',exact:true}).click();await expect(status('Text clip saved')).toBeVisible();await context.setOffline(false);await page.reload();await page.getByRole('button',{name:'Clips',exact:true}).click();await page.getByRole('button',{name:/Pasted local article/}).click();await expect(page.locator('.clip-text')).toHaveText(pasted);await expect(page.locator('.clip-reader script')).toHaveCount(0);await page.getByRole('button',{name:'Archive',exact:true}).click();await expect(status('Clip archived')).toBeVisible();await page.getByRole('button',{name:'← Back to clips',exact:true}).click();await page.getByRole('button',{name:/^Archived/}).click();await page.getByRole('button',{name:/Pasted local article/}).click();await page.getByRole('button',{name:'Unarchive',exact:true}).click();await expect(status('Clip unarchived')).toBeVisible();await page.getByRole('button',{name:'← Back to clips',exact:true}).click();
+dialog=await openForm();await dialog.getByLabel('Clip URL').fill('https://example.test/a#second');await dialog.getByLabel('Title (optional)').fill('Deliberate duplicate');await dialog.getByRole('button',{name:'Save link',exact:true}).click();dialog=page.getByRole('dialog',{name:'Already saved',exact:true});await expect(dialog).toContainText('Your new text has not been saved.');await dialog.getByRole('button',{name:'Save again',exact:true}).click();await expect(status('Link saved')).toBeVisible();await status('Link saved').getByRole('button',{name:'Undo',exact:true}).click();await expect(status('Clip save undone')).toBeVisible();await page.getByRole('group',{name:'Filter clips'}).getByRole('button',{name:/^All/}).click();await expect(page.getByRole('button',{name:/Deliberate duplicate/})).toHaveCount(0);
+await page.getByRole('button',{name:'Export Clips',exact:true}).click();dialog=page.getByRole('dialog',{name:'Export Clips',exact:true});await expect(dialog).toContainText('readable copies');await expect(page.getByRole('link',{name:'Download Clips export',exact:true})).toHaveCount(0);await dialog.getByRole('button',{name:'Export',exact:true}).click();const downloadPromise=page.waitForEvent('download');await page.getByRole('link',{name:'Download Clips export',exact:true}).click();const download=await downloadPromise;const archive=unzipSync(new Uint8Array(await readFile(await download.path())));const exported=JSON.parse(strFromU8(archive['clips.json']));if(exported.items.length!==2 || !exported.items.some(item=>item.text===pasted) || !exported.items.some(item=>item.originalUrl===url && item.readAt && item.readLater))throw Error('Complete export content/state mismatch');
+for(const theme of ['light','dark','dim']){await page.evaluate(theme=>{document.documentElement.dataset.theme=theme;},theme);await page.screenshot({path:output+`/list-${theme}.png`});}
+await page.getByRole('button',{name:/Pasted local article/}).click();await page.screenshot({path:output+'/reader-dim.png'});await page.setViewportSize({width:390,height:844});await page.emulateMedia({reducedMotion:'reduce'});await page.screenshot({path:output+'/reader-narrow-dim.png'});if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth))throw Error('Narrow page overflow');await page.getByRole('button',{name:'← Back to clips',exact:true}).click();dialog=await openForm();await dialog.getByLabel('Clip URL').fill('https://private.test/unsaved');await dialog.getByLabel('Selected text (optional)').fill('Private unsubmitted clip');await page.screenshot({path:output+'/save-narrow-dim.png'});const guard=await page.evaluate(async()=>{const module=await import('/src/desktop/closeGuard.ts');return module.checkClose();});if(guard!=='draft')throw Error('Clips draft not guarded');await page.keyboard.press('Escape');await page.getByRole('button',{name:'Keep editing',exact:true}).click();await expect(dialog.getByLabel('Clip URL')).toHaveValue('https://private.test/unsaved');await page.evaluate(()=>window.__clipsFixture.erase());await expect(dialog).toHaveCount(0);await expect(page.getByRole('heading',{name:'No clips yet',exact:true})).toBeVisible();await expect(page.getByRole('link',{name:'Download Clips export',exact:true})).toHaveCount(0);
+dialog=await openForm();await dialog.getByLabel('Clip URL').fill('https://private.test/account-draft');await page.evaluate(()=>window.__clipsFixture.lock());await expect(page.getByRole('heading',{name:'Clips is no longer available',exact:true})).toBeVisible();await expect(page.getByLabel('Clip URL')).toHaveCount(0);if(errors.length)throw Error(errors.join('\n'));
+await writeFile(output+'/result.json',JSON.stringify({qualified:'Synthetic account session; actual production Clips facade/WASM/IndexedDB. Not account-server authentication or native OS external opener.',checks:['link-only save/title fallback/source/no implicit read','pasted text offline save/reload/safe escaped rendering','independent unread/readLater/archive/unarchive','canonical duplicate deliberate Save again','real receipt Undo','complete JSON/readable ZIP export consent and exact source/body/state','Light/Dark/Dim','390px reduced motion no page overflow','dirty guard/Escape Keep editing','whole-workspace erase clears draft+held download','account lock removes draft/private DOM'],exportedItems:exported.items.length,readableFiles:Object.keys(archive).filter(name=>name.endsWith('.txt')).length,pageErrors:errors},null,2));console.log('Encrypted Clips browser journey passed');
+}finally{await browser.close();}
diff --git a/web/tests/integration/clips-browser.tsx b/web/tests/integration/clips-browser.tsx
new file mode 100644
index 0000000..b334a85
--- /dev/null
+++ b/web/tests/integration/clips-browser.tsx
@@ -0,0 +1,63 @@
+// Test-only entry. Never imported by the production application.
+import { createRoot } from "react-dom/client";
+import { Workspace } from "../../src/ui/Workspace";
+import { applyInitialPreferences } from "../../src/ui/primitives";
+import { openNotesStore } from "../../src/data";
+import { establish, lock } from "../../src/account/session";
+import { loadEngine, DeviceKeys } from "../../src/data/engine";
+import {
+ newWrappingKey,
+ wrappingSecret,
+ readRecord,
+ mutateRecord,
+} from "../../src/data/database";
+import type { DeviceRecord } from "../../src/data/database";
+import "../../src/styles/app.css";
+import "@fontsource/geist/400.css";
+import "@fontsource/geist/500.css";
+
+const identity = {
+ accountId: "77777777-7777-4777-8777-777777777777",
+ deviceId: "88888888-8888-4888-8888-888888888888",
+ email: "isolated-clips-fixture@example.test",
+ deviceState: "trusted" as const,
+};
+applyInitialPreferences();
+async function start() {
+ await loadEngine();
+ if (!(await readRecord("devices", identity.email))) {
+ const wrapKey = await newWrappingKey(),
+ secret = await wrappingSecret(wrapKey),
+ keys = DeviceKeys.create();
+ try {
+ await mutateRecord("devices", identity.email, () => ({
+ ...identity,
+ wrapKey,
+ sealedKeys: keys.export(secret),
+ generation: 0,
+ }));
+ } finally {
+ secret.fill(0);
+ keys.free();
+ }
+ }
+ establish(identity);
+ const notes = await openNotesStore(identity);
+ Object.assign(window, {
+ __clipsFixture: { erase: () => notes.erase(), lock: () => lock(false) },
+ });
+ createRoot(document.getElementById("root")!).render(
+ {
+ notes.close();
+ }}
+ />,
+ );
+}
+void start().catch((error) => {
+ document.getElementById("root")!.textContent =
+ error instanceof Error ? error.message : "Fixture failed";
+});
diff --git a/web/tests/integration/collections-browser.html b/web/tests/integration/collections-browser.html
new file mode 100644
index 0000000..3def2f5
--- /dev/null
+++ b/web/tests/integration/collections-browser.html
@@ -0,0 +1 @@
+Encrypted collections test harnessTest harness — synthetic account session; real local WASM encryption and IndexedDB. No account-server qualification.
diff --git a/web/tests/integration/collections-browser.tsx b/web/tests/integration/collections-browser.tsx
new file mode 100644
index 0000000..c3f257d
--- /dev/null
+++ b/web/tests/integration/collections-browser.tsx
@@ -0,0 +1,77 @@
+// Test-only entry. Never imported by the production application.
+import { createRoot } from "react-dom/client";
+import { StudyView } from "../../src/ui/StudyView";
+import { JournalView } from "../../src/ui/JournalView";
+import { openJournalVault } from "../../src/collections/journalVault";
+import { applyInitialPreferences } from "../../src/ui/primitives";
+import { openCollectionsStore } from "../../src/collections/store";
+import { openNotesStore } from "../../src/data";
+import { establish } from "../../src/account/session";
+import { loadEngine, DeviceKeys } from "../../src/data/engine";
+import {
+ newWrappingKey,
+ wrappingSecret,
+ readRecord,
+ mutateRecord,
+} from "../../src/data/database";
+import type { DeviceRecord } from "../../src/data/database";
+import "../../src/styles/app.css";
+import "@fontsource/geist/400.css";
+import "@fontsource/geist/500.css";
+
+const identity = {
+ accountId: "11111111-1111-4111-8111-111111111111",
+ deviceId: "22222222-2222-4222-8222-222222222222",
+ email: "isolated-collections-fixture@example.test",
+ deviceState: "trusted" as const,
+};
+applyInitialPreferences();
+async function start() {
+ await loadEngine();
+ if (!(await readRecord("devices", identity.email))) {
+ const wrapKey = await newWrappingKey(),
+ secret = await wrappingSecret(wrapKey),
+ keys = DeviceKeys.create();
+ try {
+ await mutateRecord("devices", identity.email, () => ({
+ ...identity,
+ wrapKey,
+ sealedKeys: keys.export(secret),
+ generation: 0,
+ }));
+ } finally {
+ secret.fill(0);
+ keys.free();
+ }
+ }
+ establish(identity);
+ const store = await openCollectionsStore(identity);
+ const notes = await openNotesStore(identity);
+ Object.assign(window, {
+ __collectionsFixture: {
+ read: () => store.read(),
+ close: () => store.close(),
+ erase: () => notes.erase(),
+ },
+ });
+ if (new URLSearchParams(location.search).get("view") === "journal") {
+ const vault = await openJournalVault(identity, store);
+ Object.assign(window, {
+ __journalFixture: {
+ status: () => vault.status(),
+ lock: () => vault.lock(),
+ setAway: (away: boolean) => vault.setAway(away),
+ },
+ });
+ createRoot(document.getElementById("root")!).render(
+ ,
+ );
+ } else
+ createRoot(document.getElementById("root")!).render(
+ ,
+ );
+}
+void start().catch((error) => {
+ document.getElementById("root")!.textContent =
+ error instanceof Error ? error.message : "Fixture failed";
+});
diff --git a/web/tests/integration/journal-link-browser.mjs b/web/tests/integration/journal-link-browser.mjs
new file mode 100644
index 0000000..7756f9e
--- /dev/null
+++ b/web/tests/integration/journal-link-browser.mjs
@@ -0,0 +1,169 @@
+import { chromium, expect } from "@playwright/test";
+import { mkdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+const output =
+ process.env.NOTES_UI_OUTPUT ?? "test-results/journal-link-browser";
+const browser = await chromium.launch({ channel: "chrome", headless: true });
+const results = [];
+const passphrase = "Synthetic link recovery phrase only";
+try {
+ await mkdir(output, { recursive: true });
+ for (const [theme, width] of [
+ ["light", 1160],
+ ["dark", 1160],
+ ["dim", 390],
+ ]) {
+ const context = await browser.newContext({
+ viewport: { width, height: 844 },
+ reducedMotion: "reduce",
+ });
+ const page = await context.newPage();
+ const errors = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await page.addInitScript((theme) => {
+ localStorage.setItem("notes.theme", theme);
+ localStorage.setItem("notes.motion", "reduced");
+ }, theme);
+ await page.goto(
+ `${process.env.NOTES_PREVIEW_URL ?? "http://127.0.0.1:5179"}/tests/integration/collections-browser.html?view=journal`,
+ );
+ await page.getByLabel("Passphrase", { exact: true }).fill(passphrase);
+ await page
+ .getByLabel("Confirm passphrase", { exact: true })
+ .fill(passphrase);
+ await page
+ .getByRole("button", { name: "Lock Journal", exact: true })
+ .click();
+ async function unlock(secret = passphrase) {
+ await page.getByLabel("Passphrase", { exact: true }).fill(secret);
+ await page
+ .getByRole("button", { name: "Unlock Journal", exact: true })
+ .click();
+ }
+ const editor = page.getByRole("textbox", {
+ name: "Journal entry",
+ exact: true,
+ });
+ await expect(editor).toBeVisible();
+ await editor.fill("Synthetic journal selection");
+ await expect
+ .poll(() => page.evaluate(() => window.__journalFixture.status().saving))
+ .toBe(false);
+ await editor.focus();
+ await page.keyboard.press("Control+A");
+ await page.getByRole("button", { name: "Link", exact: true }).click();
+ let popup = page.getByRole("dialog", { name: "Web link", exact: true });
+ const uri = "https://example.test/private-unfinished-link";
+ await popup.getByLabel("URL", { exact: true }).fill(uri);
+ await expect
+ .poll(() =>
+ page.evaluate(() => window.__journalFixture.status().linkDraftDirty),
+ )
+ .toBe(true);
+ await popup
+ .getByRole("button", { name: "Lock Journal", exact: true })
+ .click();
+ await expect(
+ page.getByRole("heading", { name: "Journal is locked" }),
+ ).toBeVisible();
+ await expect(editor).toHaveCount(0);
+ await expect(popup).toHaveCount(0);
+ expect(await page.locator("body").textContent()).not.toContain(uri);
+ expect(
+ await page
+ .locator("input")
+ .evaluateAll((inputs) => inputs.map((input) => input.value)),
+ ).not.toContain(uri);
+ await unlock("Wrong synthetic recovery phrase");
+ await expect(page.getByRole("alert")).toContainText(
+ "Couldn’t unlock Journal",
+ );
+ expect(await page.locator("body").textContent()).not.toContain(uri);
+ await unlock();
+ popup = page.getByRole("dialog", { name: "Web link", exact: true });
+ await expect(popup.getByLabel("URL", { exact: true })).toHaveValue(uri);
+ await expect(
+ popup.getByRole("button", { name: "Apply link" }),
+ ).toBeEnabled();
+ const rect = await popup.boundingBox();
+ expect(rect.x).toBeGreaterThanOrEqual(0);
+ expect(rect.x + rect.width).toBeLessThanOrEqual(width);
+ await page.screenshot({
+ path: join(output, `recovered-${theme}-${width}.png`),
+ });
+ await popup.getByRole("button", { name: "Apply link" }).click();
+ await expect(popup).toHaveCount(0);
+ await expect(editor.locator("a")).toHaveAttribute("href", uri);
+ await expect(editor).toHaveText("Synthetic journal selection");
+ await expect
+ .poll(() => page.evaluate(() => window.__journalFixture.status().dirty))
+ .toBe(false);
+ await editor.locator("a").click({ position: { x: 3, y: 3 } });
+ await page.getByRole("button", { name: "Link", exact: true }).click();
+ await popup
+ .getByLabel("URL", { exact: true })
+ .fill("unfinished invalid URL");
+ await page.evaluate(() => window.__journalFixture.lock());
+ await expect(
+ page.getByRole("heading", { name: "Journal is locked" }),
+ ).toBeVisible();
+ await unlock();
+ await expect(popup.getByLabel("URL", { exact: true })).toHaveValue(
+ "unfinished invalid URL",
+ );
+ await popup.getByRole("button", { name: "Cancel", exact: true }).click();
+ await expect(popup).toHaveCount(0);
+ await expect(editor.locator("a")).toHaveAttribute("href", uri);
+ await page.getByRole("button", { name: "Link", exact: true }).click();
+ await popup
+ .getByLabel("URL", { exact: true })
+ .fill("https://example.test/away-draft");
+ await page.clock.install();
+ await page.evaluate(() => window.__journalFixture.setAway(true));
+ await page.clock.fastForward(60001);
+ await expect(
+ page.getByRole("heading", { name: "Journal is locked" }),
+ ).toBeVisible();
+ expect(await page.locator("body").textContent()).not.toContain(
+ "https://example.test/away-draft",
+ );
+ await page.evaluate(() => window.__journalFixture.setAway(false));
+ await unlock();
+ await expect(popup.getByLabel("URL", { exact: true })).toHaveValue(
+ "https://example.test/away-draft",
+ );
+ await popup.getByRole("button", { name: "Cancel", exact: true }).click();
+ await page.reload();
+ await unlock();
+ await expect(editor.locator("a")).toHaveAttribute("href", uri);
+ await expect(popup).toHaveCount(0);
+ expect(errors).toEqual([]);
+ results.push({
+ theme,
+ width,
+ manualLock: true,
+ wrongProofHidden: true,
+ recoveredURL: true,
+ selectionApply: true,
+ invalidEditLockCancel: true,
+ reload: true,
+ errors,
+ });
+ await context.close();
+ }
+ await writeFile(
+ join(output, "result.json"),
+ JSON.stringify(
+ {
+ authentication:
+ "Synthetic account session; real WASM, IndexedDB and Journal encryption. No account-server qualification.",
+ results,
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(JSON.stringify({ passed: results.length, output }));
+} finally {
+ await browser.close();
+}
diff --git a/web/tests/integration/journal-owner-close-browser.mjs b/web/tests/integration/journal-owner-close-browser.mjs
new file mode 100644
index 0000000..0d0156b
--- /dev/null
+++ b/web/tests/integration/journal-owner-close-browser.mjs
@@ -0,0 +1,81 @@
+// Disposable genuine Journal owner-lifetime regression; never imported by production.
+import { chromium, expect } from "@playwright/test";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { createHash } from "node:crypto";
+import { fileURLToPath } from "node:url";
+import { resolve } from "node:path";
+if (!process.argv.includes("--run")) throw new Error("Pass --run to create disposable local test profiles.");
+const web = fileURLToPath(new URL("../../", import.meta.url));
+const output = process.env.NOTES_UI_OUTPUT ?? resolve(web, "test-results/journal-owner-close-browser");
+const base = process.env.NOTES_PREVIEW_URL ?? "http://127.0.0.1:5179";
+const sources = ["src/ui/JournalView.tsx", "src/collections/journalVault.ts", "src/generated/wasm/orbyn_wasm_bg.wasm", "tests/integration/collections-browser.tsx", "tests/integration/journal-owner-close-browser.mjs"];
+const hashes = () => Promise.all(sources.map(async path => ({path, sha256:createHash("sha256").update(await readFile(resolve(web,path))).digest("hex")})));
+const before = await hashes(), runs = [];
+await mkdir(output,{recursive:true});
+const browser = await chromium.launch({channel:"chrome",headless:true});
+const phrase = "Synthetic owner-close browser phrase only";
+const marker = "Synthetic private Journal owner-close entry";
+try {
+ for (const [mode,theme] of [["creating","light"],["locked","dim"],["unlocking","dark"],["unlocked-form","light"]]) {
+ const context = await browser.newContext({viewport:{width:1160,height:850}}),page=await context.newPage(),errors=[];
+ page.on("pageerror", e=>errors.push(e.message));
+ try {
+ await context.addInitScript(theme=>localStorage.setItem("notes.theme",theme),theme);
+ await page.goto(`${base}/tests/integration/collections-browser.html?view=journal`);
+ await page.getByLabel("Passphrase",{exact:true}).fill(phrase);
+ await page.getByLabel("Confirm passphrase",{exact:true}).fill(phrase);
+ if(mode!=="creating") {
+ await page.getByRole("button",{name:"Lock Journal",exact:true}).click();
+ const editor=page.getByRole("textbox",{name:"Journal entry",exact:true});
+ await expect(editor).toBeVisible();
+ await editor.fill(marker);
+ await expect.poll(()=>page.evaluate(()=>{const s=window.__journalFixture.status();return !s.saving&&!s.dirty;})).toBe(true);
+ if(mode==="unlocked-form") {
+ await page.getByRole("button",{name:"Add a habit",exact:true}).click();
+ await page.getByRole("dialog").getByLabel("Name",{exact:true}).fill("Synthetic private unsubmitted habit");
+ } else {
+ await page.evaluate(()=>window.__journalFixture.lock());
+ await page.getByLabel("Passphrase",{exact:true}).fill(phrase);
+ if(mode==="unlocking") {
+ await page.evaluate(()=>{
+ let release;const wait=new Promise(resolve=>{release=resolve;});
+ const actual=crypto.subtle.decrypt.bind(crypto.subtle);
+ const gate={reached:false,done:false,release};window.__journalCloseCryptoGate=gate;
+ crypto.subtle.decrypt=async(...args)=>{if(!gate.reached){gate.reached=true;await wait;}try{return await actual(...args);}finally{gate.done=true;}};
+ });
+ await page.getByRole("button",{name:"Unlock Journal",exact:true}).click();
+ await expect.poll(()=>page.evaluate(()=>window.__journalCloseCryptoGate.reached)).toBe(true);
+ }
+ }
+ }
+ const beforeClose=await page.evaluate(()=>({phase:window.__journalFixture.status().phase,credentialNodes:document.querySelectorAll("input[type=password]").length,privateEntry:!!document.querySelector('[aria-label="Journal entry"]')}));
+ const immediate=await page.evaluate(()=>{
+ window.__collectionsFixture.close();
+ return {phase:window.__journalFixture.status().phase,credentialNodes:document.querySelectorAll("input[type=password]").length,privateEntry:!!document.querySelector('[aria-label="Journal entry"]'),privateHabit:!!document.querySelector(".journal-form"),dialogs:document.querySelectorAll("dialog").length};
+ });
+ expect(immediate).toEqual({phase:"revoked",credentialNodes:0,privateEntry:false,privateHabit:false,dialogs:0});
+ if(mode==="unlocking") {
+ await page.evaluate(()=>window.__journalCloseCryptoGate.release());
+ await expect.poll(()=>page.evaluate(()=>window.__journalCloseCryptoGate.done)).toBe(true);
+ }
+ await expect(page.getByRole("heading",{name:"Journal access changed",exact:true})).toBeVisible();
+ await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
+ await expect(page.getByRole("textbox",{name:"Journal entry",exact:true})).toHaveCount(0);
+ await expect(page.locator('input[type="password"]')).toHaveCount(0);
+ expect(await page.locator("body").textContent()).not.toContain(marker);
+ const closeGuard=await page.evaluate(async()=>(await import("/src/desktop/closeGuard.ts")).checkClose());
+ expect(closeGuard).toBe("allowed");
+ if(mode==="unlocked-form") {
+ await page.reload();
+ await page.getByLabel("Passphrase",{exact:true}).fill(phrase);
+ await page.getByRole("button",{name:"Unlock Journal",exact:true}).click();
+ await expect(page.getByRole("textbox",{name:"Journal entry",exact:true})).toContainText(marker);
+ }
+ expect(errors).toEqual([]);
+ runs.push({mode,theme,beforeClose,immediate,closeGuard,pendingCryptoReleased:mode==="unlocking",savedEntryReopened:mode==="unlocked-form",errors});
+ } finally {await context.close();}
+ }
+ const after=await hashes();expect(after).toEqual(before);
+ await writeFile(resolve(output,"result.json"),JSON.stringify({qualification:"Real local WASM/IDB, genuine Collections close and JournalVault/JournalView; synthetic disposable profiles; no native physical-input or server-account claim",before,after,runs},null,2));
+ console.log(JSON.stringify({contexts:runs.length,sourceDrift:false,errors:0}));
+} finally {await browser.close();}