evOo-local/apps/web/e2e/ui-appearance.spec.ts
cynarAI c4c5ce3c2d
fix(ui): polish the light palette, center the stage bloom (#108)
* fix(ui): make the light theme a real light theme

The web shell hardcoded dark surfaces (panel hex, white/black alphas,
white text), so switching to light left the sidebar, chat, composer and
side panel dark with ink text on top. Shell surfaces now use the shared
tokens: light gets a warm paper frame with white panels, hairline borders
and short soft shadows; dark keeps its look via dark: variants.

- Light palette: visible borders (border was equal to muted), white
  primary-foreground, lighter user bubble, readable warning/success.
- Selected sidebar row lives in one place (roster-row-style).
- Markdown, inline code, code blocks, quotes and scrollbars follow tokens.
- cn() now knows the text-ui-* scale is a font size; tailwind-merge
  treated it as a color and dropped labels' color on resized buttons
  (sign-up submit rendered ink on ink).
- Stage bloom was offset left by a double translateX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT

* refactor(ui-web): leave the cn() fix to #115

#115 fixes the same tailwind-merge bug more completely (text, control,
icon and spacing scales, guarded against styles.css) and adds the same
two files, so this branch drops its narrower copy to avoid the overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 04:37:24 +02:00

140 lines
5.7 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, rpc, signup } from "./helpers";
async function captureSidebarSearchSelected(
page: Page,
testInfo: Parameters<typeof captureScreenshot>[1],
name: string,
) {
const aside = page.locator("aside").first();
const search = aside.getByTestId("sidebar-search");
const selected = aside.getByRole("button", { name: /^Chief/ }).first();
await expect(search).toBeVisible();
await expect(selected).toBeVisible();
await search.scrollIntoViewIfNeeded();
const searchBox = await search.boundingBox();
const selectedBox = await selected.boundingBox();
expect(searchBox).toBeTruthy();
expect(selectedBox).toBeTruthy();
if (searchBox && selectedBox) {
expect(Math.abs(searchBox.x - selectedBox.x)).toBeLessThanOrEqual(1);
expect(
Math.abs(searchBox.x + searchBox.width - (selectedBox.x + selectedBox.width)),
).toBeLessThanOrEqual(1);
}
const box = await aside.boundingBox();
if (box) {
const screenshotPath = testInfo.outputPath(`${name}.png`);
await page.screenshot({
animations: "disabled",
caret: "hide",
path: screenshotPath,
clip: {
x: Math.max(0, box.x),
y: Math.max(0, box.y),
width: Math.min(box.width + 24, 360),
height: Math.min(Math.max(box.height * 0.45, 280), 420),
},
});
await testInfo.attach(name, { contentType: "image/png", path: screenshotPath });
return;
}
await captureScreenshot(page, testInfo, name);
}
test("account settings appearance control switches to light mode", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `ui-appearance-${stamp}@rakazo.test`, "password12", "Appearance QA");
await completeOnboarding(page, testInfo);
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();
await expect(settings.getByRole("heading", { name: "Appearance", exact: true })).toBeVisible();
const picker = settings.getByTestId("ui-appearance-select");
await expect(picker).toBeVisible();
await captureScreenshot(page, testInfo, "ui-appearance-control");
await settings.getByTestId("ui-appearance-light").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await expect(settings.getByTestId("ui-appearance-light")).toHaveAttribute("aria-pressed", "true");
await captureScreenshot(page, testInfo, "ui-appearance-light-settings");
await settings.getByRole("button", { name: "Close user settings" }).click();
await expect(settings).toBeHidden();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
// Light surfaces stay light: no dark panel or white-on-paper text survives the switch.
await expect(page.locator("main").first()).toHaveCSS("background-color", "rgb(255, 255, 255)");
await expect(page.locator("aside").first()).toHaveCSS("background-color", "rgb(245, 245, 242)");
await captureScreenshot(page, testInfo, "ui-appearance-light-shell");
await captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-light");
const composer = page.getByRole("combobox", { name: /^Message/ });
await composer.fill("Please review `shared/PROJECT_CHECKPOINT_WRAPUP.md`.");
await composer.press("Enter");
const assistantReply = page
.getByTestId("transcript")
.locator("[data-message-id]")
.filter({ hasText: "done. i handled:" });
const inlinePath = assistantReply
.locator("code")
.filter({ hasText: "shared/PROJECT_CHECKPOINT_WRAPUP.md" });
await expect(inlinePath).toBeVisible({ timeout: 30_000 });
await expect(inlinePath).toHaveCSS("color", "rgb(26, 26, 26)");
await captureScreenshot(page, testInfo, "inline-code-light");
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(settings).toBeVisible();
await settings.getByTestId("ui-appearance-dark").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await settings.getByRole("button", { name: "Close user settings" }).click();
await expect(settings).toBeHidden();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-dark");
});
test("sidebar bot rows share a hover tone", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `ui-hover-${stamp}@rakazo.test`, "password12", "Hover QA");
await completeOnboarding(page, testInfo);
await rpc(page, "bots/create", {
name: "Second Bot",
title: "",
description: "",
notifyOnFinish: true,
computerMode: "team",
});
await rpc(page, "bots/create", {
name: "Third Bot",
title: "",
description: "",
notifyOnFinish: true,
computerMode: "team",
});
await page.reload();
await page.waitForURL(/\/app\/[^/]+$/);
const sidebar = page.locator("aside").first();
const second = sidebar.getByRole("button", { name: /^Second Bot/ }).first();
const third = sidebar.getByRole("button", { name: /^Third Bot/ }).first();
await expect(second).toBeVisible();
await expect(third).toBeVisible();
const readRowBg = (locator: typeof second) =>
locator.evaluate((el) => getComputedStyle(el).backgroundColor);
const readHoverBg = async (locator: typeof second) => {
await locator.hover();
await expect.poll(() => readRowBg(locator)).not.toBe("rgba(0, 0, 0, 0)");
return readRowBg(locator);
};
await readHoverBg(second);
await captureScreenshot(page, testInfo, "sidebar-row-hover");
await readHoverBg(third);
});