| 1 | // Run: tsx src/__tests__/typography-overflow-contract.test.ts |
| 2 | |
| 3 | import { readFileSync } from "node:fs"; |
| 4 | import { dirname, resolve } from "node:path"; |
| 5 | import { fileURLToPath } from "node:url"; |
| 6 | import { TEXT_SIZES } from "../lib/textSize"; |
| 7 | |
| 8 | const testDir = dirname(fileURLToPath(import.meta.url)); |
| 9 | const styles = [ |
| 10 | readFileSync(resolve(testDir, "../styles.css"), "utf8"), |
| 11 | readFileSync(resolve(testDir, "../components/CompactRatioSettings.css"), "utf8"), |
| 12 | ].join("\n").replace(/\/\*[\s\S]*?\*\//g, ""); |
| 13 | |
| 14 | let passed = 0; |
| 15 | let failed = 0; |
| 16 | |
| 17 | function ok(value: unknown, label: string) { |
| 18 | if (value) { |
| 19 | process.stdout.write(` PASS ${label}\n`); |
| 20 | passed += 1; |
| 21 | } else { |
| 22 | process.stdout.write(` FAIL ${label}\n`); |
| 23 | failed += 1; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | function eq(a: unknown, b: unknown, label: string) { |
| 28 | if (a === b) { |
| 29 | process.stdout.write(` PASS ${label}\n`); |
| 30 | passed += 1; |
| 31 | } else { |
| 32 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`); |
| 33 | failed += 1; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | function matchingBlocks(selector: string): string[] { |
| 38 | const blocks: string[] = []; |
| 39 | const rule = /([^{}]+)\{([^{}]*)\}/g; |
| 40 | let match: RegExpExecArray | null; |
| 41 | while ((match = rule.exec(styles)) !== null) { |
| 42 | const selectors = match[1].split(",").map((part) => part.trim()); |
| 43 | if (selectors.includes(selector)) blocks.push(match[2]); |
| 44 | } |
| 45 | return blocks; |
| 46 | } |
| 47 | |
| 48 | function finalDeclaration(selector: string, property: string): string | undefined { |
| 49 | let value: string | undefined; |
| 50 | for (const block of matchingBlocks(selector)) { |
| 51 | const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g"); |
| 52 | let match: RegExpExecArray | null; |
| 53 | while ((match = declaration.exec(block)) !== null) { |
| 54 | value = match[1].trim(); |
| 55 | } |
| 56 | } |
| 57 | return value; |
| 58 | } |
| 59 | |
| 60 | function hasDeclaration(selector: string, property: string, expected: string): boolean { |
| 61 | return matchingBlocks(selector).some((block) => { |
| 62 | const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g"); |
| 63 | let match: RegExpExecArray | null; |
| 64 | while ((match = declaration.exec(block)) !== null) { |
| 65 | if (match[1].trim() === expected) return true; |
| 66 | } |
| 67 | return false; |
| 68 | }); |
| 69 | } |
| 70 | |
| 71 | function clipsSingleLine(selector: string) { |
| 72 | eq(finalDeclaration(selector, "overflow"), "hidden", `${selector} clips long text`); |
| 73 | eq(finalDeclaration(selector, "text-overflow"), "ellipsis", `${selector} uses ellipsis`); |
| 74 | eq(finalDeclaration(selector, "white-space"), "nowrap", `${selector} stays on one line`); |
| 75 | } |
| 76 | |
| 77 | console.log("\ntypography overflow contract"); |
| 78 | |
| 79 | eq( |
| 80 | JSON.stringify(TEXT_SIZES), |
| 81 | JSON.stringify(["small", "default", "large", "xlarge", "xxlarge"]), |
| 82 | "text-size presets include the large accessibility step", |
| 83 | ); |
| 84 | eq(finalDeclaration(":root", "--sans"), "var(--font-ui)", "legacy sans alias stays synced with UI font"); |
| 85 | eq(finalDeclaration(':root[data-text-size="xxlarge"]', "--font-scale"), "1.32", "xxlarge has a real scale bump"); |
| 86 | ok( |
| 87 | (finalDeclaration(":root", "--statusbar-dock-height") ?? "").includes("var(--font-scale)"), |
| 88 | "status bar dock height scales with interface text size", |
| 89 | ); |
| 90 | ok( |
| 91 | hasDeclaration(".layout", "--statusbar-height", "var(--statusbar-dock-height)"), |
| 92 | "layout reserves scaled status bar height", |
| 93 | ); |
| 94 | eq( |
| 95 | finalDeclaration(".app", "height"), |
| 96 | "var(--app-viewport-height, 100%)", |
| 97 | "app height follows the live viewport height variable", |
| 98 | ); |
| 99 | eq(finalDeclaration(".transcript--empty", "overflow-y"), "auto", "empty transcript can scroll instead of clipping"); |
| 100 | eq(finalDeclaration(".welcome", "overflow"), "visible", "welcome empty state is not clipped by its own box"); |
| 101 | ok( |
| 102 | hasDeclaration(".transcript--empty > .welcome", "margin-block", "auto"), |
| 103 | "empty-state auto margins apply only to the welcome content", |
| 104 | ); |
| 105 | ok( |
| 106 | finalDeclaration(".transcript--empty > *", "margin-block") === undefined, |
| 107 | "empty-state generic children do not receive auto margins", |
| 108 | ); |
| 109 | eq( |
| 110 | finalDeclaration(":root[data-theme-style] .statusbar", "height"), |
| 111 | "var(--statusbar-dock-height)", |
| 112 | "fixed status bar height follows the scaled dock token", |
| 113 | ); |
| 114 | eq( |
| 115 | finalDeclaration(":root[data-theme-style] .statusbar", "min-height"), |
| 116 | "var(--statusbar-dock-height)", |
| 117 | "status bar min-height follows the scaled dock token", |
| 118 | ); |
| 119 | eq(finalDeclaration(".provider-template-grid", "grid-auto-rows"), "92px", "provider preset cards use compact equal-height grid rows"); |
| 120 | eq(finalDeclaration(".provider-template-card", "height"), "100%", "provider preset cards stretch to the grid row height"); |
| 121 | eq(finalDeclaration(".provider-template-card strong", "-webkit-line-clamp"), "1", "provider preset card titles clamp to one line"); |
| 122 | eq(finalDeclaration(".provider-template-card span", "-webkit-line-clamp"), "2", "provider preset card descriptions clamp to two lines"); |
| 123 | eq(finalDeclaration(".provider-model-draft__list", "grid-auto-rows"), "min-content", "provider model rows grow with their content"); |
| 124 | eq(finalDeclaration(".provider-model-draft__option", "min-height"), undefined, "provider model cards do not force undersized rows"); |
| 125 | eq(finalDeclaration(".provider-model-draft__option", "overflow"), "hidden", "provider model cards contain overflowing controls"); |
| 126 | eq(finalDeclaration(".compact-ratio-presets", "width"), "100%", "compaction presets use the full settings control width"); |
| 127 | eq(finalDeclaration(".compact-ratio-presets .set-seg__btn", "flex"), "1 1 0", "three compaction presets share the available width equally"); |
| 128 | eq(finalDeclaration(".compact-ratio-presets .set-seg__btn", "flex-direction"), "column", "compaction presets place percentage and strategy on separate lines"); |
| 129 | eq(finalDeclaration(".compact-ratio-presets .set-seg__btn", "min-height"), "44px", "two-line compaction presets keep a stable target height"); |
| 130 | eq(finalDeclaration(".compact-ratio-presets .set-seg__btn", "white-space"), "normal", "compaction labels do not depend on ellipsis for their meaning"); |
| 131 | |
| 132 | eq(finalDeclaration(".statusbar", "white-space"), "nowrap", "status bar keeps metrics on one row"); |
| 133 | eq(finalDeclaration(".statusbar", "overflow"), "hidden", "status bar clips instead of overflowing"); |
| 134 | clipsSingleLine(".statusbar__model"); |
| 135 | |
| 136 | for (const selector of [ |
| 137 | ".sidebar-im__summary-label", |
| 138 | ".sidebar-im__summary-status", |
| 139 | ".workbench-dock__tab-label", |
| 140 | ".workspace-files__scope-title", |
| 141 | ".workspace-files__scope-meta", |
| 142 | ".context-panel__section-head span", |
| 143 | ".context-panel__metric span", |
| 144 | ".context-panel__metric strong", |
| 145 | ".app--creation .context-panel__mini-stat span", |
| 146 | ".app--creation .context-panel__mini-stat strong", |
| 147 | ".topbar__model", |
| 148 | ".composer-modebar__item span", |
| 149 | ".composer-more-menu__item span", |
| 150 | ]) { |
| 151 | clipsSingleLine(selector); |
| 152 | } |
| 153 | |
| 154 | eq( |
| 155 | finalDeclaration(".app--creation .layout.layout--workspace-open", "transition"), |
| 156 | "grid-template-columns 0s, min-width 0s", |
| 157 | "creation dock skips zero-width grid interpolation on open", |
| 158 | ); |
| 159 | eq( |
| 160 | finalDeclaration(".app--creation .context-panel__usage", "animation"), |
| 161 | "none", |
| 162 | "creation overview usage card disables inherited entrance animation", |
| 163 | ); |
| 164 | ok( |
| 165 | finalDeclaration(".app--creation .context-panel__mini-stat", "justify-content") !== "space-between", |
| 166 | "creation overview rows avoid edge-pinned value alignment", |
| 167 | ); |
| 168 | ok( |
| 169 | finalDeclaration(".app--creation .context-panel__mini-stat", "grid-template-columns") !== "minmax(0, 1fr) auto", |
| 170 | "creation overview rows avoid the spacer grid that pushes values to the edge", |
| 171 | ); |
| 172 | ok( |
| 173 | finalDeclaration(".app--creation .context-panel__mini-stat strong", "max-width") !== "14ch", |
| 174 | "creation overview values are not capped to a fixed 14ch width", |
| 175 | ); |
| 176 | |
| 177 | eq(finalDeclaration(".composer-modebar", "overflow"), "hidden", "chat mode switcher contains enlarged labels"); |
| 178 | eq(finalDeclaration(".composer-meta__control--profile", "flex"), "0 0 auto", "work mode selector sizes to its localized label"); |
| 179 | eq(finalDeclaration(".composer-meta__control--profile", "max-width"), "68px", "work mode selector keeps a compact narrow-width bound"); |
| 180 | eq(finalDeclaration(".composer-profile-trigger__label", "overflow"), "hidden", "work mode selector clips only when space is constrained"); |
| 181 | eq(finalDeclaration(".composer-profile-trigger__label", "text-overflow"), "ellipsis", "work mode selector shows an ellipsis when constrained"); |
| 182 | eq(finalDeclaration(".composer-meta__control--intent", "max-width"), "72px", "task method selector keeps its current state visible at narrow widths"); |
| 183 | eq(finalDeclaration(".composer-task-mode-trigger__value", "text-overflow"), "ellipsis", "task method selector truncates its value only when constrained"); |
| 184 | eq(finalDeclaration(".composer-meta .modelsw__trigger", "font-weight"), "var(--composer-control-font-weight)", "model selector uses the shared control weight"); |
| 185 | eq(finalDeclaration(".composer-meta__divider", "height"), "18px", "execution policy and model settings have a compact visual divider"); |
| 186 | ok( |
| 187 | /@container \(max-width: 560px\)\s*\{[\s\S]*?\.composer-meta__control--more\s*\{[\s\S]*?flex-basis:\s*38px;/.test(styles), |
| 188 | "composer enters icon-only mode before model and effort controls overlap", |
| 189 | ); |
| 190 | ok( |
| 191 | /@container \(max-width: 760px\)\s*\{[\s\S]*?\.composer-meta__control--approval \.composer-modebar--approval\s*\{[^}]*flex:\s*1 1 auto;[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/.test(styles), |
| 192 | "approval mode switcher shrinks with its compact composer container", |
| 193 | ); |
| 194 | eq(finalDeclaration(".composer-modebar--approval", "--composer-modebar-active-bg"), "var(--mode-auto-bg)", "ask approval restores the solid semantic fill"); |
| 195 | eq(finalDeclaration('.composer-modebar--approval[data-mode="auto"]', "--composer-modebar-active-fg"), "#fff", "auto approval keeps high-contrast text on its solid fill"); |
| 196 | eq(finalDeclaration('.composer-modebar--approval[data-mode="yolo"]', "--composer-modebar-active-bg"), "var(--mode-yolo-bg)", "yolo approval restores the solid warning fill"); |
| 197 | eq(finalDeclaration(".composer-intent-menu", "width"), "min(284px, calc(100vw - 16px))", "task method menu uses the shared menu width"); |
| 198 | eq(finalDeclaration(".composer-profile-menu", "width"), "min(284px, calc(100vw - 16px))", "work mode menu uses the shared menu width"); |
| 199 | eq(finalDeclaration(".composer-access-menu__desc", "white-space"), "normal", "menu descriptions can wrap onto a second line"); |
| 200 | eq(finalDeclaration(".composer-access-menu__desc", "text-overflow"), "clip", "menu descriptions no longer use single-line ellipsis"); |
| 201 | eq(finalDeclaration(".composer-profile-menu .composer-access-menu__desc", "font-size"), "12px", "work mode summaries use the shared control text size"); |
| 202 | eq(finalDeclaration(".composer-profile-menu .composer-access-menu__desc", "color"), "var(--fg-dim)", "work mode summaries remain readable as secondary text"); |
| 203 | eq(finalDeclaration(".composer-profile-menu .composer-access-menu__desc", "white-space"), "nowrap", "work mode summaries stay on one scannable line"); |
| 204 | eq(finalDeclaration(".composer-task-mode-trigger:focus-visible", "box-shadow"), "var(--focus-ring)", "task method selector uses the shared keyboard focus ring"); |
| 205 | eq(finalDeclaration(".composer-profile-trigger:focus-visible", "box-shadow"), "var(--focus-ring)", "work mode selector uses the shared keyboard focus ring"); |
| 206 | eq(finalDeclaration(".composer-meta .modelsw__trigger:focus-visible", "box-shadow"), "var(--focus-ring)", "model and effort selectors use the shared keyboard focus ring"); |
| 207 | eq(finalDeclaration(":root[data-theme-style] .composer-modebar__item--active:focus-visible", "box-shadow"), "var(--focus-ring)", "active permission options retain keyboard focus feedback"); |
| 208 | eq( |
| 209 | finalDeclaration(".app--creation .msg--assistant .msg__body", "font-size"), |
| 210 | "var(--font-content)", |
| 211 | "creation assistant body text follows the conversation text size", |
| 212 | ); |
| 213 | eq( |
| 214 | finalDeclaration(":root[data-theme-style] .msg--assistant .msg__body", "font-size"), |
| 215 | "var(--font-content)", |
| 216 | "themed assistant body text follows the conversation text size", |
| 217 | ); |
| 218 | eq( |
| 219 | finalDeclaration(".app--creation .msg--assistant .msg__body", "font-family"), |
| 220 | "var(--font-content-family)", |
| 221 | "creation assistant body follows the conversation font family", |
| 222 | ); |
| 223 | eq( |
| 224 | finalDeclaration(".app--creation .md", "font-family"), |
| 225 | "var(--font-content-family)", |
| 226 | "creation markdown follows the conversation font family", |
| 227 | ); |
| 228 | eq( |
| 229 | finalDeclaration(".app--creation .composer__input", "font-size"), |
| 230 | "var(--font-content)", |
| 231 | "creation composer input follows the composer text size", |
| 232 | ); |
| 233 | eq( |
| 234 | finalDeclaration("body", "--text-base"), |
| 235 | "var(--typography-interface-size, calc(14px * var(--font-scale)))", |
| 236 | "interface text resolves its own exact regional size", |
| 237 | ); |
| 238 | eq( |
| 239 | finalDeclaration(".transcript", "--text-base"), |
| 240 | "var(--typography-conversation-size, calc(14px * var(--font-scale)))", |
| 241 | "conversation text resolves its own exact regional size", |
| 242 | ); |
| 243 | eq( |
| 244 | finalDeclaration(".composer-wrap", "--text-base"), |
| 245 | "var(--typography-composer-size, calc(14px * var(--font-scale)))", |
| 246 | "composer text resolves its own exact regional size", |
| 247 | ); |
| 248 | eq( |
| 249 | finalDeclaration(".code", "--font-code"), |
| 250 | "var(--typography-code-size, calc(12px * var(--font-scale)))", |
| 251 | "code text resolves its own exact regional size", |
| 252 | ); |
| 253 | eq(finalDeclaration(".code", "font-family"), "var(--font-code-family)", "code blocks keep the regional code font"); |
| 254 | eq(finalDeclaration(".md-code", "font-family"), "var(--font-code-family)", "inline code keeps the regional code font"); |
| 255 | eq(finalDeclaration(".code code", "font-family"), "inherit", "nested code text inherits the regional code font"); |
| 256 | eq(finalDeclaration(".code-line-text", "font-family"), "inherit", "line-numbered code text inherits its viewer font"); |
| 257 | eq( |
| 258 | finalDeclaration(".code-lines-wrap", "font-family"), |
| 259 | "var(--typography-code-font, var(--font-mono))", |
| 260 | "line-numbered code viewers use the regional code font", |
| 261 | ); |
| 262 | eq( |
| 263 | finalDeclaration(".diff", "font-size"), |
| 264 | "var(--typography-code-size, calc(12.5px * var(--global-font-scale)))", |
| 265 | "diff text follows the global scale until the code region is customized", |
| 266 | ); |
| 267 | eq(finalDeclaration(".msg-meta", "font-size"), "var(--font-status)", "message metadata keeps its regional size"); |
| 268 | eq( |
| 269 | finalDeclaration(".composer-meta", "font-family"), |
| 270 | "var(--font-metadata-family)", |
| 271 | "composer metadata keeps the regional font", |
| 272 | ); |
| 273 | eq(finalDeclaration(".statusbar", "font-family"), "var(--font-metadata-family)", "status bar keeps the regional font"); |
| 274 | eq( |
| 275 | finalDeclaration(".typography-settings__preview", "--preview-size"), |
| 276 | "var(--typography-conversation-size, calc(14px * var(--global-font-scale)))", |
| 277 | "conversation preview uses the exact conversation size", |
| 278 | ); |
| 279 | eq( |
| 280 | finalDeclaration(".typography-settings__preview--interface", "--preview-size"), |
| 281 | "var(--typography-interface-size, calc(14px * var(--global-font-scale)))", |
| 282 | "interface preview uses the exact interface size", |
| 283 | ); |
| 284 | eq( |
| 285 | finalDeclaration(".typography-settings__preview--composer", "--preview-size"), |
| 286 | "var(--typography-composer-size, calc(14px * var(--global-font-scale)))", |
| 287 | "composer preview uses the exact composer size", |
| 288 | ); |
| 289 | eq( |
| 290 | finalDeclaration(".typography-settings__preview--code", "--preview-size"), |
| 291 | "var(--typography-code-size, calc(12px * var(--global-font-scale)))", |
| 292 | "code preview uses the exact code size", |
| 293 | ); |
| 294 | eq( |
| 295 | finalDeclaration(".typography-settings__preview--metadata", "--preview-size"), |
| 296 | "var(--typography-metadata-size, calc(12px * var(--global-font-scale)))", |
| 297 | "metadata preview uses the exact supporting-text size", |
| 298 | ); |
| 299 | eq( |
| 300 | finalDeclaration(".typography-settings__preview-body", "font-size"), |
| 301 | "var(--preview-size)", |
| 302 | "live preview renders the selected region's exact size", |
| 303 | ); |
| 304 | eq( |
| 305 | finalDeclaration(".app--creation .reasoning__body", "font-family"), |
| 306 | "var(--font-content-family)", |
| 307 | "creation reasoning keeps the conversation font", |
| 308 | ); |
| 309 | eq( |
| 310 | finalDeclaration(".app--creation .tool__name", "font-family"), |
| 311 | "var(--font-code-family)", |
| 312 | "creation tool names keep the code font", |
| 313 | ); |
| 314 | ok( |
| 315 | !/\.app--creation[^{]*\{[^}]*font-size:\s*[0-9.]+px\s*(?:!important\s*)?;/.test(styles), |
| 316 | "creation rules do not hardcode bare px font sizes (except font-size:0)", |
| 317 | ); |
| 318 | eq( |
| 319 | finalDeclaration(".context-ring-popover__title", "font-size"), |
| 320 | "calc(14px * var(--font-scale))", |
| 321 | "creation context-ring popover (portaled to body) follows interface text size", |
| 322 | ); |
| 323 | ok( |
| 324 | !/\.context-ring-popover[^{]*\{[^}]*font-size:\s*[0-9.]+px\s*(?:!important\s*)?;/.test(styles), |
| 325 | "context-ring popover rules do not hardcode bare px font sizes (except font-size:0)", |
| 326 | ); |
| 327 | eq( |
| 328 | finalDeclaration(".app--creation .tool:not(.tool--open) > .tool__body", "height"), |
| 329 | "0 !important", |
| 330 | "collapsed creation tool bodies keep mounted content clipped", |
| 331 | ); |
| 332 | eq( |
| 333 | finalDeclaration(".app--creation .tool:not(.tool--open) > .tool__body", "visibility"), |
| 334 | "hidden", |
| 335 | "collapsed creation tool bodies do not paint hidden tool text", |
| 336 | ); |
| 337 | ok( |
| 338 | /@container\s*\(max-width:\s*760px\)[\s\S]*?\.composer-meta__control--model\s*\{[\s\S]*?flex\s*:\s*0 1 auto[\s\S]*?width\s*:\s*fit-content[\s\S]*?max-width\s*:\s*min\(240px,\s*42vw\)[\s\S]*?\.composer-meta__control--profile\s*\{[\s\S]*?max-width\s*:\s*126px[\s\S]*?\.composer-meta__control--intent\s*\{[\s\S]*?max-width\s*:\s*128px[\s\S]*?\.composer-meta__control--effort\s*\{[\s\S]*?display\s*:\s*none[\s\S]*?\.composer-meta__control--more\s*\{[\s\S]*?display\s*:\s*inline-flex/.test(styles), |
| 339 | "composer compact controls activate at the capped theme width", |
| 340 | ); |
| 341 | eq(finalDeclaration(".md table", "overflow-x"), "auto", "markdown tables scroll horizontally"); |
| 342 | eq(finalDeclaration(".code", "overflow"), "auto", "code blocks scroll instead of widening the layout"); |
| 343 | ok( |
| 344 | /@media\s*\(max-width:\s*900px\)[\s\S]*?\.settings-center\s*\{[\s\S]*?grid-template-columns\s*:\s*1fr/.test(styles), |
| 345 | "settings center stacks navigation before the modal is too narrow", |
| 346 | ); |
| 347 | ok( |
| 348 | /@media\s*\(max-width:\s*900px\)[\s\S]*?\.settings-field\s*\{[\s\S]*?grid-template-columns\s*:\s*1fr/.test(styles), |
| 349 | "settings fields collapse to one column at the mid-width breakpoint", |
| 350 | ); |
| 351 | ok( |
| 352 | /@media\s*\(max-width:\s*760px\)[\s\S]*?\.settings-modal\s*\{[\s\S]*?width\s*:\s*100vw[\s\S]*?height\s*:\s*100vh/.test(styles), |
| 353 | "settings modal only becomes fullscreen at the narrow breakpoint", |
| 354 | ); |
| 355 | ok( |
| 356 | /@media\s*\(max-width:\s*820px\)[\s\S]*?\.app\s+\.layout[\s\S]*?grid-template-columns\s*:\s*minmax\(0,\s*1fr\)\s*!important[\s\S]*?\.app\s+\.sidebar[\s\S]*?display\s*:\s*none\s*!important[\s\S]*?\.app\s+\.chat-pane[\s\S]*?grid-column\s*:\s*1\s*!important/.test(styles), |
| 357 | "narrow workbench layout hides side panels and keeps chat single-column", |
| 358 | ); |
| 359 | |
| 360 | for (const selector of [ |
| 361 | ".reasoning__head", |
| 362 | ".turn-collapse__reasoning-head", |
| 363 | ".process-card__head", |
| 364 | ".tool__difflabel", |
| 365 | ".msg-memory-citations", |
| 366 | ".msg-memory-citations__source", |
| 367 | ".msg-memory-citations__note", |
| 368 | ".msg-attachment__name", |
| 369 | ".msg-attachment__meta", |
| 370 | ".msg-pasted-head", |
| 371 | ".msg-pasted-expanded", |
| 372 | ".msg-edit__input", |
| 373 | ".msg-edit__btn", |
| 374 | ".msg__send-failed", |
| 375 | ":root[data-theme-style] .process-card__kind", |
| 376 | ':root[data-theme-style] .msg--assistant > .process-card[data-tone="violet"] .process-card__name', |
| 377 | ]) { |
| 378 | const size = finalDeclaration(selector, "font-size"); |
| 379 | ok(size !== undefined && !/^[0-9.]+px$/.test(size), `${selector} font size follows the text-size scale`); |
| 380 | } |
| 381 | |
| 382 | console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`); |
| 383 | if (failed > 0) process.exit(1); |
| 384 |