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
574 changes: 574 additions & 0 deletions web/tests/integration/notes-source-motion-browser.mjs

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions web/tests/integration/source-opening-browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { chromium, expect } from "@playwright/test";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
const base = new URL(process.env.NOTES_PREVIEW_URL ?? "http://127.0.0.1:5179");
if (!["localhost", "127.0.0.1", "[::1]"].includes(base.hostname))
throw new Error("Source-opening proof requires a loopback preview.");
const source = new URL("/tests/integration/source-proof.html", base).href;
const output =
process.env.NOTES_UI_OUTPUT ?? "test-results/source-opening-browser";
const browser = await chromium.launch({ channel: "chrome", headless: true });
try {
await mkdir(output, { recursive: true });
const context = await browser.newContext();
const requests = [],
errors = [];
context.on("request", (request) => {
if (request.url() === source) requests.push(request.url());
});
await context.addInitScript(() => localStorage.setItem("notes.editor.v1", JSON.stringify({textSize:"large",lineWidth:"wide",markdown:true,spellcheck:true})));
const page = await context.newPage();
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(new URL("/tests/integration/clips-browser.html", base).href);
await page.getByRole("button", { name: "Clips", exact: true }).click();
await page
.getByRole("button", { name: "Save a link", exact: true })
.first()
.click();
const dialog = page.getByRole("dialog", { name: "Save a link", exact: true });
await dialog.getByLabel("Clip URL").fill(source);
await dialog.getByLabel("Title (optional)").fill("Controlled saved source");
await dialog.getByLabel("Selected text (optional)").fill("Controlled pasted text for reader preferences.");
await dialog.getByRole("button", { name: "Save link", exact: true }).click();
const notice = page.getByRole("status").filter({ hasText: "Text clip saved" });
await expect(notice).toBeVisible();
await notice.getByRole("button", { name: "Open clip", exact: true }).click();
expect(requests).toHaveLength(0);
await expect(page.locator(".clip-text")).toHaveCSS("font-size", "16px");
await expect(page.locator(".clip-reader-content")).toHaveCSS("max-width", "880px");
const original = page.getByRole("link", {
name: "Open original",
exact: true,
});
await expect(original).toHaveAttribute("rel", "noopener noreferrer");
const pending = page.waitForEvent("popup");
await original.click();
const popup = await pending;
await expect(
popup.getByRole("heading", { name: "Controlled clip source", exact: true }),
).toBeVisible();
expect(popup.url()).toBe(source);
expect(await popup.evaluate(() => window.opener === null)).toBe(true);
expect(requests).toHaveLength(1);
await expect(
page.getByRole("button", { name: "Mark read", exact: true }),
).toBeVisible();
expect(errors).toEqual([]);
await popup.close();
await writeFile(
join(output, "result.json"),
JSON.stringify(
{
qualified:
"Actual browser source opening from a real-WASM/IndexedDB Clip under a synthetic account session. No native OS handoff or account-server qualification.",
noFetchOnSave: true,
oneExplicitLoopbackHandoff: true,
openerNull: true,
stillUnread: true,
pageErrors: errors,
},
null,
2,
),
);
console.log(
"Controlled browser source opening passed; no prefetch, opener null, clip remains unread.",
);
} finally {
await browser.close();
}
11 changes: 11 additions & 0 deletions web/tests/integration/source-proof.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Controlled clip source</title>
</head>
<body>
<h1>Controlled clip source</h1>
<p>This local test page is opened only by the explicit source action.</p>
</body>
</html>
45 changes: 45 additions & 0 deletions web/tests/integration/study-browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { chromium, expect } from "@playwright/test";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";

