LazyBoy2/tests/chat_overlap_ui.mjs

98 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.endsWith("/computer")) {
return route.fulfill({ json: { ready: true, state: "ready", viewer_url: "/fixture-viewer" } });
}
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();
}