2026-09-15 09:07:16 +00:00
|
|
|
|
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" });
|
|
|
|
|
|
}
|
2026-09-16 06:40:00 +00:00
|
|
|
|
if (path.endsWith("/computer")) {
|
|
|
|
|
|
return route.fulfill({ json: { ready: true, state: "ready", viewer_url: "/fixture-viewer" } });
|
|
|
|
|
|
}
|
2026-09-15 09:07:16 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|