test(ui): remove stylesheet grep tests (#88847)

This commit is contained in:
Dallin Romney
2026-05-31 19:05:02 -07:00
committed by GitHub
parent 4b56c44c02
commit 632447d66d
12 changed files with 144 additions and 644 deletions

View File

@@ -1,98 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../../test/helpers/ui-style-fixtures.js";
function readLayoutCss(): string {
return readStyleSheet("ui/src/styles/chat/layout.css");
}
function readBaseCss(): string {
return readStyleSheet("ui/src/styles/base.css");
}
describe("chat layout styles", () => {
it("styles queued-message steering controls and pending indicators", () => {
const css = readLayoutCss();
expect(css).toContain(".chat-queue__steer");
expect(css).toContain(".chat-queue__actions");
expect(css).toContain(".chat-queue__item--steered");
expect(css).toContain(".chat-queue__badge");
});
it("includes assistant text avatar styles for configured IDENTITY avatars", () => {
const css = readLayoutCss();
expect(css).toContain(".agent-chat__avatar--text");
expect(css).toContain("font-size: 20px;");
expect(css).toContain("place-items: center;");
});
it("keeps composer text scale-driven while preserving mobile input zoom safety", () => {
const css = readLayoutCss();
const baseCss = readBaseCss();
expect(baseCss).toContain(
"--control-ui-input-text-size: max(16px, calc(14px * var(--control-ui-text-scale)));",
);
expect(css).toContain("font-size: var(--control-ui-input-text-size);");
expect(css).toContain(".agent-chat__composer-combobox > textarea");
expect(css).toContain(".chat-compose .chat-compose__field textarea");
});
it("keeps mobile PWA composer controls above under-reported safe areas", () => {
const css = readLayoutCss();
expect(css).toContain("margin: 0 8px calc(14px + var(--safe-area-bottom));");
expect(css).toContain("@media (display-mode: standalone) and (max-width: 768px)");
expect(css).toContain("margin-bottom: calc(14px + max(var(--safe-area-bottom), 34px));");
});
it("keeps desktop chat header controls on a compact aligned rhythm", () => {
const css = readLayoutCss();
expect(css).toContain("min-height: 36px;");
expect(css).toContain("height: 36px;");
expect(css).toContain(".chat-controls .btn--icon {");
expect(css).toContain("width: 36px;");
expect(css).toContain(".chat-controls__separator {");
expect(css).toContain("height: 22px;");
});
it("keeps chat session picker search icon buttons fixed size", () => {
const css = readLayoutCss();
expect(css).toContain(".chat-session-picker .chat-session-picker__icon-button.btn--icon {");
expect(css).toContain("flex: 0 0 36px;");
expect(css).toContain("width: 36px;");
expect(css).toContain("min-width: 36px;");
});
it("keeps composer controls labeled and large enough without shrinking mobile taps", () => {
const css = readLayoutCss();
expect(css).toContain(".agent-chat__control-label");
expect(css).toContain("min-width: 36px;");
expect(css).toContain("height: 36px;");
expect(css).toContain("@media (max-width: 860px)");
expect(css).toContain("width: 44px;");
});
it("keeps the initial chat loading skeleton wide enough to read as message bubbles", () => {
const css = readLayoutCss();
expect(css).toContain(".chat-loading-skeleton .chat-msg");
expect(css).toContain("width: min(560px, 82%);");
expect(css).toContain(".chat-loading-skeleton .chat-line.user .chat-msg");
expect(css).toContain("width: min(360px, 70%);");
expect(css).toContain(".chat-loading-skeleton .chat-bubble");
expect(css).toContain("width: 100%;");
});
it("lets realtime Talk turns flow in the chat thread", () => {
const css = readLayoutCss();
expect(css).toContain(".agent-chat__voice-turns");
expect(css).toContain("background: transparent;");
expect(css).not.toContain("max-height: min(28vh, 220px);");
});
});

View File

@@ -1,15 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../../test/helpers/ui-style-fixtures.js";
function readTextCss(): string {
return readStyleSheet("ui/src/styles/chat/text.css");
}
describe("chat text styles", () => {
it("uses browser-local text scale variables for message text", () => {
const css = readTextCss();
expect(css).toContain("font-size: var(--chat-text-size);");
expect(css).toContain("font-size: var(--control-ui-text-sm);");
});
});

View File

@@ -1,52 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../../test/helpers/ui-style-fixtures.js";
function readToolCardsCss(): string {
return readStyleSheet("ui/src/styles/chat/tool-cards.css");
}
describe("chat tool card styles", () => {
it("keeps collapsed tool summaries readable without premature ellipsis", () => {
const css = readToolCardsCss();
const summaryRule = css.match(/\.chat-tool-msg-summary\s*\{[^}]*\}/)?.[0] ?? "";
expect(css).toContain(".chat-tool-msg-summary {");
expect(css).toContain("align-items: center;");
expect(css).toContain("align-content: center;");
expect(css).toContain(".chat-bubble--tool-shell > .chat-tool-msg-collapse {\n margin-top: 0;");
expect(css).toContain("flex-wrap: wrap;");
expect(css).toContain("gap: 12px;");
expect(css).toContain("font-size: var(--control-ui-text-sm);");
expect(css).toContain(
".chat-tool-msg-summary svg,\n.chat-tool-msg-summary__icon {\n flex-shrink: 0;",
);
expect(css).toContain(
".chat-tool-msg-summary span {\n display: inline-flex;\n align-items: center;\n line-height: var(--control-ui-text-sm);",
);
expect(summaryRule).toContain("font-family: inherit;");
expect(summaryRule).not.toContain("font: inherit;");
expect(css).toContain("color: var(--text);");
expect(css).toMatch(/\.chat-tool-msg-summary__names\s*,/);
expect(css).toContain(".chat-tool-msg-summary__preview");
expect(css).toContain("font-size: var(--control-ui-text-sm);");
expect(css).toContain("transform: translateY(0.8px);");
expect(css).toContain("flex: 0 1 auto;");
expect(css).toContain("height: 20px;");
expect(css).toContain("min-height: 20px;");
expect(css).not.toContain(".chat-tool-msg-summary__names {\n text-align: center;");
expect(css).toContain("overflow-wrap: anywhere;");
expect(css).toContain("text-overflow: clip;");
expect(css).toContain("white-space: normal;");
expect(css).not.toContain("max-width: 42%;");
});
it("keeps expanded tool cards and actions usable on narrow screens", () => {
const css = readToolCardsCss();
expect(css).toContain(".chat-tool-card--expanded {");
expect(css).toContain("max-height: none;");
expect(css).toContain("overflow: hidden;");
expect(css).toContain("white-space: nowrap;");
expect(css).toContain(".chat-tool-card__block-content code");
});
});

