| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | import path from "node:path"; |
| 4 | import { fileURLToPath } from "node:url"; |
| 5 | import { startPreviewServer } from "./vite-preview-server.mjs"; |
| 6 | |
| 7 | const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 8 | process.env.PLAYWRIGHT_BROWSERS_PATH = !process.env.PLAYWRIGHT_BROWSERS_PATH || process.env.PLAYWRIGHT_BROWSERS_PATH === ".pw-browsers" |
| 9 | ? path.join(frontendDir, ".pw-browsers") |
| 10 | : process.env.PLAYWRIGHT_BROWSERS_PATH; |
| 11 | const { chromium } = await import("playwright"); |
| 12 | const port = Number(process.env.REASONIX_APPROVAL_BROWSER_PORT ?? 4620); |
| 13 | const url = `http://127.0.0.1:${port}/`; |
| 14 | |
| 15 | function assert(condition, message) { |
| 16 | if (!condition) throw new Error(message); |
| 17 | process.stdout.write(` PASS ${message}\n`); |
| 18 | } |
| 19 | |
| 20 | const preview = await startPreviewServer(frontendDir, port); |
| 21 | |
| 22 | let browser; |
| 23 | try { |
| 24 | browser = await chromium.launch({ headless: true }); |
| 25 | const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); |
| 26 | const pageErrors = []; |
| 27 | page.on("pageerror", (error) => pageErrors.push(String(error))); |
| 28 | await page.addInitScript(() => { |
| 29 | window.__reasonixApprovalAnimationCalls = []; |
| 30 | const original = Element.prototype.animate; |
| 31 | Element.prototype.animate = function (frames, options) { |
| 32 | if (this instanceof HTMLElement && this.querySelector(".prompt-shelf")) { |
| 33 | window.__reasonixApprovalAnimationCalls.push({ |
| 34 | easing: typeof options === "object" && options ? String(options.easing ?? "") : "", |
| 35 | }); |
| 36 | } |
| 37 | return original.call(this, frames, options); |
| 38 | }; |
| 39 | }); |
| 40 | |
| 41 | const response = await page.goto(url, { waitUntil: "domcontentloaded" }); |
| 42 | assert(response?.ok(), `approval preview serves the frontend (HTTP ${response?.status()})`); |
| 43 | await page.waitForFunction(() => !document.querySelector(".startup-splash"), undefined, { timeout: 30_000 }); |
| 44 | const composer = page.locator("#composer-input"); |
| 45 | await composer.waitFor({ state: "visible", timeout: 30_000 }); |
| 46 | await composer.fill("/mock-tool-approval"); |
| 47 | await page.locator(".composer__btn--send").click(); |
| 48 | |
| 49 | const action = page.locator(".prompt-shelf__actions .prompt-action").first(); |
| 50 | await action.waitFor({ state: "visible", timeout: 30_000 }); |
| 51 | await action.click(); |
| 52 | await page.locator(".decision-confirm-bar__confirm").click(); |
| 53 | try { |
| 54 | await page.waitForFunction(() => !document.querySelector(".prompt-shelf--tool-approval"), undefined, { timeout: 10_000 }); |
| 55 | } catch (error) { |
| 56 | const diagnostics = await page.evaluate(() => { |
| 57 | const shelf = document.querySelector(".prompt-shelf--tool-approval"); |
| 58 | const wrapper = shelf?.parentElement; |
| 59 | return { |
| 60 | shelfText: shelf?.textContent?.replace(/\s+/g, " ").trim(), |
| 61 | wrapperStyle: wrapper?.getAttribute("style"), |
| 62 | disabledActions: [...document.querySelectorAll(".prompt-shelf button:disabled")].map((button) => button.textContent?.trim()), |
| 63 | notices: [...document.querySelectorAll(".toast, .notice, [role='alert']")].map((notice) => notice.textContent?.replace(/\s+/g, " ").trim()), |
| 64 | }; |
| 65 | }); |
| 66 | throw new Error(`approval card did not resolve: ${JSON.stringify(diagnostics)}`, { cause: error }); |
| 67 | } |
| 68 | |
| 69 | const calls = await page.evaluate(() => window.__reasonixApprovalAnimationCalls ?? []); |
| 70 | assert(calls.length === 1, `approval invokes one native Web Animation (${JSON.stringify(calls)})`); |
| 71 | assert(calls[0]?.easing === "cubic-bezier(0.8, 0, 0.8, 0.28)", `approval uses the CSS easing contract (${calls[0]?.easing})`); |
| 72 | assert(pageErrors.length === 0, `approval interaction completes without page errors (${JSON.stringify(pageErrors)})`); |
| 73 | process.stdout.write("\napproval animation browser gate passed\n"); |
| 74 | } finally { |
| 75 | await browser?.close(); |
| 76 | await new Promise((resolve) => preview.httpServer.close(resolve)); |
| 77 | } |
| 78 |