// Run with the local Vite server. Auth is a synthetic session; encryption/storage
// are the actual production adapters. This does not qualify account-server entry.
const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL ?? "chrome", headless: true });
const output = process.env.NOTES_UI_OUTPUT ?? "test-results/study-browser";
try {
const page = await browser.newPage({ viewport: { width: 1160, height: 760 } });
const errors = [];
page.on("pageerror", error => errors.push(error.message));
await page.goto(`${process.env.NOTES_PREVIEW_URL ?? "http://127.0.0.1:5179"}/tests/integration/collections-browser.html`);
await expect(page.getByText("No decks yet", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "New deck", exact: true }).first().click();
await page.getByRole("dialog", { name: "New deck" }).getByLabel("Name").fill("Encrypted browser study");
await page.getByRole("button", { name: "Create deck", exact: true }).click();
await expect(page.getByRole("heading", { name: "Encrypted browser study" })).toBeVisible();
await page.getByRole("button", { name: "Add card", exact: true }).first().click();
const dialog = page.getByRole("dialog", { name: "Add card" });
await dialog.getByLabel("Front", { exact: true }).fill("Synthetic question");
await dialog.getByLabel("Back", { exact: true }).fill("Synthetic answer");
await dialog.getByRole("button", { name: "Add card", exact: true }).click();
await expect(dialog).toHaveCount(0);
const before = await page.evaluate(() => window.__collectionsFixture.read());
if (before.study.cards.length !== 1) throw new Error("Card not committed");
await page.reload();
await expect(page.getByRole("button", { name: "Encrypted browser study", exact: true })).toBeVisible();
await page.getByRole("button", { name: /^Study 1 card$/ }).click();
await page.getByRole("button", { name: "Show answer", exact: true }).click();
await page.getByRole("button", { name: /^Good/ }).click();
await expect(page.getByRole("heading", { name: "Done for today" })).toBeVisible();
const after = await page.evaluate(() => window.__collectionsFixture.read());
if (after.study.events.length !== 1 || after.study.cards[0].schedule.state !== "review") throw new Error("Grade not persisted");
await page.reload();
await expect(page.getByRole("button", { name: "Encrypted browser study", exact: true })).toBeVisible();
const persisted = await page.evaluate(() => window.__collectionsFixture.read());
if (persisted.study.events.length !== 1) throw new Error("Review lost on reopen");
await page.evaluate(() => window.__collectionsFixture.erase());
await expect(page.getByText("No decks yet", { exact: true })).toBeVisible();
if (errors.length) throw new Error(errors.join("\n"));
await mkdir(output, { recursive: true });
await writeFile(join(output, "result.json"), JSON.stringify({ harness: "Synthetic account session; real WASM + IndexedDB; no account-server qualification", passed: ["create deck", "create card", "reopen persisted card", "grade changes schedule", "reopen review event", "erase invalidates view"], errors }, null, 2));
console.log("Encrypted Study browser journey passed: create, reopen, grade, reopen, erase.");
} finally { await browser.close(); }
206 changes: 206 additions & 0 deletions web/tests/integration/study-motion-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { chromium, expect } from "@playwright/test";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { createHash } from "node:crypto";

