diff --git a/AGENTS.md b/AGENTS.md index eb8e961..bccff3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,3 +144,43 @@ node test.js - Add or extend APIs on **`window.LAMBS`** inside `index.html`. - Document JSON field changes here under **Using LAMBS** (`reportVersion` bumps if breaking). - Prefer **Copy JSON** / `lambsReportToJSON` over new DOM scraping hooks. + +--- + +## Cursor Cloud specific instructions + +LAMBS is a **zero-backend** product: one `index.html` file runs entirely in the browser. No database, API server, or Docker is required. + +### Verify logic (CI-equivalent) + +```bash +node test.js +``` + +Requires **Node.js only** — no `pnpm install`. CI uses Node 24; Node 22+ works locally. + +There is **no lint command** configured (no ESLint/Prettier in the repo). + +### Run the application + +Open `index.html` in a browser (`file://` or static server). For a headless UI smoke test covering **both** tabs (Single mAb Analysis + Multiple mAb Analysis with built-in example sequences): + +```bash +pnpm install +pnpm exec playwright install chromium # one-time per VM; not in update script +node scripts/verify-ui.mjs +``` + +This loads the Trastuzumab example on the Single tab, runs **Analyze**, then switches to Multiple mAb Analysis, loads the 4-sequence example CSV, runs **Cluster & Analyze**, and saves screenshots under `LAMBS_UI_OUT` (default `/opt/cursor/artifacts/screenshots`). + +Or serve statically: `python3 -m http.server 8080 --directory /workspace` → `http://localhost:8080/index.html`. + +### Optional dev dependencies + +For README screenshots: + +```bash +pnpm screenshots +``` + +Python 3 scripts under `scripts/` regenerate embedded germline/stats data in `index.html`; only needed when changing those datasets, not for normal dev/test. diff --git a/package.json b/package.json index 10352e3..d5cf271 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "LAMBS - Local Analysis of mAb Sequences", "scripts": { "release": "commit-and-tag-version --config ./scripts/version-updater/versionrc.cjs", - "screenshots": "node scripts/capture-readme-screenshots.mjs" + "screenshots": "node scripts/capture-readme-screenshots.mjs", + "verify-ui": "node scripts/verify-ui.mjs" }, "devDependencies": { "commit-and-tag-version": "12.7.1", diff --git a/scripts/verify-ui.mjs b/scripts/verify-ui.mjs new file mode 100644 index 0000000..c7ed219 --- /dev/null +++ b/scripts/verify-ui.mjs @@ -0,0 +1,103 @@ +/** + * Headless browser smoke test for LAMBS UI (Single + Multiple mAb Analysis tabs). + * + * Prerequisites: + * pnpm install + * pnpm exec playwright install chromium + * + * Usage: + * node scripts/verify-ui.mjs + * LAMBS_UI_OUT=/path/to/dir node scripts/verify-ui.mjs + */ +import { mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { chromium } from "playwright"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(__dirname, ".."); +const INDEX_URL = pathToFileURL(join(REPO_ROOT, "index.html")).href; +const OUT_DIR = process.env.LAMBS_UI_OUT || "/opt/cursor/artifacts/screenshots"; + +mkdirSync(OUT_DIR, { recursive: true }); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const browser = await chromium.launch({ headless: true }); + +try { + const page = await browser.newPage({ viewport: { width: 1200, height: 1220 } }); + await page.goto(INDEX_URL, { waitUntil: "domcontentloaded" }); + + // --- Single mAb Analysis --- + console.log("=== Single mAb Analysis tab ==="); + await page.evaluate(() => { + switchTab("single"); + loadExampleSingle(); + analyze(); + }); + await page + .locator("#results") + .getByText(/germline|VH|VL|liability/i) + .first() + .waitFor({ timeout: 15000 }); + + const singleApi = await page.evaluate(() => { + const r = LAMBS.analyzeMabReportFromRaw( + document.getElementById("vh-input").value, + document.getElementById("vl-input").value + ); + if (!r.ok) return { ok: false, errors: r.errors }; + return { + ok: true, + kind: r.report.kind, + vhVGene: r.report.chains.vh.variable.germline.vGene?.gene, + vlVGene: r.report.chains.vl.variable.germline.vGene?.gene, + }; + }); + console.log(JSON.stringify(singleApi, null, 2)); + assert(singleApi.ok && singleApi.kind === "single-mab", "Single tab analysis failed"); + + const singleShot = join(OUT_DIR, "lambs-single-analysis-demo.png"); + await page.screenshot({ path: singleShot, fullPage: false }); + console.log("Screenshot:", singleShot); + + // --- Multiple mAb Analysis --- + console.log("\n=== Multiple mAb Analysis tab ==="); + await page.evaluate(() => { + switchTab("multi"); + loadExampleCSV(); + runMultiAnalysis(); + }); + await page + .locator("#multi-results") + .getByText(/cluster|alignment|consensus/i) + .first() + .waitFor({ timeout: 15000 }); + + const multiApi = await page.evaluate(() => ({ + basketCount: typeof basket !== "undefined" ? basket.length : 0, + clusterReport: LAMBS.lastClusterReport + ? { + kind: LAMBS.lastClusterReport.kind, + inputCount: LAMBS.lastClusterReport.inputCount, + clusterCount: LAMBS.lastClusterReport.clusterCount, + firstClusterSize: LAMBS.lastClusterReport.clusters?.[0]?.size, + } + : null, + })); + console.log(JSON.stringify(multiApi, null, 2)); + assert(multiApi.basketCount === 4, "Expected 4 example sequences in basket"); + assert(multiApi.clusterReport?.kind === "cluster", "Multi tab cluster report missing"); + assert(multiApi.clusterReport.clusterCount >= 1, "Expected at least one cluster"); + + const multiShot = join(OUT_DIR, "lambs-multiple-analysis-demo.png"); + await page.screenshot({ path: multiShot, fullPage: false }); + console.log("Screenshot:", multiShot); + + console.log("\nUI verification passed (Single + Multiple mAb Analysis)."); +} finally { + await browser.close(); +}