Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/tests/integration/clips-browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Encrypted Clips test harness</title></head><body><div id="fixture-label">Test harness — synthetic account session; real local WASM encryption and IndexedDB. No account-server qualification.</div><div id="root"></div><script type="module" src="./clips-browser.tsx"></script></body></html>
20 changes: 20 additions & 0 deletions web/tests/integration/clips-browser.mjs
Original file line number Diff line number Diff line change
@@ -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='<script>window.evil = true</script>\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();}
63 changes: 63 additions & 0 deletions web/tests/integration/clips-browser.tsx
Original file line number Diff line number Diff line change
@@ -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<DeviceRecord>("devices", identity.email))) {
const wrapKey = await newWrappingKey(),
secret = await wrappingSecret(wrapKey),
keys = DeviceKeys.create();
try {
await mutateRecord<DeviceRecord>("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(
<Workspace
store={notes}
library={notes.library}
session={identity}
onSignOut={async () => {
notes.close();
}}
/>,
);
}
void start().catch((error) => {
document.getElementById("root")!.textContent =
error instanceof Error ? error.message : "Fixture failed";
});
1 change: 1 addition & 0 deletions web/tests/integration/collections-browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Encrypted collections test harness</title></head><body><div id="fixture-label">Test harness — synthetic account session; real local WASM encryption and IndexedDB. No account-server qualification.</div><div id="root"></div><script type="module" src="./collections-browser.tsx"></script></body></html>
77 changes: 77 additions & 0 deletions web/tests/integration/collections-browser.tsx
Original file line number Diff line number Diff line change
@@ -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<DeviceRecord>("devices", identity.email))) {
const wrapKey = await newWrappingKey(),
secret = await wrappingSecret(wrapKey),
keys = DeviceKeys.create();
try {
await mutateRecord<DeviceRecord>("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(
<JournalView vault={vault} />,
);
} else
createRoot(document.getElementById("root")!).render(
<StudyView store={store} />,
);
}
void start().catch((error) => {
document.getElementById("root")!.textContent =
error instanceof Error ? error.message : "Fixture failed";
});
169 changes: 169 additions & 0 deletions web/tests/integration/journal-link-browser.mjs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading
Loading