const output = process.env.NOTES_UI_OUTPUT ?? "test-results/study-motion-audit";
const preview = process.env.NOTES_PREVIEW_URL ?? "http://127.0.0.1:5179";
await mkdir(output, { recursive: true });
const sourcePaths = ["src/ui/StudyView.tsx", "src/styles/study.css"];
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
const sourceBefore = await Promise.all(
sourcePaths.map(async (path) => ({
path,
sha256: hash(await readFile(path)),
})),
);
const browser = await chromium.launch({ channel: "chrome", headless: true });
const results = [];
try {
for (const reduced of [false, true]) {
const context = await browser.newContext({
reducedMotion: reduced ? "reduce" : "no-preference",
viewport: { width: 1160, height: 850 },
});
try {
const page = await context.newPage(),
errors = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(
`${preview}/tests/integration/study-settings-browser.html`,
);
await expect(
page.getByRole("heading", { name: "All notes", exact: true }),
).toBeVisible();
await expect
.poll(() =>
page.evaluate(async () =>
(await import("/src/desktop/closeGuard.ts")).checkClose(),
),
)
.toBe("allowed");
await page.evaluate(() => {
window.__studyMotionAudit = [];
const observe = (element, phase) => {
if (!(element instanceof Element)) return;
const css = getComputedStyle(element);
window.__studyMotionAudit.push({
phase,
at: performance.now(),
kind: element.classList.contains("study-toast") ? "toast" : "card",
label: element.getAttribute("aria-label"),
duration: css.animationDuration,
animation: css.animationName,
frames: element.getAnimations().flatMap((animation) =>
animation.effect
.getKeyframes()
.map(({ opacity, transform, offset }) => ({
opacity,
transform,
offset,
})),
),
});
};
new MutationObserver((records) => {
for (const record of records) {
for (const [nodes, phase] of [
[record.addedNodes, "added"],
[record.removedNodes, "removed"],
]) {
for (const node of nodes) {
if (!(node instanceof Element)) continue;
if (node.matches(".study-flash,.study-toast"))
observe(node, phase);
for (const element of node.querySelectorAll(
".study-flash,.study-toast",
))
observe(element, phase);
}
}
}
}).observe(document.getElementById("root"), {
childList: true,
subtree: true,
});
});
await page.getByRole("button", { name: "Study", exact: true }).click();
await page.getByRole("button", { name: /^Study 4 cards$/ }).click();
await expect(
page.getByRole("article", { name: "Card question" }),
).toBeVisible();
await page
.getByRole("button", { name: "Show answer", exact: true })
.click();
await expect(
page.getByRole("article", { name: "Card answer" }),
).toBeVisible();
const good = page.getByRole("button", { name: /^Good/ });
await good.focus();
await page.keyboard.press("Tab");
await page.keyboard.press("Shift+Tab");
const gradeStyle = await good.evaluate((element) => {
const css = getComputedStyle(element);
return {
transitionDuration: css.transitionDuration,
transitionProperty: css.transitionProperty,
transitionTimingFunction: css.transitionTimingFunction,
outlineWidth: css.outlineWidth,
outlineStyle: css.outlineStyle,
};
});
expect(gradeStyle.outlineStyle).toBe("solid");
expect(gradeStyle.outlineWidth).toBe("2px");
const box = await good.boundingBox();
expect(box).toBeTruthy();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await good.evaluate((element) =>
Promise.all(
element
.getAnimations()
.map((animation) => animation.finished.catch(() => undefined)),
),
);
const pressedTransform = await good.evaluate(
(element) => getComputedStyle(element).transform,
);
expect(pressedTransform).toBe(
reduced ? "none" : "matrix(0.97, 0, 0, 0.97, 0, 0)",
);
await page.mouse.up();
await expect(
page.getByRole("article", { name: "Card question" }),
).toBeVisible();
await expect(page.locator(".study-toast")).toBeVisible();
// Wait on the actual UI disappearance, rather than using a guessed sleep.
await expect(page.locator(".study-toast")).toHaveCount(0, {
timeout: 7000,
});
const timeline = await page.evaluate(() => window.__studyMotionAudit);
const entered = timeline.filter(
(item) => item.phase === "added" && item.kind === "card",
);
expect(entered.length).toBeGreaterThanOrEqual(3);
expect(entered.some((item) => item.label === "Card answer")).toBe(true);
if (!reduced)
expect(
entered.every(
(item) =>
item.animation === "study-answer-in" && item.duration === "0.15s",
),
).toBe(true);
else
expect(
entered.every((item) =>
item.frames.every(
(frame) => !frame.transform || frame.transform === "none",
),
),
).toBe(true);
const toastEnter = timeline.find(
(item) => item.phase === "added" && item.kind === "toast",
),
toastExit = timeline.find(
(item) => item.phase === "removed" && item.kind === "toast",
);
expect(toastEnter).toBeTruthy();
expect(toastExit.at - toastEnter.at).toBeGreaterThanOrEqual(4900);
expect(errors).toEqual([]);
results.push({
reduced,
gradeStyle,
pressedTransform,
timeline,
toastLifetimeMs: toastExit.at - toastEnter.at,
errors,
});
} finally {
await context.close();
}
}
const sourceAfter = await Promise.all(
sourcePaths.map(async (path) => ({
path,
sha256: hash(await readFile(path)),
})),
);
expect(sourceAfter).toEqual(sourceBefore);
await writeFile(
`${output}/result.json`,
JSON.stringify(
{
qualified:
"Disposable synthetic account; genuine App/WASM/IndexedDB. Browser presentation observations only; no native input, backend authentication or whole-app frame-rate claim.",
source: sourceBefore,
results,
},
null,
2,
),
);
console.log(
"Study motion baseline passed: keyed card/answer entry, grade feedback styles, actual toast expiry and reduced-motion paths observed.",
);
} finally {
await browser.close();
}
Loading
Loading