View File

@@ -1,115 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
function readComponentsCss(): string {
return readStyleSheet("ui/src/styles/components.css");
}
describe("code block highlight styles", () => {
it("targets the markdown renderer's generated code block wrapper", () => {
const css = readComponentsCss();
expect(css).toContain(":is(.code-block .hljs, .code-block-wrapper pre code.hljs)");
expect(css).toContain(":is(.code-block, .code-block-wrapper pre code.hljs) .hljs-keyword");
expect(css).toContain(
':root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-string',
);
});
});
describe("agent fallback chip styles", () => {
it("styles the chip remove control inside the agent model input", () => {
const css = readComponentsCss();
expect(css).toContain(".agent-chip-input .chip {");
expect(css).toContain(".agent-chip-input .chip-remove {");
expect(css).toContain(".agent-chip-input .chip-remove:hover:not(:disabled)");
expect(css).toContain(".agent-chip-input .chip-remove:focus-visible:not(:disabled)");
expect(css).toContain("outline: 2px solid var(--accent);");
expect(css).toContain("outline-offset: 2px;");
expect(css).toContain(".agent-chip-input .chip-remove:disabled");
});
it("keeps touch-primary field controls large enough to avoid iOS focus zoom", () => {
const css = readComponentsCss();
expect(css).toMatch(
/@media \(hover: none\) and \(pointer: coarse\) \{[\s\S]*\.field input,[\s\S]*\.field textarea,[\s\S]*\.field select \{[\s\S]*font-size: 16px;/,
);
});
});
describe("field select styles", () => {
it("keeps light-mode native select arrows visible without tiling", () => {
const css = readComponentsCss();
expect(css).toMatch(
/\.field select \{[\s\S]*background-image: url\("data:image\/svg\+xml,[^"]*stroke='%23a1a1aa'[^"]*"\);[\s\S]*background-repeat: no-repeat;[\s\S]*background-position: right 10px center;/,
);
expect(css).toMatch(
/:root\[data-theme-mode="light"\] \.field input,[\s\S]*:root\[data-theme-mode="light"\] \.field textarea,[\s\S]*:root\[data-theme-mode="light"\] \.field select \{[\s\S]*background-color: var\(--card\);[\s\S]*border-color: var\(--input\);[\s\S]*\}\n\n:root\[data-theme-mode="light"\] \.field select \{[\s\S]*background-image: url\("data:image\/svg\+xml,[^"]*stroke='%23444'[^"]*"\);/,
);
expect(css).not.toContain(
':root[data-theme-mode="light"] .field select {\n background: var(--card);',
);
});
});
describe("sessions filter styles", () => {
it("keeps the expanded sessions filters on one row until the mobile breakpoint", () => {
const css = readComponentsCss();
expect(css).toContain(".sessions-filter-bar {\n display: flex;\n flex-wrap: wrap;");
expect(css).toContain("@media (max-width: 760px)");
expect(css).toContain(".sessions-filter-bar {\n flex-direction: column;");
});
});
describe("sessions table responsive styles", () => {
it("keeps the compaction disclosure and details usable on narrow screens", () => {
const componentsCss = readComponentsCss();
const mobileCss = readStyleSheet("ui/src/styles/layout.mobile.css");
expect(componentsCss).toContain(".session-compaction-cell {");
expect(componentsCss).toContain(".session-compaction-trigger {");
expect(componentsCss).toContain(".session-status-badge {");
expect(componentsCss).toContain(".sessions-table tbody tr.session-data-row > td {");
expect(componentsCss).toContain(".session-runtime-cell .mono {");
expect(componentsCss).toContain("text-overflow: ellipsis;");
expect(componentsCss).toContain(".session-details-panel {");
expect(componentsCss).not.toContain(".session-checkpoint-toggle {");
expect(mobileCss).toContain(".data-table.sessions-table {\n min-width: 560px;");
expect(mobileCss).toContain(
".sessions-table th:nth-child(12),\n .sessions-table td:nth-child(12),\n .sessions-table th:nth-child(13),\n .sessions-table td:nth-child(13)",
);
expect(mobileCss).toContain(
".sessions-table th:nth-child(4),\n .sessions-table td:nth-child(4),\n .sessions-table th:nth-child(11),\n .sessions-table td:nth-child(11)",
);
expect(mobileCss).toContain(
".sessions-table th:nth-child(3),\n .sessions-table td:nth-child(3),\n .sessions-table th:nth-child(10),\n .sessions-table td:nth-child(10)",
);
expect(mobileCss).toContain(
".sessions-table th:nth-child(6),\n .sessions-table td:nth-child(6),\n .sessions-table th:nth-child(7),\n .sessions-table td:nth-child(7)",
);
expect(mobileCss).toContain(".data-table.sessions-table .data-table-key-col {");
expect(mobileCss).toContain(".sessions-table .session-status-col {");
expect(mobileCss).toContain(".sessions-table .session-goal-chip {");
expect(mobileCss).not.toContain(
".sessions-table th:nth-child(5),\n .sessions-table td:nth-child(5)",
);
});
});
describe("overview access grid styles", () => {
it("keeps access fields and native controls within the card", () => {
const css = readComponentsCss();
expect(css).toContain(
"grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));",
);
expect(css).toContain(".ov-access-grid .field {\n min-width: 0;");
expect(css).toContain(".ov-access-grid .field input,\n.ov-access-grid .field select {");
expect(css).toContain("box-sizing: border-box;");
expect(css).toContain("width: 100%;");
});
});

View File

@@ -1,84 +0,0 @@
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const cssPath = [
resolve(process.cwd(), "ui/src/styles/config-quick.css"),
resolve(process.cwd(), "..", "ui/src/styles/config-quick.css"),
].find((candidate) => existsSync(candidate));
if (!cssPath) {
throw new Error(`config-quick.css not found from cwd: ${process.cwd()}`);
}
const css = readFileSync(cssPath, "utf8");
function expectSelectorBlockToMatch(selector: string, pattern: RegExp) {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const blockMatches = [...css.matchAll(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`, "gs"))];
expect(blockMatches.map((match) => match[0]).some((block) => pattern.test(block))).toBe(true);
}
describe("config-quick styles", () => {
it("includes the local user identity quick-settings styles", () => {
expect(css).toContain(".qs-identity-grid");
expect(css).toContain(".qs-identity-card__source");
expect(css).toContain(".qs-identity-card__issue");
expect(css).toContain(".qs-identity-card__repair");
expect(css).toContain(".qs-identity-card__error");
expect(css).toContain(".qs-assistant-avatar");
expect(css).toContain(".qs-user-avatar");
expect(css).toContain(".qs-card--personal");
});
it("includes the dashboard quick-settings density layout", () => {
expect(css).toContain(".qs-card--model");
expect(css).toContain(".qs-card--automations");
expect(css).toContain(".qs-side-stack");
expect(css).toContain("grid-template-rows: auto 1fr;");
expect(css).toContain(".qs-identity-card__actions");
expect(css).toContain("grid-template-columns: repeat(12, minmax(0, 1fr));");
expect(css).toContain("grid-column: 1 / -1;");
expectSelectorBlockToMatch(".qs-side-stack", /grid-column:\s*span\s+3;/);
expectSelectorBlockToMatch(".qs-card--personal", /grid-column:\s*span\s+9;/);
expect(css).toContain("grid-template-columns: repeat(2, minmax(0, 1fr));");
expect(css).toContain("align-items: stretch;");
expect(css).toContain("display: contents;");
expectSelectorBlockToMatch(".qs-card--personal", /order:\s*4;/);
expectSelectorBlockToMatch(".qs-card--appearance", /order:\s*5;/);
expect(css).toContain(".qs-card--appearance");
expect(css).toContain("order: 5");
expect(css).toContain(".qs-card--automations");
expect(css).toContain("order: 6");
});
it("includes explicit context profile layout hooks", () => {
expect(css).toContain(".qs-profiles");
expect(css).toContain(".qs-profile-state--pending");
expect(css).toContain(".qs-profile-panel__actions-row");
});
it("keeps settings section tabs padded away from scoped page content", () => {
expect(css).toContain("padding: 24px 16px 16px;");
expect(css).toContain("padding: 16px 0 12px;");
});
it("keeps settings section icons on the current text color", () => {
expect(css).toMatch(
/\.settings-section-nav__icon svg \{[\s\S]*stroke: currentColor;[\s\S]*fill: none;/,
);
expect(css).toMatch(
/\.settings-section-nav__icon svg \* \{[\s\S]*stroke: currentColor;[\s\S]*fill: none;/,
);
});
it("scopes the logs fill-height chain to the explicit logs route class", () => {
expect(css).toContain(".content--logs {");
expectSelectorBlockToMatch(".content--logs", /overflow:\s*hidden;/);
expectSelectorBlockToMatch(".content--logs .settings-workspace", /display:\s*flex;/);
expectSelectorBlockToMatch(".content--logs .settings-workspace__body", /min-height:\s*0;/);
expect(css).not.toContain(":has(.card--fill-height)");
});
it("avoids transition-all in the quick settings surface", () => {
expect(css).not.toContain("transition: all");
});
});

View File

@@ -1,38 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
function readConfigCss(): string {
return readStyleSheet("ui/src/styles/config.css");
}
describe("config styles", () => {
it("keeps touch-primary config text controls large enough to avoid iOS focus zoom", () => {
const css = readConfigCss();
expect(css).toMatch(
/@media \(hover: none\) and \(pointer: coarse\) \{[\s\S]*\.config-search__input,[\s\S]*\.settings-theme-import__input,[\s\S]*\.config-raw-field textarea,[\s\S]*\.cfg-input,[\s\S]*\.cfg-input--sm,[\s\S]*\.cfg-textarea,[\s\S]*\.cfg-textarea--sm,[\s\S]*\.cfg-number__input,[\s\S]*\.cfg-select \{[\s\S]*font-size: 16px;/,
);
});
it("keeps the config chrome padded away from the page edge", () => {
const css = readConfigCss();
expect(css).toMatch(/\.config-layout \{[\s\S]*margin: 12px 0 0;/);
expect(css).toMatch(/\.config-actions \{[\s\S]*padding: 14px 24px;/);
expect(css).toMatch(/\.config-top-tabs \{[\s\S]*padding: 14px 24px 12px;/);
expect(css).toMatch(/\.config-top-tabs__tab \{[\s\S]*min-height: 36px;/);
expect(css).not.toContain("margin: 0 -16px -32px");
expect(css).not.toContain("margin: 0 -8px -16px");
});
it("keeps light-mode config select arrows visible", () => {
const css = readConfigCss();
expect(css).toMatch(
/\.cfg-select \{[\s\S]*background-image: url\("data:image\/svg\+xml,[^"]*stroke='%23888'[^"]*"\);[\s\S]*background-repeat: no-repeat;[\s\S]*background-position: right 10px center;/,
);
expect(css).toMatch(
/:root\[data-theme-mode="light"\] \.cfg-select \{[\s\S]*background-color: white;[\s\S]*border-color: var\(--border\);[\s\S]*background-image: url\("data:image\/svg\+xml,[^"]*stroke='%23444'[^"]*"\);/,
);
});
});

View File

@@ -1,159 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
function readMobileCss(): string {
return readStyleSheet("ui/src/styles/layout.mobile.css");
}
function readLayoutCss(): string {
return readStyleSheet("ui/src/styles/layout.css");
}
function readGroupedChatCss(): string {
return readStyleSheet("ui/src/styles/chat/grouped.css");
}
function selectorBlocks(css: string, selector: string): string[] {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return [...css.matchAll(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`, "gs"))].map(
(match) => match[0],
);
}
describe("chat header responsive mobile styles", () => {
it("keeps the chat header and session controls from clipping on narrow widths", () => {
const css = readMobileCss();
const layoutCss = readLayoutCss();
expect(css).toContain("@media (max-width: 1320px)");
expect(css).toContain(".content--chat .content-header");
expect(layoutCss).toContain(".content--chat {\n display: flex;\n flex-direction: column;\n gap: 2px;\n overflow: hidden;\n padding-top: 0;");
expect(css).toContain("max-height: 44px;");
expect(layoutCss).toContain(".content--chat .content-header .chat-controls__session-notice");
expect(layoutCss).toContain("position: absolute;");
expect(css).toContain(".chat-controls__session-row");
expect(css).toContain('grid-template-areas: "agent session model";');
});
it("lays out mobile chat header action icons as an even full-width grid", () => {
const css = readMobileCss();
expect(css).toContain(
".chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__thinking",
);
expect(css).toContain("grid-template-columns: repeat(4, minmax(0, 1fr));");
expect(css).toContain(
".chat-mobile-controls-wrapper .chat-controls-dropdown .btn--icon {\n width: 100%;",
);
expect(css).toContain("height: 44px;");
});
it("keeps chat session picker search icons from stretching in mobile controls", () => {
const css = readMobileCss();
expect(css).toContain(".chat-session-picker__icon-button.btn--icon {");
expect(css).toContain("flex: 0 0 44px;");
expect(css).toContain("width: 44px;");
expect(css).toContain("min-width: 44px;");
});
it("restores single-page logs scrolling on mobile", () => {
const mobileCss = readMobileCss();
const logsBlock = selectorBlocks(mobileCss, ".content.content--logs").join("\n");
const workspaceBlock = selectorBlocks(
mobileCss,
".content.content--logs .settings-workspace",
).join("\n");
const logStreamBlock = selectorBlocks(
mobileCss,
".card--fill-height.card--fill-height .log-stream",
).join("\n");
expect(logsBlock).toContain("display: block;");
expect(logsBlock).toContain("overflow-y: auto;");
expect(workspaceBlock).toContain("display: block;");
expect(logStreamBlock).toContain("max-height: 380px;");
});
});
describe("sidebar menu trigger styles", () => {
it("keeps the mobile sidebar trigger visibly interactive on hover and keyboard focus", () => {
const css = readLayoutCss();
expect(css).toContain(".sidebar-menu-trigger {");
expect(css).toContain("cursor: pointer;");
expect(css).toContain(".sidebar-menu-trigger:hover {");
expect(css).toContain("background: color-mix(in srgb, var(--bg-hover) 84%, transparent);");
expect(css).toContain("color: var(--text);");
expect(css).toContain(".sidebar-menu-trigger:focus-visible {");
expect(css).toContain("box-shadow: var(--focus-ring);");
expect(css).toContain(".topbar-nav-toggle {");
expect(css).toContain("display: none;");
});
it("keeps the sidebar new-session button inset and its icon visible", () => {
const css = readLayoutCss();
const sessionsBlock = selectorBlocks(css, ".sidebar-sessions").join("\n");
const newSessionBlock = selectorBlocks(css, ".sidebar-new-session").join("\n");
const newSessionIconBlock = selectorBlocks(css, ".sidebar-new-session__icon svg").join("\n");
const collapsedSessionsBlock = selectorBlocks(
css,
".sidebar--collapsed .sidebar-sessions",
).join("\n");
expect(sessionsBlock).toContain("padding: 0 8px;");
expect(newSessionBlock).toContain("min-height: 38px;");
expect(newSessionBlock).toContain("box-sizing: border-box;");
expect(newSessionIconBlock).toContain("stroke: currentColor;");
expect(newSessionIconBlock).toContain("fill: none;");
expect(collapsedSessionsBlock).toContain("padding: 0;");
});
});
describe("topbar theme mode tooltip styles", () => {
it("clamps the rightmost color mode tooltip inside the viewport edge", () => {
const css = readLayoutCss();
const lastChildAfterBlock = selectorBlocks(
css,
".topbar-theme-mode__btn:last-child[data-tooltip]::after",
).join("\n");
const lastChildHoverAfterBlock = selectorBlocks(
css,
".topbar-theme-mode__btn:last-child[data-tooltip]:hover::after",
).join("\n");
const lastChildFocusAfterBlock = selectorBlocks(
css,
".topbar-theme-mode__btn:last-child[data-tooltip]:focus-visible::after",
).join("\n");
expect(lastChildAfterBlock).toContain("right: 0;");
expect(lastChildHoverAfterBlock).toContain("transform: translateY(0);");
expect(lastChildFocusAfterBlock).toContain("transform: translateY(0);");
const tooltipBlock =
selectorBlocks(css, ".topbar-theme-mode__btn[data-tooltip]::after").find((block) =>
block.includes("content: attr(data-tooltip);"),
) ?? "";
expect(tooltipBlock).toBeTruthy();
expect(tooltipBlock).not.toContain("min-width:");
expect(tooltipBlock).toContain("max-width: min(220px, 60vw);");
});
});
describe("grouped chat width styles", () => {
it("uses the config-fed CSS variable with the current fallback", () => {
const css = readGroupedChatCss();
expect(css).toContain("max-width: var(--chat-message-max-width, min(900px, 68%));");
});
it("excludes tool shells from light hover without overriding user bubble hover", () => {
const css = readGroupedChatCss();
expect(css).toContain(
':root[data-theme-mode="light"] .chat-bubble:not(:where(.chat-bubble--tool-shell)):hover',
);
expect(css).not.toContain(
':root[data-theme-mode="light"] .chat-bubble:not(.chat-bubble--tool-shell):hover',
);
});
});

View File

@@ -1,49 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheetAsync } from "../../../test/helpers/ui-style-fixtures.js";
describe("markdown preview styles", () => {
it("keeps the preview dialog canvas unified", async () => {
const css = await readStyleSheetAsync("ui/src/styles/components.css");
expect(css).toContain(".md-preview-dialog__header-main");
expect(css).toContain(".md-preview-dialog__meta");
expect(css).toContain("--cm-bg: transparent;");
expect(css).toContain(".md-preview-dialog__reader .cm-preview");
expect(css).not.toContain("width: min(780px, calc(100vw - 48px));");
expect(css).not.toContain("background: rgba(0, 0, 0, 0.65);");
expect(css).not.toContain("color-mix(in srgb, var(--card) 94%, white 6%)");
});
it("keeps expanded previews focused on header controls and reading space", async () => {
const css = await readStyleSheetAsync("ui/src/styles/components.css");
expect(css).toContain(".md-preview-dialog__panel.fullscreen .md-preview-dialog__header-main");
expect(css).toContain("clip-path: inset(50%);");
expect(css).toMatch(
/\.md-preview-dialog__panel\.fullscreen\s+\.md-preview-dialog__meta\s*\{[^}]*display:\s*none;/,
);
expect(css).toContain(".md-preview-dialog__panel.fullscreen .md-preview-dialog__body");
expect(css).toContain("width: min(100%, 96ch);");
});
it("styles preview header controls as compact icon buttons", async () => {
const css = await readStyleSheetAsync("ui/src/styles/components.css");
expect(css).toContain(".md-preview-icon-btn");
expect(css).toContain("width: 36px;");
expect(css).toContain("height: 36px;");
expect(css).toContain('.md-preview-icon-btn[aria-pressed="true"]');
});
it("keeps the sidebar reader shell in sidebar.css", async () => {
const css = await readStyleSheetAsync("ui/src/styles/chat/sidebar.css");
expect(css).toContain(".sidebar-markdown-shell__toolbar");
expect(css).toContain(".sidebar-markdown-reader");
expect(css).toContain(".sidebar-markdown-shell__hint");
expect(css).toContain(".sidebar-markdown-empty");
expect(css).toMatch(
/\.sidebar-markdown-shell__eyebrow svg\s*\{[^}]*stroke:\s*currentColor;[^}]*fill:\s*none;/,
);
});
});

View File

@@ -1,16 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
function readUsageCss(): string {
return readStyleSheet("ui/src/styles/usage.css");
}
describe("usage styles", () => {
it("keeps touch-primary usage text controls large enough to avoid iOS focus zoom", () => {
const css = readUsageCss();
expect(css).toMatch(
/@media \(hover: none\) and \(pointer: coarse\) \{[\s\S]*\.usage-date-input,[\s\S]*\.usage-select,[\s\S]*\.usage-query-input,[\s\S]*\.usage-filters-inline select,[\s\S]*\.usage-filters-inline input\[type="text"\] \{[\s\S]*font-size: 16px;/,
);
});
});

View File

@@ -1,18 +0,0 @@
import { describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
function readWorkboardCss(): string {
return readStyleSheet("ui/src/styles/workboard.css");
}
describe("workboard styles", () => {
it("keeps status columns in one horizontally scrollable grid row", () => {
const css = readWorkboardCss();
expect(css).toContain(".workboard-board {\n display: grid;\n grid-auto-flow: column;");
expect(css).toContain("grid-auto-columns: minmax(220px, 1fr);");
expect(css).toContain("overflow-x: auto;");
expect(css).toContain("grid-auto-columns: minmax(260px, 82vw);");
expect(css).not.toContain("grid-template-columns: repeat(6");
});
});

View File

@@ -0,0 +1,143 @@
import { existsSync } from "node:fs";
import { chromium, type Browser, type Page } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { readStyleSheet } from "../../../test/helpers/ui-style-fixtures.js";
const describeBrowserLayout = existsSync(chromium.executablePath()) ? describe : describe.skip;
let browser: Browser;
function readUiCss(): string {
const files = [
"ui/src/styles/base.css",
"ui/src/styles/components.css",
"ui/src/styles/config.css",
"ui/src/styles/usage.css",
"ui/src/styles/chat/layout.css",
];
return files.map((file) => readStyleSheet(file)).join("\n");
}
function controlsHtml() {
return `
<main>
<label class="field"><input value="field input" /></label>
<label class="field"><textarea>field textarea</textarea></label>
<label class="field"><select><option>field select</option></select></label>
<input class="config-search__input" value="search" />
<input class="settings-theme-import__input" value="theme" />
<label class="config-raw-field"><textarea>raw config</textarea></label>
<input class="cfg-input" value="config input" />
<input class="cfg-input cfg-input--sm" value="small config input" />
<textarea class="cfg-textarea">config textarea</textarea>
<textarea class="cfg-textarea cfg-textarea--sm">small config textarea</textarea>
<label class="cfg-number"><input class="cfg-number__input" value="1" /></label>
<select class="cfg-select"><option>config select</option></select>
<input class="usage-date-input" value="2026-05-31" />
<select class="usage-select"><option>usage select</option></select>
<input class="usage-query-input" value="usage query" />
<div class="usage-filters-inline">
<select><option>inline usage select</option></select>
<input type="text" value="inline usage input" />
</div>
<div class="agent-chat__composer-combobox"><textarea>chat composer</textarea></div>
<div class="chat-compose"><label class="chat-compose__field"><textarea>chat compose</textarea></label></div>
</main>
`;
}
async function openMobileFixture(): Promise<Page> {
const page = await browser.newPage({
hasTouch: true,
isMobile: true,
viewport: { width: 390, height: 844 },
});
await page.setContent(
`<!doctype html><html data-theme-mode="light"><head><style>${readUiCss()}</style></head><body>${controlsHtml()}</body></html>`,
);
return page;
}
describeBrowserLayout("touch-primary form controls", () => {
beforeAll(async () => {
browser = await chromium.launch({ headless: true });
});
afterAll(async () => {
await browser.close();
});
it("keeps text-entry controls large enough to avoid mobile focus zoom", async () => {
const page = await openMobileFixture();
try {
const metrics = await page.evaluate(() => {
const selectors = [
".field input",
".field textarea",
".field select",
".config-search__input",
".settings-theme-import__input",
".config-raw-field textarea",
".cfg-input",
".cfg-input--sm",
".cfg-textarea",
".cfg-textarea--sm",
".cfg-number__input",
".cfg-select",
".usage-date-input",
".usage-select",
".usage-query-input",
'.usage-filters-inline input[type="text"]',
".usage-filters-inline select",
".agent-chat__composer-combobox > textarea",
".chat-compose .chat-compose__field textarea",
];
return {
touchPrimary: matchMedia("(hover: none) and (pointer: coarse)").matches,
sizes: selectors.map((selector) => {
const node = document.querySelector(selector);
if (!(node instanceof HTMLElement)) {
throw new Error(`Missing control ${selector}`);
}
return {
selector,
fontSize: Number.parseFloat(getComputedStyle(node).fontSize),
};
}),
};
});
expect(metrics.touchPrimary).toBe(true);
for (const size of metrics.sizes) {
expect(size.fontSize, size.selector).toBeGreaterThanOrEqual(16);
}
} finally {
await page.close();
}
});
it("keeps native select affordances visible in light mode", async () => {
const page = await openMobileFixture();
try {
const selects = await page.locator(".cfg-select, .field select").evaluateAll((nodes) =>
nodes.map((node) => {
const style = getComputedStyle(node as HTMLElement);
return {
image: style.backgroundImage,
paddingRight: Number.parseFloat(style.paddingRight),
repeat: style.backgroundRepeat,
};
}),
);
expect(selects).toHaveLength(2);
for (const select of selects) {
expect(select.image).not.toBe("none");
expect(select.paddingRight).toBeGreaterThanOrEqual(32);
expect(select.repeat).toContain("no-repeat");
}
} finally {
await page.close();
}
});
});

View File

@@ -15,6 +15,7 @@ const sharedUiTestConfig = {
} as const;
const nodeDrivenBrowserLayoutTests = [
"src/ui/chat/chat-responsive.browser.test.ts",
"src/ui/form-controls.browser.test.ts",
"src/ui/views/sessions.browser.test.ts",
] as const;