diff --git a/ui/src/styles/chat/layout.test.ts b/ui/src/styles/chat/layout.test.ts deleted file mode 100644 index 052b7eb454a..00000000000 --- a/ui/src/styles/chat/layout.test.ts +++ /dev/null @@ -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);"); - }); -}); diff --git a/ui/src/styles/chat/text.test.ts b/ui/src/styles/chat/text.test.ts deleted file mode 100644 index bf319e955b0..00000000000 --- a/ui/src/styles/chat/text.test.ts +++ /dev/null @@ -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);"); - }); -}); diff --git a/ui/src/styles/chat/tool-cards.test.ts b/ui/src/styles/chat/tool-cards.test.ts deleted file mode 100644 index 5c6a8afd831..00000000000 --- a/ui/src/styles/chat/tool-cards.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/ui/src/styles/components.test.ts b/ui/src/styles/components.test.ts deleted file mode 100644 index 2c85ab1d573..00000000000 --- a/ui/src/styles/components.test.ts +++ /dev/null @@ -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%;"); - }); -}); diff --git a/ui/src/styles/config-quick.test.ts b/ui/src/styles/config-quick.test.ts deleted file mode 100644 index 3a55169a438..00000000000 --- a/ui/src/styles/config-quick.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/ui/src/styles/config.test.ts b/ui/src/styles/config.test.ts deleted file mode 100644 index 427a46e7e0c..00000000000 --- a/ui/src/styles/config.test.ts +++ /dev/null @@ -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'[^"]*"\);/, - ); - }); -}); diff --git a/ui/src/styles/layout.mobile.test.ts b/ui/src/styles/layout.mobile.test.ts deleted file mode 100644 index 3352676f536..00000000000 --- a/ui/src/styles/layout.mobile.test.ts +++ /dev/null @@ -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', - ); - }); -}); diff --git a/ui/src/styles/markdown-preview.test.ts b/ui/src/styles/markdown-preview.test.ts deleted file mode 100644 index 426803de60f..00000000000 --- a/ui/src/styles/markdown-preview.test.ts +++ /dev/null @@ -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;/, - ); - }); -}); diff --git a/ui/src/styles/usage.test.ts b/ui/src/styles/usage.test.ts deleted file mode 100644 index 5da20f00984..00000000000 --- a/ui/src/styles/usage.test.ts +++ /dev/null @@ -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;/, - ); - }); -}); diff --git a/ui/src/styles/workboard.test.ts b/ui/src/styles/workboard.test.ts deleted file mode 100644 index 3ac71790d8e..00000000000 --- a/ui/src/styles/workboard.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/ui/src/ui/form-controls.browser.test.ts b/ui/src/ui/form-controls.browser.test.ts new file mode 100644 index 00000000000..0e6bef2fbd2 --- /dev/null +++ b/ui/src/ui/form-controls.browser.test.ts @@ -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 ` +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+ `; +} + +async function openMobileFixture(): Promise { + const page = await browser.newPage({ + hasTouch: true, + isMobile: true, + viewport: { width: 390, height: 844 }, + }); + await page.setContent( + `${controlsHtml()}`, + ); + 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(); + } + }); +}); diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts index 3ff6dd9a69b..20b98dd90c4 100644 --- a/ui/vitest.config.ts +++ b/ui/vitest.config.ts @@ -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;