feat(08-04): 添加工作区主题颜色回归端到端测试

- 添加可复用的 setTheme 辅助函数,用于在端到端测试中切换亮色/暗色主题
- 添加 theme-colors 测试规范,覆盖线程根节点、提交按钮悬停、产物详情等场景
This commit is contained in:
肖应宇 2026-04-23 09:42:19 +08:00
parent 08b3864673
commit cf36873d99
2 changed files with 179 additions and 0 deletions

View File

@ -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) {

View File

@ -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);
});
});