import { chromium } from "../tools/playwright/node_modules/playwright/index.mjs"; import assert from "node:assert/strict"; import { mkdirSync } from "node:fs"; const outDir = "/tmp/lazyboy2-chat-overlap"; mkdirSync(outDir, { recursive: true }); const transcript = [ { role: "user", content: "幫我整理這份資料", at: "2026-09-15T01:00:00Z" }, { role: "assistant", content: "好,我先看過內容再整理成條列。", at: "2026-09-15T01:00:05Z" }, { role: "user", content: "要包含這三點:\n1. 重點\n2. 風險\n3. 下一步", at: "2026-09-15T01:01:00Z" }, { role: "assistant", content: "## 整理\n\n- **重點**:目前流程能跑,但訊息排版會疊在一起。\n- **風險**:重整後看起來像兩段對話。\n- **下一步**:把氣泡、時間戳、輸入框分開。\n\n```js\nconst overlap = true;\n```\n\n最後請確認長文不會蓋住下一則。", at: "2026-09-15T01:01:20Z" }, { role: "user", content: "短", at: "2026-09-15T01:02:00Z" }, { role: "user", content: "然後這個也一起看", at: "2026-09-15T01:02:08Z" }, { role: "assistant", content: "收到。", at: "2026-09-15T01:02:20Z" }, { role: "assistant", content: "我先從第一點開始。", at: "2026-09-15T01:02:25Z" }, { role: "user", content: "再來一則更長的使用者訊息,看看右邊氣泡跟時間會不會疊在一起,以及會不會跟下一則助理訊息重疊。abcdefghijklmnop qrstuvwxyz 1234567890", at: "2026-09-16T08:10:00Z" }, { role: "assistant", content: "這是第二天的回覆,用來測日期分隔跟氣泡間距。", at: "2026-09-16T08:10:08Z" }, ]; function overlap(a, b) { return a.l < b.r - 1 && b.l < a.r - 1 && a.t < b.b - 1 && b.t < a.b - 1; } const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); page.setDefaultTimeout(8000); await page.addInitScript(() => { localStorage.setItem("lazyboy.locale", "zh-Hant"); localStorage.setItem("lazyboy.theme", "dark"); }); await page.route("**/api/**", async (route) => { const path = new URL(route.request().url()).pathname; if (path.endsWith("/activity")) { return route.fulfill({ json: { state: "idle", running: false, queued: false, active_task_ids: [], observed_at_ms: Date.now() } }); } if (path.endsWith("/events")) { return route.fulfill({ contentType: "text/event-stream", body: "data: {\"events\":[]}\n\n" }); } if (path === "/api/agents") { return route.fulfill({ json: { agents: [{ id: "owner", name: "測試", running: false }] } }); } if (path === "/api/agents/owner") { return route.fulfill({ json: { id: "owner", name: "測試", expertise: "", preview: "", transcript, running: false, activity: { state: "idle", running: false, queued: false, active_task_ids: [], observed_at_ms: Date.now() }, }, }); } return route.fulfill({ json: {} }); }); await page.goto(process.env.LAZYBOY_TEST_UI_URL || "http://127.0.0.1:5173"); await page.locator(".message").first().waitFor(); async function assertNoOverlap(label) { await page.locator(".messages").evaluate((el) => { el.scrollTop = el.scrollHeight; }); const result = await page.evaluate(() => { const box = (el) => { const r = el.getBoundingClientRect(); return { t: r.top, l: r.left, b: r.bottom, r: r.right }; }; const hits = []; const ov = (a, b) => a.l < b.r - 1 && b.l < a.r - 1 && a.t < b.b - 1 && b.t < a.b - 1; const blocks = [...document.querySelectorAll(".message-block")]; for (let i = 1; i < blocks.length; i++) { if (ov(box(blocks[i - 1]), box(blocks[i]))) hits.push(`block-${i}`); } for (const block of blocks) { const day = block.querySelector(".day-divider"); const body = block.querySelector(".message-body"); const time = block.querySelector(".message-time"); if (day && body && ov(box(day), box(body))) hits.push("day-body"); if (time && body && ov(box(time), box(body))) hits.push("time-body"); } const composer = document.querySelector(".composer"); const lastBody = document.querySelector(".message-block:last-child .message-body"); if (composer && lastBody && ov(box(composer), box(lastBody))) hits.push("composer-body"); return hits; }); assert.deepEqual(result, [], `${label} overlap ${JSON.stringify(result)}`); } await assertNoOverlap("desktop"); await page.screenshot({ path: `${outDir}/desktop.png` }); await page.setViewportSize({ width: 390, height: 844 }); await page.waitForTimeout(150); await assertNoOverlap("mobile"); await page.screenshot({ path: `${outDir}/mobile.png` }); console.log("PASS chat bubbles, timestamps, day markers and composer do not overlap"); } finally { await browser.close(); }