| 1 | import assert from "node:assert/strict"; |
| 2 | import { mkdir } from "node:fs/promises"; |
| 3 | import path from "node:path"; |
| 4 | import { fileURLToPath } from "node:url"; |
| 5 | import { createServer } from "vite"; |
| 6 | |
| 7 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 8 | process.env.PLAYWRIGHT_BROWSERS_PATH ??= path.join(root, ".pw-browsers"); |
| 9 | const { chromium } = await import("playwright"); |
| 10 | const evidence = process.env.REASONIX_LOADING_EVIDENCE ?? "/tmp/reasonix-session-loading-evidence"; |
| 11 | const server = await createServer({ root, logLevel: "error", server: { host: "127.0.0.1", port: 0 } }); |
| 12 | await server.listen(); |
| 13 | let browser; |
| 14 | try { |
| 15 | browser = await chromium.launch({ headless: true }); |
| 16 | const page = await browser.newPage({ viewport: { width: 1280, height: 800 }, reducedMotion: "reduce" }); |
| 17 | const errors = []; |
| 18 | page.on("pageerror", error => errors.push(error.message)); |
| 19 | const address = server.httpServer.address(); |
| 20 | await page.goto(`http://127.0.0.1:${address.port}/bench/session-loading.html`); |
| 21 | await page.waitForFunction(() => typeof window.paintLoading === "function"); |
| 22 | await page.clock.install(); |
| 23 | await page.clock.pauseAt(new Date(Date.now() + 1000)); |
| 24 | const paint = input => page.evaluate(input => window.paintLoading(input), input); |
| 25 | const feedback = page.locator(".session-loading-indicator"); |
| 26 | const userNode = page.locator('.chat-node[data-chat-kind="user"]'); |
| 27 | await mkdir(evidence, { recursive: true }); |
| 28 | for (const surface of ["local", "remote", "transcript"]) { |
| 29 | const input = { surface, identity: `${surface}-A`, phase: "loading" }; |
| 30 | await paint(input); |
| 31 | await page.clock.runFor(249); |
| 32 | assert.equal(await feedback.count(), 0, `${surface}: no loading UI for fast requests`); |
| 33 | assert.equal(await page.locator(".session-recovery,.session-recovery-placeholder").count(), 0); |
| 34 | await page.clock.runFor(1); |
| 35 | assert.equal(await feedback.count(), 1, `${surface}: one delayed indicator`); |
| 36 | const mainBefore = await page.locator(surface === "transcript" ? ".chat-transcript" : ".main").boundingBox(); |
| 37 | const shortText = await feedback.innerText(); |
| 38 | await page.clock.runFor(1750); |
| 39 | assert.notEqual(await feedback.innerText(), shortText); |
| 40 | assert.deepEqual(await page.locator(surface === "transcript" ? ".chat-transcript" : ".main").boundingBox(), mainBefore, |
| 41 | "loading detail does not change the pane geometry"); |
| 42 | assert.equal(await feedback.evaluate(el => getComputedStyle(el).position), "absolute"); |
| 43 | assert.equal(await feedback.locator("svg").evaluate(el => getComputedStyle(el).animationName), "none", "respects reduced motion"); |
| 44 | await page.screenshot({ path: path.join(evidence, `${surface}-slow.png`) }); |
| 45 | |
| 46 | await paint({ ...input, phase: "ready" }); |
| 47 | await page.clock.runFor(32); |
| 48 | assert.equal(await feedback.count(), 0); |
| 49 | assert.match(await userNode.innerText(), new RegExp(`Content ${input.identity}`)); |
| 50 | const nodeBefore = await userNode.boundingBox(); |
| 51 | await userNode.evaluate(el => { window.retainedLoadingNode = el; }); |
| 52 | await paint({ ...input, cached: true }); |
| 53 | await page.clock.runFor(2000); |
| 54 | assert.equal(await userNode.evaluate(el => el === window.retainedLoadingNode), true); |
| 55 | assert.deepEqual(await userNode.boundingBox(), nodeBefore, "cached content never shifts for loading feedback"); |
| 56 | assert.equal(await feedback.count(), 1); |
| 57 | await page.screenshot({ path: path.join(evidence, `${surface}-cached.png`) }); |
| 58 | |
| 59 | await paint({ ...input, identity: `${surface}-B` }); |
| 60 | assert.equal(await feedback.count(), 0, "switching hides the previous session's pending indicator"); |
| 61 | assert.equal(await page.locator(".chat-node").count(), 0, "the previous session never appears under a new identity"); |
| 62 | await page.clock.runFor(100); |
| 63 | await paint({ ...input, identity: `${surface}-B`, phase: "ready" }); |
| 64 | await page.clock.runFor(2500); |
| 65 | assert.equal(await feedback.count(), 0, "old deadlines cannot appear after completion"); |
| 66 | if (surface !== "transcript") { |
| 67 | await paint({ ...input, phase: "error" }); |
| 68 | assert.equal(await page.locator('.session-recovery[role="alert"]').count(), 1); |
| 69 | assert.equal(await feedback.count(), 0); |
| 70 | } |
| 71 | console.log(`PASS ${surface}: delayed feedback, immediate readiness, stable cached layout, session isolation and recovery`); |
| 72 | } |
| 73 | assert.deepEqual(errors, []); |
| 74 | console.log(`Browser evidence: ${evidence}`); |
| 75 | } finally { |
| 76 | await browser?.close(); |
| 77 | await server.close(); |
| 78 | } |
| 79 |