| 1 | import assert from "node:assert/strict"; |
| 2 | import path from "node:path"; |
| 3 | import { fileURLToPath } from "node:url"; |
| 4 | import { createServer } from "vite"; |
| 5 | import { copyFile, mkdtemp, rm } from "node:fs/promises"; |
| 6 | import { createRequire } from "node:module"; |
| 7 | import { tmpdir } from "node:os"; |
| 8 | import { selectSession } from "./app-page-actions.mjs"; |
| 9 | |
| 10 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 11 | process.env.PLAYWRIGHT_BROWSERS_PATH ||= path.join(root, ".pw-browsers"); |
| 12 | const { chromium, _electron } = await import("playwright"); |
| 13 | const native = process.argv.includes("--electron"); |
| 14 | const server = await createServer({ root, server: { host: "127.0.0.1", port: 0, hmr: false }, logLevel: "error" }); |
| 15 | await server.listen(); |
| 16 | let browser, electronApp, profile; |
| 17 | try { |
| 18 | let page; |
| 19 | if (native) { |
| 20 | profile = await mkdtemp(path.join(tmpdir(), "reasonix-responsive-dock-")); |
| 21 | const main = path.join(profile, "main.cjs"); |
| 22 | await copyFile(path.join(root, "bench/transcript-layout-electron.cjs"), main); |
| 23 | const requireElectron = createRequire(path.join(root, "../electron/package.json")); |
| 24 | electronApp = await _electron.launch({ executablePath: requireElectron("electron"), args: [main], |
| 25 | env: { ...process.env, REASONIX_LAYOUT_URL: "about:blank" } }); |
| 26 | page = await electronApp.firstWindow(); |
| 27 | } else { |
| 28 | browser = await chromium.launch({ headless: true }); |
| 29 | page = await browser.newPage(); |
| 30 | } |
| 31 | const resize = async (width, height) => { |
| 32 | if (electronApp) await electronApp.evaluate(({ BrowserWindow }, size) => BrowserWindow.getAllWindows()[0].setContentSize(size.width, size.height), { width, height }); |
| 33 | else await page.setViewportSize({ width, height }); |
| 34 | }; |
| 35 | await resize(1600, 1000); |
| 36 | const errors = []; |
| 37 | page.on("pageerror", error => errors.push(error.message)); |
| 38 | await page.addInitScript(() => localStorage.setItem("reasonix-lang", "en")); |
| 39 | await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/?mock=bench&bench=1`); |
| 40 | await page.locator("textarea.composer__input:not([aria-hidden=true])").waitFor(); |
| 41 | await selectSession(page, "bench:small-6t"); |
| 42 | await page.getByRole("tab", { name: "Overview", exact: true }).waitFor(); |
| 43 | const dock = page.locator(".workbench-dock"); |
| 44 | const near = async (width) => { |
| 45 | try { |
| 46 | await page.waitForFunction(expected => Math.abs((document.querySelector(".workbench-dock")?.getBoundingClientRect().width ?? 0) - expected) < 2, width); |
| 47 | } catch (error) { |
| 48 | console.error(await page.evaluate(() => [".layout", ".chat-pane", ".workbench-dock"].map(selector => { |
| 49 | const el = document.querySelector(selector); |
| 50 | return { selector, className: el?.className, rect: el?.getBoundingClientRect().toJSON(), grid: el && getComputedStyle(el).gridTemplateColumns }; |
| 51 | }))); |
| 52 | throw error; |
| 53 | } |
| 54 | }; |
| 55 | await near(720); |
| 56 | const splitter = page.locator(".workspace-panel-resizer"); |
| 57 | await splitter.focus(); |
| 58 | await page.keyboard.press("ArrowLeft"); |
| 59 | await near(736); |
| 60 | const saved = await page.evaluate(() => localStorage.getItem("reasonix.layoutPreferences.v1")); |
| 61 | await dock.getByRole("button", { name: "Close workspace panel", exact: true }).click(); |
| 62 | await dock.waitFor({ state: "detached" }); |
| 63 | await page.locator(".topicbar__chrome-btn--workspace[aria-pressed=false]").click(); |
| 64 | await near(736); |
| 65 | // Browser-only mock has no Electron host/menu entry. Mount the actual browser |
| 66 | // panel through its tab store; native page sizing has separate host tests. |
| 67 | await page.evaluate(async () => { |
| 68 | const { useActivityBarStore } = await import("/src/store/activityBar.ts"); |
| 69 | useActivityBarStore.getState().openEntry("browser", "Browser"); |
| 70 | }); |
| 71 | await page.locator(".browser-panel").waitFor(); |
| 72 | for (const width of [1100, 1024, 1000, 800, 768]) { |
| 73 | console.log(`checking split layout at ${width}`); |
| 74 | await resize(width, 900); |
| 75 | // Read the actual sidebar preference rather than assuming a platform width. |
| 76 | await page.waitForFunction(w => { |
| 77 | const chat = document.querySelector(".chat-pane").getBoundingClientRect(); |
| 78 | const dock = document.querySelector(".workbench-dock").getBoundingClientRect(); |
| 79 | return chat.width >= 399 && dock.width >= 299 && dock.right <= w + 1 && chat.right <= dock.left + 1; |
| 80 | }, width); |
| 81 | assert(await dock.isVisible(), `dock visible at ${width}`); |
| 82 | if (width === 800) { |
| 83 | await page.locator('button[aria-label="Terminal"][aria-pressed]').click(); |
| 84 | await page.waitForFunction(() => { |
| 85 | const drawer = document.querySelector(".terminal-drawer")?.getBoundingClientRect(); |
| 86 | return drawer && drawer.width >= 399 && drawer.height > 100; |
| 87 | }); |
| 88 | await page.locator('button[aria-label="Terminal"][aria-pressed]').click(); |
| 89 | } |
| 90 | } |
| 91 | for (const width of [767, 600, 390]) { |
| 92 | console.log(`checking fullscreen at ${width}`); |
| 93 | await resize(width, 800); |
| 94 | await near(width); |
| 95 | assert.equal(await page.locator(".chat-pane").isVisible(), false, "fullscreen removes covered chat controls from focus"); |
| 96 | assert.equal(await splitter.count(), 0, "fullscreen has no resize handle"); |
| 97 | const rect = await dock.boundingBox(); |
| 98 | assert(rect.x >= -1 && rect.x + rect.width <= width + 1); |
| 99 | await dock.getByRole("button", { name: "Close workspace panel", exact: true }).click(); |
| 100 | assert(await page.locator(".chat-pane").isVisible(), "closing fullscreen returns to chat"); |
| 101 | await page.locator(".topicbar__chrome-btn--workspace[aria-pressed=false]").click(); |
| 102 | await near(width); |
| 103 | } |
| 104 | await resize(1600, 1000); |
| 105 | await near(736); |
| 106 | assert.equal(await page.evaluate(() => localStorage.getItem("reasonix.layoutPreferences.v1")), saved); |
| 107 | await page.reload(); |
| 108 | await near(736); |
| 109 | const handle = await splitter.boundingBox(); |
| 110 | await page.mouse.move(handle.x, handle.y + handle.height / 2); |
| 111 | await page.mouse.down(); |
| 112 | await page.mouse.move(handle.x - 64, handle.y + handle.height / 2, { steps: 5 }); |
| 113 | await page.mouse.up(); |
| 114 | await near(800); |
| 115 | assert.deepEqual(errors, []); |
| 116 | console.log(`responsive dock ${native ? "Electron" : "browser"}: initial ratio, keyboard/pointer resize, Browser tab, narrow fullscreen, close/reopen and restart passed`); |
| 117 | } finally { |
| 118 | await browser?.close(); |
| 119 | await electronApp?.close(); |
| 120 | await server.close(); |
| 121 | if (profile) await rm(profile, { recursive: true, force: true }); |
| 122 | } |
| 123 |