From cbd2c65f7c8252f21c59d096bca5dcbe86ae6cf1 Mon Sep 17 00:00:00 2001 From: Luan Nguyen Date: Wed, 11 Nov 2026 21:38:51 +1100 Subject: [PATCH] test(journal): cover private content and owner-close journeys --- web/tests/integration/clips-browser.html | 1 + web/tests/integration/clips-browser.mjs | 20 +++ web/tests/integration/clips-browser.tsx | 63 +++++++ .../integration/collections-browser.html | 1 + web/tests/integration/collections-browser.tsx | 77 ++++++++ .../integration/journal-link-browser.mjs | 169 ++++++++++++++++++ .../journal-owner-close-browser.mjs | 81 +++++++++ 7 files changed, 412 insertions(+) create mode 100644 web/tests/integration/clips-browser.html create mode 100644 web/tests/integration/clips-browser.mjs create mode 100644 web/tests/integration/clips-browser.tsx create mode 100644 web/tests/integration/collections-browser.html create mode 100644 web/tests/integration/collections-browser.tsx create mode 100644 web/tests/integration/journal-link-browser.mjs create mode 100644 web/tests/integration/journal-owner-close-browser.mjs 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 harness
Test 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 harness
Test 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();}