| 1 | import assert from "node:assert/strict"; |
| 2 | import { mkdir, mkdtemp, copyFile, rm, writeFile } from "node:fs/promises"; |
| 3 | import { createRequire } from "node:module"; |
| 4 | import { tmpdir } from "node:os"; |
| 5 | import path from "node:path"; |
| 6 | import { fileURLToPath } from "node:url"; |
| 7 | import { createServer } from "vite"; |
| 8 | import { chromium, _electron } from "playwright"; |
| 9 | |
| 10 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 11 | const electron = process.argv.includes("--electron"); |
| 12 | const evidence = process.env.REASONIX_OUTLINE_EVIDENCE ?? path.join(tmpdir(), "reasonix-turn-outline-evidence"); |
| 13 | await mkdir(evidence, { recursive: true }); |
| 14 | const host = await mkdtemp(path.join(tmpdir(), "reasonix-outline-host-")); |
| 15 | const server = await createServer({ root, server: { host: "127.0.0.1", port: 0 }, clearScreen: false }); await server.listen(); |
| 16 | const url = new URL("bench/turn-outline.html", server.resolvedUrls.local[0]).href; |
| 17 | let browser, app; |
| 18 | const report = { host: electron ? "electron" : "chromium", complete: false, errors: [] }; |
| 19 | try { |
| 20 | let page; |
| 21 | if (electron) { |
| 22 | await copyFile(path.join(root, "bench/transcript-layout-electron.cjs"), path.join(host, "main.cjs")); |
| 23 | app = await _electron.launch({ executablePath: createRequire(path.join(root, "../electron/package.json"))("electron"), args: [path.join(host, "main.cjs")], env: { ...process.env, REASONIX_LAYOUT_URL: url } }); |
| 24 | page = await app.firstWindow(); |
| 25 | } else { browser = await chromium.launch({ headless: true }); page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); await page.goto(url); } |
| 26 | page.on("pageerror", error => report.errors.push(error.message)); |
| 27 | await page.waitForFunction(() => Boolean(window.turnOutlineFixture)); |
| 28 | const rail = page.locator(".dsh-TurnNavigator-frame"); await rail.waitFor(); |
| 29 | const inspect = () => page.evaluate(() => window.turnOutlineFixture.inspect()); |
| 30 | const go = async key => { await rail.focus(); await rail.press(key); await rail.press("Enter"); }; |
| 31 | await go("Home"); await page.locator('[data-chat-anchor-key="m:u1"]').waitFor(); |
| 32 | const scroller = page.locator(".dsh-TurnNavigator-scroller"); |
| 33 | await scroller.evaluate(el => el.scrollTo({ top: 10000 * 10 })); |
| 34 | await page.locator('[data-nav-turn="m:u10001"]').waitFor(); |
| 35 | await page.locator('[data-nav-turn="m:u10001"]').focus(); await page.keyboard.press("Enter"); |
| 36 | await page.locator('[data-chat-anchor-key="m:u10001"]').waitFor(); |
| 37 | await go("End"); await page.locator('[data-chat-anchor-key="m:u25000"]').waitFor(); |
| 38 | assert.equal((await inspect()).reads.length, 3); |
| 39 | assert.ok(await page.locator("[data-nav-turn]").count() <= 52); |
| 40 | assert.ok((await inspect()).cacheEntries <= 768); |
| 41 | assert.ok((await inspect()).stateItems <= 96); |
| 42 | await rail.focus(); await rail.press("Home"); |
| 43 | await page.locator('[data-nav-turn="m:u1"]').waitFor(); |
| 44 | // Pointer input belongs to the rail column; mark buttons are keyboard-only. |
| 45 | await rail.hover({ position: { x: 14, y: 6 } }); |
| 46 | await page.getByRole("tooltip").filter({ hasText: "Question 1" }).waitFor(); |
| 47 | await page.setViewportSize({ width: 700, height: 800 }); |
| 48 | for (const theme of ["light", "dark"]) { |
| 49 | await page.evaluate(theme => { document.documentElement.dataset.theme = theme; }, theme); |
| 50 | assert.ok(await page.locator("[data-nav-turn]").count() <= 52); |
| 51 | await page.screenshot({ path: path.join(evidence, `${report.host}-${theme}-narrow.png`) }); |
| 52 | } |
| 53 | await page.screenshot({ path: path.join(evidence, `${report.host}-outline.png`) }); |
| 54 | // Native browser wheel input must cancel a delayed target's visible commit. |
| 55 | await page.evaluate(() => window.turnOutlineFixture.delay()); await go("Home"); |
| 56 | await page.waitForFunction(() => window.turnOutlineFixture.inspect().reads.length === 4); |
| 57 | await page.locator(".chat-flow-scroll").evaluate(el => { window.outlineWheelReceived = false; el.addEventListener("wheel", () => { window.outlineWheelReceived = true; }, { once: true }); }); |
| 58 | await page.locator(".chat-flow-scroll").hover(); await page.mouse.wheel(0, -80); |
| 59 | await page.waitForFunction(() => window.outlineWheelReceived === true); |
| 60 | await page.evaluate(() => window.turnOutlineFixture.resume()); |
| 61 | await page.waitForFunction(() => !document.querySelector('[aria-busy="true"][data-nav-turn]')); |
| 62 | assert.deepEqual((await inspect()).users, ["u25000"]); |
| 63 | await page.evaluate(() => window.turnOutlineFixture.delay()); await go("Home"); |
| 64 | await page.waitForFunction(() => window.turnOutlineFixture.inspect().reads.length === 5); |
| 65 | await page.evaluate(() => window.turnOutlineFixture.switchSession()); |
| 66 | await page.evaluate(() => window.turnOutlineFixture.resume()); |
| 67 | await page.locator('[data-chat-anchor-key="m:u25000"]').waitFor(); |
| 68 | assert.deepEqual((await inspect()).users, ["u25000"]); |
| 69 | await go("Home"); |
| 70 | await page.locator('[data-chat-anchor-key="m:u1"]').waitFor(); |
| 71 | const beforeLatest = await inspect(); |
| 72 | await page.evaluate(() => window.turnOutlineFixture.delay()); |
| 73 | await page.locator(".chat-to-bottom").click(); |
| 74 | await page.waitForFunction(count => window.turnOutlineFixture.inspect().reads.length === count + 1, beforeLatest.reads.length); |
| 75 | // A later click on a mounted turn must cancel the pending return-to-latest. |
| 76 | await go("Home"); |
| 77 | await page.evaluate(() => window.turnOutlineFixture.resume()); |
| 78 | await page.waitForFunction(count => window.turnOutlineFixture.inspect().completedReads === count + 1, beforeLatest.completedReads); |
| 79 | assert.deepEqual((await inspect()).users, beforeLatest.users); |
| 80 | assert.equal(await page.locator(".chat-flow-scroll").getAttribute("data-scroll-mode"), "reader"); |
| 81 | await page.locator(".chat-to-bottom").click(); |
| 82 | await page.locator('[data-chat-anchor-key="m:u25000"]').waitFor(); |
| 83 | await page.waitForFunction(() => document.querySelector(".chat-flow-scroll")?.getAttribute("data-scroll-mode") === "tail"); |
| 84 | report.result = await inspect(); report.complete = true; assert.deepEqual(report.errors, []); |
| 85 | } finally { |
| 86 | await writeFile(path.join(evidence, `${report.host}.json`), JSON.stringify(report, null, 2)); |
| 87 | await app?.close(); await browser?.close(); await server.close(); await rm(host, { recursive: true, force: true }); |
| 88 | } |
| 89 | console.log(JSON.stringify(report)); |
| 90 |