diff --git a/frontend/tests/e2e/support/chat-helpers.ts b/frontend/tests/e2e/support/chat-helpers.ts index 906fc4c4..9d643c39 100644 --- a/frontend/tests/e2e/support/chat-helpers.ts +++ b/frontend/tests/e2e/support/chat-helpers.ts @@ -100,6 +100,15 @@ export async function openChat( } } +export async function setTheme(page: Page, theme: "light" | "dark") { + await page.evaluate((nextTheme) => { + const root = document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(nextTheme); + root.style.colorScheme = nextTheme; + }, theme); +} + export async function expandComposer(page: Page) { const expander = page.locator("div.absolute.inset-0.z-1.cursor-text"); if ((await expander.count()) > 0) { diff --git a/frontend/tests/e2e/theme-colors.spec.ts b/frontend/tests/e2e/theme-colors.spec.ts new file mode 100644 index 00000000..748ae41f --- /dev/null +++ b/frontend/tests/e2e/theme-colors.spec.ts @@ -0,0 +1,170 @@ +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); + }); +});