| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | import assert from "node:assert/strict"; |
| 4 | import { copyFile, mkdtemp, rm } from "node:fs/promises"; |
| 5 | import { createRequire } from "node:module"; |
| 6 | import { tmpdir } from "node:os"; |
| 7 | import path from "node:path"; |
| 8 | import { fileURLToPath } from "node:url"; |
| 9 | import { createServer } from "vite"; |
| 10 | |
| 11 | const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 12 | process.env.PLAYWRIGHT_BROWSERS_PATH = !process.env.PLAYWRIGHT_BROWSERS_PATH || process.env.PLAYWRIGHT_BROWSERS_PATH === ".pw-browsers" |
| 13 | ? path.join(frontendDir, ".pw-browsers") |
| 14 | : process.env.PLAYWRIGHT_BROWSERS_PATH; |
| 15 | const { chromium, _electron } = await import("playwright"); |
| 16 | const electronEngine = process.argv.includes("--electron"); |
| 17 | const electronHome = electronEngine ? await mkdtemp(path.join(tmpdir(), "reasonix-popover-electron-")) : ""; |
| 18 | const server = await createServer({ |
| 19 | root: frontendDir, |
| 20 | server: { host: "127.0.0.1", port: 0, hmr: false }, |
| 21 | logLevel: "error", |
| 22 | plugins: [{ |
| 23 | name: "popover-test-favicon", |
| 24 | configureServer(vite) { |
| 25 | vite.middlewares.use((req, res, next) => { |
| 26 | if (req.url !== "/favicon.ico") return next(); |
| 27 | res.statusCode = 204; |
| 28 | res.end(); |
| 29 | }); |
| 30 | }, |
| 31 | }], |
| 32 | }); |
| 33 | await server.listen(); |
| 34 | |
| 35 | let browser; |
| 36 | let electronApp; |
| 37 | try { |
| 38 | const address = server.httpServer.address(); |
| 39 | if (!address || typeof address === "string") throw new Error("popover test server did not expose a port"); |
| 40 | const url = `http://127.0.0.1:${address.port}/?mock=demo&bench=1`; |
| 41 | let page; |
| 42 | if (electronEngine) { |
| 43 | const main = path.join(electronHome, "main.cjs"); |
| 44 | await copyFile(path.join(frontendDir, "bench/transcript-layout-electron.cjs"), main); |
| 45 | const electronRequire = createRequire(path.join(frontendDir, "../electron/package.json")); |
| 46 | electronApp = await _electron.launch({ |
| 47 | executablePath: electronRequire("electron"), args: [main], |
| 48 | env: { ...process.env, REASONIX_LAYOUT_URL: url }, |
| 49 | }); |
| 50 | page = await electronApp.firstWindow(); |
| 51 | } else { |
| 52 | browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_EXECUTABLE }); |
| 53 | page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); |
| 54 | } |
| 55 | const errors = []; |
| 56 | page.on("pageerror", error => errors.push(error.message)); |
| 57 | page.on("console", message => { if (message.type() === "error") errors.push(message.text()); }); |
| 58 | await page.addInitScript(() => localStorage.setItem("reasonix-lang", "en")); |
| 59 | await page.goto(url); |
| 60 | await page.locator("#composer-input").waitFor({ state: "visible" }); |
| 61 | await page.evaluate(() => { |
| 62 | window.__reasonixPopoverCornerSamples = []; |
| 63 | const sample = () => { |
| 64 | const menu = document.querySelector(".modelsw__menu"); |
| 65 | if (menu instanceof HTMLElement && getComputedStyle(menu).visibility === "visible") { |
| 66 | const rect = menu.getBoundingClientRect(); |
| 67 | if (rect.left <= 8.5 && rect.top <= 8.5) window.__reasonixPopoverCornerSamples.push({ left: rect.left, top: rect.top }); |
| 68 | } |
| 69 | window.__reasonixPopoverFrame = requestAnimationFrame(sample); |
| 70 | }; |
| 71 | sample(); |
| 72 | }); |
| 73 | |
| 74 | const input = page.locator("#composer-input"); |
| 75 | await input.fill("/mock-ask"); |
| 76 | await page.locator(".modelsw__trigger:visible").click(); |
| 77 | await page.locator(".modelsw__menu").waitFor({ state: "visible" }); |
| 78 | await input.press("Enter"); |
| 79 | await page.locator(".prompt-shelf--ask").waitFor({ state: "visible" }); |
| 80 | assert.equal(await page.locator(".modelsw__menu").count(), 0, "decision takeover removes the model menu"); |
| 81 | assert.deepEqual(await page.evaluate(() => window.__reasonixPopoverCornerSamples), [], "model menu never flashes in the top-left corner"); |
| 82 | |
| 83 | await page.getByRole("button", { name: "Stop task" }).click(); |
| 84 | await page.locator("#composer-input").waitFor({ state: "visible" }); |
| 85 | assert.equal(await page.locator(".modelsw__menu").count(), 0, "model menu stays closed after the composer returns"); |
| 86 | assert.equal(await page.locator(".modelsw__trigger").getAttribute("aria-expanded"), "false"); |
| 87 | await page.evaluate(() => cancelAnimationFrame(window.__reasonixPopoverFrame)); |
| 88 | assert.deepEqual(errors, []); |
| 89 | console.log(`PASS ${electronEngine ? "Electron" : "Chromium"} decision takeover closes model popover without corner flash or reopen`); |
| 90 | } finally { |
| 91 | await browser?.close(); |
| 92 | await electronApp?.close(); |
| 93 | await server.close(); |
| 94 | if (electronHome) await rm(electronHome, { recursive: true, force: true }); |
| 95 | } |
| 96 |