import { expect, test } from "@playwright/test"; import { THREAD_WITH_ARTIFACTS, THREAD_WITH_HISTORY, openChat, reuseThreadChatEntry, setTheme, skipIfMissingThread, } from "./support/chat-helpers"; function isTransparent(color: string) { const normalized = color.replace(/\s+/g, "").toLowerCase(); return normalized === "transparent" || normalized.endsWith(",0)"); } function parseRgb(color: string) { const match = color.match( /rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)/i, ); if (!match) return null; return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]), a: match[4] == null ? 1 : Number(match[4]), }; } function luminance(color: string) { const rgb = parseRgb(color); if (!rgb) return null; return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b; } test.describe("聊天工作台 / 主题颜色回归", () => { test("DF-THEME-001 thread 页面在 light/dark 根容器颜色不同且非透明", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_HISTORY, "FRONTEND_E2E_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_HISTORY!)); const main = page.getByRole("main").first(); await expect(main).toBeVisible(); await setTheme(page, "light"); const lightBg = await main.evaluate( (element) => getComputedStyle(element).backgroundColor, ); await setTheme(page, "dark"); const darkBg = await main.evaluate( (element) => getComputedStyle(element).backgroundColor, ); expect(isTransparent(lightBg)).toBe(false); expect(isTransparent(darkBg)).toBe(false); expect(darkBg).not.toBe(lightBg); }); test("DF-THEME-002 dark 模式下发送按钮 hover 前后颜色变化存在且可见", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_HISTORY, "FRONTEND_E2E_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_HISTORY!)); await setTheme(page, "dark"); const textarea = page.locator("textarea[name='message']"); const submit = page.locator("button[aria-label='Submit']"); await textarea.fill("theme hover regression"); await expect(submit).toBeEnabled(); const before = await submit.evaluate((element) => { const style = getComputedStyle(element); return { background: style.backgroundColor, color: style.color, border: style.borderTopColor, }; }); await submit.hover(); const after = await submit.evaluate((element) => { const style = getComputedStyle(element); return { background: style.backgroundColor, color: style.color, border: style.borderTopColor, }; }); const changed = before.background !== after.background || before.color !== after.color || before.border !== after.border; expect(changed).toBe(true); expect(isTransparent(after.background)).toBe(false); expect(isTransparent(after.color)).toBe(false); const bgLum = luminance(after.background); const textLum = luminance(after.color); if (bgLum != null && textLum != null) { expect(Math.abs(bgLum - textLum)).toBeGreaterThan(15); } }); test("DF-THEME-003 artifact detail 面板在 light/dark 渲染 token 颜色", async ({ page, }, testInfo) => { skipIfMissingThread( testInfo, THREAD_WITH_ARTIFACTS, "FRONTEND_E2E_ARTIFACTS_THREAD_ID", ); await openChat(page, reuseThreadChatEntry(THREAD_WITH_ARTIFACTS!)); const openArtifacts = page.getByTestId("artifacts-open-button"); testInfo.skip( (await openArtifacts.count()) === 0, "当前线程未展示 artifacts 入口。", ); await openArtifacts.click(); const firstCard = page.getByTestId("artifact-file-card").first(); testInfo.skip((await firstCard.count()) === 0, "当前线程没有 artifact 文件。"); await firstCard.click(); const detailRoot = page .locator("div.bg-background.relative.h-full.overflow-hidden.rounded-2xl") .first(); await expect(detailRoot).toBeVisible(); await setTheme(page, "light"); const light = await detailRoot.evaluate((element) => { const style = getComputedStyle(element); const header = element.querySelector("header"); const headerStyle = header ? getComputedStyle(header) : null; return { panelBg: style.backgroundColor, headerBorder: headerStyle?.borderBottomColor ?? "", }; }); await setTheme(page, "dark"); const dark = await detailRoot.evaluate((element) => { const style = getComputedStyle(element); const header = element.querySelector("header"); const headerStyle = header ? getComputedStyle(header) : null; return { panelBg: style.backgroundColor, headerBorder: headerStyle?.borderBottomColor ?? "", }; }); expect(isTransparent(light.panelBg)).toBe(false); expect(isTransparent(dark.panelBg)).toBe(false); expect(light.panelBg).not.toBe(dark.panelBg); expect(light.headerBorder).not.toBe(dark.headerBorder); }); });