mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 20:44:07 +00:00
test(ui): remove stylesheet grep tests (#88847)
This commit is contained in:
@@ -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);");
|
||||
});
|
||||
});
|
||||
@@ -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);");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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%;");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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'[^"]*"\);/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
143
ui/src/ui/form-controls.browser.test.ts
Normal file
143
ui/src/ui/form-controls.browser.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user