diff --git a/frontend/src/__tests__/typography.test.ts b/frontend/src/__tests__/typography.test.ts new file mode 100644 index 000000000..b5fd83b6a --- /dev/null +++ b/frontend/src/__tests__/typography.test.ts @@ -0,0 +1,49 @@ +/** + * Typography foundation (issue #741): body line-height, form controls that + * inherit the page font, a base heading scale on the --cudly-fs-* tokens, and + * no literal sizes or weights that duplicate a token. + */ +import fs from 'fs'; +import path from 'path'; + +const stylesDir = path.join(__dirname, '..', 'styles'); +const read = (file: string): string => fs.readFileSync(path.join(stylesDir, file), 'utf8'); +const files = fs.readdirSync(stylesDir).filter(file => file.endsWith('.css')); + +/** Body of the first rule whose selector list matches `selector` exactly. */ +function ruleBody(css: string, selector: RegExp): string { + const match = new RegExp(`(?:^|\\})\\s*${selector.source}\\s*\\{([^}]*)\\}`, 'm').exec(css); + if (!match) throw new Error(`no rule for ${selector.source}`); + return match[1]!; +} + +describe('Typography foundation (issue #741)', () => { + const base = read('base.css'); + + test('body sets the normal line-height token', () => { + expect(ruleBody(base, /body/)).toMatch(/line-height:\s*var\(--cudly-lh-normal\)/); + }); + + test('form controls inherit the page font at the button size', () => { + const body = ruleBody(base, /button,\s*input,\s*select,\s*textarea/); + expect(body).toMatch(/font:\s*inherit/); + expect(body).toMatch(/font-size:\s*var\(--cudly-fs-sm\)/); + }); + + test.each([ + ['h1', 'fs-2xl'], + ['h2', 'fs-xl'], + ['h3', 'fs-lg'], + ['h4', 'fs-md'], + ['h5, h6', 'fs-sm'], + ])('%s has a zero-specificity base size on --cudly-%s', (tags, token) => { + const selector = new RegExp(`:where\\(${tags.replace(/ /g, '\\s*')}\\)`); + expect(ruleBody(base, selector)).toContain(`var(--cudly-${token})`); + }); + + test.each(files)('%s has no literal font size or weight that duplicates a token', file => { + const css = read(file); + expect(css).not.toMatch(/font-size:\s*(0\.75|0\.875|1|1\.125|1\.375|1\.75|2\.25)rem\s*;/); + expect(css).not.toMatch(/font-weight:\s*(bold|400|500|600|700)\s*;/); + }); +}); diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css index 2efe48a61..2ea7053c1 100644 --- a/frontend/src/styles/base.css +++ b/frontend/src/styles/base.css @@ -107,10 +107,41 @@ html { body { font-family: var(--cudly-font-base); + line-height: var(--cudly-lh-normal); background: var(--cudly-bg); color: var(--cudly-text); } +/* Form controls do not inherit the page font by default (inputs fall back + * to 13.33px system text next to 14px buttons). Inherit family and weight, + * then pin the size to the button size and keep the line-height tight so + * control heights stay as they were. Element selectors, so the existing + * `select`, `button` and class rules later in the cascade still win. */ +button, +input, +select, +textarea { + font: inherit; + font-size: var(--cudly-fs-sm); + line-height: var(--cudly-lh-tight); +} + +/* Base heading scale on the type tokens, close to the browser defaults it + * replaces (h1 32px -> 28px, h2 24px -> 22px, h5/h6 grow to 14px). Weight + * stays the browser's bold so headings that only set a size keep their + * look. :where() keeps it at zero specificity so every contextual heading + * rule (.card h3, .page-hero h2, ...) still overrides it. */ +:where(h1, h2, h3, h4, h5, h6) { + line-height: var(--cudly-lh-tight); + font-weight: var(--cudly-fw-bold); +} + +:where(h1) { font-size: var(--cudly-fs-2xl); } +:where(h2) { font-size: var(--cudly-fs-xl); } +:where(h3) { font-size: var(--cudly-fs-lg); } +:where(h4) { font-size: var(--cudly-fs-md); } +:where(h5, h6) { font-size: var(--cudly-fs-sm); } + /* Default keyboard focus ring. Zero specificity (:where) so a component that * draws its own focus style keeps it; everything else gets a visible ring. */ :where(a[href], button, input, select, textarea, summary, [tabindex]):focus-visible { @@ -234,7 +265,7 @@ body { /* Help text */ .help-text { color: var(--cudly-text-muted); - font-size: 0.875rem; + font-size: var(--cudly-fs-sm); margin-bottom: 0.75rem; } diff --git a/frontend/src/styles/charts.css b/frontend/src/styles/charts.css index ee4d34850..d41dd5ba2 100644 --- a/frontend/src/styles/charts.css +++ b/frontend/src/styles/charts.css @@ -108,12 +108,12 @@ .ladder-timeline-controls .form-group label { margin-bottom: var(--cudly-sp-1); font-size: var(--cudly-fs-xs); - font-weight: 500; + font-weight: var(--cudly-fw-medium); color: var(--cudly-text-muted); } .ladder-timeline-controls input[data-date] { width: 11.5rem; font-variant-numeric: tabular-nums; } .ladder-timeline-state { margin: 0 0 var(--cudly-sp-3); } -.ladder-timeline-stat { margin: var(--cudly-sp-3) 0 var(--cudly-sp-1); font-weight: 600; } +.ladder-timeline-stat { margin: var(--cudly-sp-3) 0 var(--cudly-sp-1); font-weight: var(--cudly-fw-semibold); } .ladder-timeline-note { margin: 0 0 var(--cudly-sp-2); font-size: var(--cudly-fs-sm); color: var(--cudly-text-muted); } .ladder-timeline-chart { height: 320px; margin: 1rem 0; } #ladder-purchase-timeline [data-table] { overflow-x: auto; } diff --git a/frontend/src/styles/components.css b/frontend/src/styles/components.css index 3e76f543c..8e6ebee72 100644 --- a/frontend/src/styles/components.css +++ b/frontend/src/styles/components.css @@ -87,13 +87,13 @@ .card h3 { color: var(--cudly-text-muted); - font-size: 0.875rem; + font-size: var(--cudly-fs-sm); margin-bottom: 0.5rem; } .card .value { - font-size: 1.75rem; - font-weight: bold; + font-size: var(--cudly-fs-2xl); + font-weight: var(--cudly-fw-bold); } .card .monthly { @@ -123,7 +123,7 @@ button { background: var(--cudly-surface); color: var(--cudly-text); cursor: pointer; - font-size: 0.875rem; + font-size: var(--cudly-fs-sm); transition: background var(--cudly-dur) var(--cudly-ease), border-color var(--cudly-dur) var(--cudly-ease); } @@ -195,7 +195,7 @@ button.secondary:hover { background: var(--cudly-surface); color: var(--cudly-text); cursor: pointer; - font-size: 0.875rem; + font-size: var(--cudly-fs-sm); transition: background var(--cudly-dur) var(--cudly-ease), border-color var(--cudly-dur) var(--cudly-ease); } @@ -389,7 +389,7 @@ button.secondary:hover { .toast--error { border-left-color: var(--cudly-error-fg); } .toast-icon { - font-weight: bold; + font-weight: var(--cudly-fw-bold); font-size: 1.1rem; line-height: 1.2; } @@ -454,7 +454,7 @@ button.secondary:hover { .btn-download .download-icon { margin-right: 0.4rem; - font-weight: bold; + font-weight: var(--cudly-fw-bold); color: var(--cudly-primary); } @@ -559,7 +559,7 @@ button.secondary:hover { .accounts-overview-chip-count { font-size: 1.4rem; - font-weight: 700; + font-weight: var(--cudly-fw-bold); color: var(--cudly-primary); } @@ -598,7 +598,7 @@ button.secondary:hover { background: var(--cudly-info-bg); border-color: #4a90e2; color: #1a56c4; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } /* Per-provider accounts table (P3). Replaces the prior inline-row @@ -622,7 +622,7 @@ button.secondary:hover { .accounts-table th { font-size: 0.85rem; color: var(--cudly-text-muted); - font-weight: 500; + font-weight: var(--cudly-fw-medium); background: #fafafa; } @@ -714,7 +714,7 @@ th .column-filter-btn:hover { } th .column-filter-btn.active { color: var(--cudly-primary); - font-weight: 700; + font-weight: var(--cudly-fw-bold); } /* Detached popover (lives in document.body, positioned absolutely). */ @@ -733,7 +733,7 @@ th .column-filter-btn.active { .column-filter-popover .column-filter-heading { margin: 0 0 0.5rem 0; font-size: 0.9rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } .column-filter-popover .column-filter-all { display: flex; @@ -742,7 +742,7 @@ th .column-filter-btn.active { padding: 0.25rem 0; border-bottom: 1px solid var(--cudly-border); margin-bottom: 0.5rem; - font-weight: 500; + font-weight: var(--cudly-fw-medium); } .column-filter-popover .column-filter-list { max-height: 240px; @@ -959,7 +959,7 @@ tr.recommendation-row:hover { .column-visibility-popover .column-visibility-title { margin: 0 0 0.4rem; font-size: 0.8em; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: #444; text-transform: uppercase; letter-spacing: 0.04em; @@ -1043,7 +1043,7 @@ tr.rec-cell-summary-row:hover { box-shadow: var(--cudly-focus-ring); } .rec-cell-identity { - font-weight: 500; + font-weight: var(--cudly-fw-medium); font-size: 0.9em; } .rec-cell-range { @@ -1142,7 +1142,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { } .account-delete-pending-total { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } /* Approval-details modal (issue #374): when the confirmDialog body @@ -1187,8 +1187,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { } .approval-details-stat-value { - font-size: 1rem; - font-weight: 600; + font-size: var(--cudly-fs-md); + font-weight: var(--cudly-fw-semibold); color: #222; } @@ -1217,7 +1217,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { position: sticky; top: 0; background: var(--cudly-surface-muted); - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text-muted, #444); font-size: 0.78rem; text-transform: uppercase; @@ -1239,8 +1239,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; - font-size: 0.75rem; - font-weight: 600; + font-size: var(--cudly-fs-xs); + font-weight: var(--cudly-fw-semibold); /* No text-transform: let the HTML control casing. Acronyms (AWS, * GCP) render as-written; brand names (Azure) keep their native * title case rather than being SHOUT-CASED by CSS. */ @@ -1266,7 +1266,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; - font-size: 0.75rem; + font-size: var(--cudly-fs-xs); background: var(--cudly-info-bg); color: var(--cudly-primary); } @@ -1276,7 +1276,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } .status-badge.active { @@ -1395,8 +1395,8 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { /* Role badge */ .role-badge { - font-size: 0.75rem; - font-weight: 600; + font-size: var(--cudly-fs-xs); + font-weight: var(--cudly-fw-semibold); padding: 0.15rem 0.5rem; margin-left: 0.4rem; border-radius: 999px; @@ -1410,7 +1410,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { .badge-admin { display: inline-block; font-size: 0.8rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); padding: 0.2rem 0.5rem; border-radius: 4px; background: var(--cudly-info-bg); @@ -1420,7 +1420,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { .badge-user { display: inline-block; font-size: 0.8rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); padding: 0.2rem 0.5rem; border-radius: 4px; background: var(--cudly-border); @@ -1430,7 +1430,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { .badge-readonly { display: inline-block; font-size: 0.8rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); padding: 0.2rem 0.5rem; border-radius: 4px; background: var(--cudly-bg); @@ -1445,7 +1445,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { .badge { display: inline-block; font-size: 0.8rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); padding: 0.2rem 0.55rem; border-radius: 999px; letter-spacing: 0.01em; @@ -1498,7 +1498,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { .history-approver strong { color: var(--cudly-text); - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } /* Upfront Cost cell when the amortize toggle folds the upfront into the @@ -1549,7 +1549,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) { } .copy-btn .copy-icon { - font-size: 1rem; + font-size: var(--cudly-fs-md); } .copy-btn.copied { diff --git a/frontend/src/styles/forms.css b/frontend/src/styles/forms.css index 647e1bf22..e9d44a9b4 100644 --- a/frontend/src/styles/forms.css +++ b/frontend/src/styles/forms.css @@ -5,14 +5,14 @@ select { padding: 0.5rem; border: 1px solid var(--cudly-border-strong); border-radius: 4px; - font-size: 0.9rem; + font-size: var(--cudly-fs-sm); background: white; } label { display: block; margin-bottom: 0.5rem; - font-weight: 500; + font-weight: var(--cudly-fw-medium); color: var(--cudly-text); } @@ -145,7 +145,7 @@ label { .form-group label { display: block; margin-bottom: 0.5rem; - font-weight: 500; + font-weight: var(--cudly-fw-medium); } .form-group input, @@ -164,7 +164,11 @@ label { border-color: var(--cudly-error); } +/* Block, not the inline default: inline it sat on the 16px body + * line box, so with the 1.5 body line-height its wrapped lines were 24px + * apart, and its margin-top had no effect. */ .form-help { + display: block; font-size: 0.8rem; color: var(--cudly-text-muted); margin-top: 0.25rem; @@ -228,7 +232,7 @@ input:checked + .slider:before { } .form-section h3 { - font-size: 1rem; + font-size: var(--cudly-fs-md); color: var(--cudly-primary); margin-bottom: 1rem; } @@ -332,7 +336,7 @@ input:checked + .slider:before { background: var(--cudly-surface-muted); border: 1px solid #dee2e6; border-radius: 4px; - font-size: 0.875rem; + font-size: var(--cudly-fs-sm); } .requirement { @@ -347,11 +351,11 @@ input:checked + .slider:before { width: 1.25rem; height: 1.25rem; margin-right: 0.5rem; - font-weight: bold; + font-weight: var(--cudly-fw-bold); text-align: center; line-height: 1.25rem; border-radius: 50%; - font-size: 0.75rem; + font-size: var(--cudly-fs-xs); } .requirement.unmet { diff --git a/frontend/src/styles/modals.css b/frontend/src/styles/modals.css index 0bea526b4..56a97653d 100644 --- a/frontend/src/styles/modals.css +++ b/frontend/src/styles/modals.css @@ -52,7 +52,7 @@ border: 1px solid var(--cudly-border-strong); border-radius: 4px; margin-top: 0.25rem; - font-size: 0.9rem; + font-size: var(--cudly-fs-sm); } .modal-content textarea { @@ -119,7 +119,7 @@ .modal-hint a { color: var(--cudly-primary); text-decoration: none; - font-weight: 500; + font-weight: var(--cudly-fw-medium); } .modal-hint a:hover { @@ -288,7 +288,7 @@ } .archera-page-lead { - font-size: 1rem; + font-size: var(--cudly-fs-md); color: #555; line-height: 1.6; margin-bottom: 1rem; @@ -353,8 +353,8 @@ text-decoration: none; padding: 0.65rem 1.5rem; border-radius: 4px; - font-size: 1rem; - font-weight: 500; + font-size: var(--cudly-fs-md); + font-weight: var(--cudly-fw-medium); transition: background var(--cudly-dur) var(--cudly-ease); } @@ -468,7 +468,7 @@ padding: 0.55rem 1.1rem; border-radius: 4px; font-size: 0.92rem; - font-weight: 500; + font-weight: var(--cudly-fw-medium); transition: background var(--cudly-dur) var(--cudly-ease); } diff --git a/frontend/src/styles/plans.css b/frontend/src/styles/plans.css index c1d3af16b..ed4cdc40d 100644 --- a/frontend/src/styles/plans.css +++ b/frontend/src/styles/plans.css @@ -70,7 +70,7 @@ } .plan-detail-value { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text); } @@ -126,7 +126,7 @@ .upcoming-date .day { font-size: 1.5rem; - font-weight: bold; + font-weight: var(--cudly-fw-bold); color: var(--cudly-primary); } @@ -162,7 +162,7 @@ .upcoming-savings .amount { font-size: 1.25rem; - font-weight: bold; + font-weight: var(--cudly-fw-bold); color: var(--cudly-success); } @@ -218,7 +218,7 @@ } .ramp-label { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text); margin-bottom: 0.25rem; } @@ -273,7 +273,7 @@ #plan-editor-panel p { line-height: 1.5; margin: 12px 0; } #plan-editor-panel .editor-draft-notice { padding: 12px; background: var(--cudly-info-bg); border-radius: 8px; } #plan-editor-panel .editor-help { margin-bottom: 16px; } -#plan-editor-panel .editor-status { font-weight: 600; min-height: 1.5em; } +#plan-editor-panel .editor-status { font-weight: var(--cudly-fw-semibold); min-height: 1.5em; } #plan-editor-panel .schedule-preview-scroll { min-height: 372px; } #plan-editor-panel .schedule-preview-scroll[hidden] { display: none; } #plan-editor-panel [data-editor="changes"] { margin-top: 16px; border-top: 1px solid var(--cudly-border); } diff --git a/frontend/src/styles/settings.css b/frontend/src/styles/settings.css index 8f94f9e5b..d78fed3d5 100644 --- a/frontend/src/styles/settings.css +++ b/frontend/src/styles/settings.css @@ -29,7 +29,7 @@ .sub-tab-btn.active { color: var(--cudly-primary); border-bottom-color: var(--cudly-primary); - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } /* Visible keyboard focus on sub-tab buttons (Admin sub-tabs + Inventory @@ -77,7 +77,7 @@ } .settings-category legend { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); font-size: 1.1rem; color: var(--cudly-primary); padding: 0 0.5rem; @@ -112,7 +112,7 @@ .setting-info label, .setting-info .label-like { - font-weight: 500; + font-weight: var(--cudly-fw-medium); color: var(--cudly-text); margin: 0; } @@ -185,7 +185,7 @@ /* Subsection title */ .subsection-title { font-size: 0.95rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text); margin: 1.5rem 0 1rem; padding-bottom: 0.5rem; @@ -224,13 +224,13 @@ .service-default-card h5 { margin: 0 0 0.75rem; font-size: 0.9rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text); } .service-default-card .service-default-hint { margin: -0.5rem 0 0.75rem; - font-size: 0.75rem; + font-size: var(--cudly-fs-xs); line-height: 1.3; color: var(--cudly-text-muted); } @@ -313,7 +313,7 @@ select.dirty { } .target-cloud-row label { - font-weight: 500; + font-weight: var(--cudly-fw-medium); color: var(--cudly-text); margin: 0; } @@ -330,7 +330,7 @@ select.dirty { border: 1px solid #c8d6ea; background: #f4f8fd; color: #355273; - font-weight: 500; + font-weight: var(--cudly-fw-medium); } .target-cloud-pills .target-cloud-pill[aria-pressed="true"], @@ -358,7 +358,7 @@ select.dirty { } .federation-format-buttons .btn-multiline .label { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); } .federation-format-buttons .btn-multiline .sublabel { @@ -396,7 +396,7 @@ select.dirty { } .federation-account-name { - font-weight: 600; + font-weight: var(--cudly-fw-semibold); font-size: 0.9rem; white-space: nowrap; overflow: hidden; @@ -498,9 +498,9 @@ select.dirty { /* Permission matrix */ .permission-matrix { width: 100%; border-collapse: collapse; font-size: 0.85rem; } .permission-matrix th, .permission-matrix td { padding: 0.4rem 0.6rem; border: 1px solid var(--cudly-border); text-align: center; } -.permission-matrix th { background: var(--cudly-bg); font-weight: 600; } +.permission-matrix th { background: var(--cudly-bg); font-weight: var(--cudly-fw-semibold); } .permission-matrix td:first-child { text-align: left; font-family: monospace; } -.permission-matrix .has-perm { color: #137333; font-weight: 600; } +.permission-matrix .has-perm { color: #137333; font-weight: var(--cudly-fw-semibold); } .permission-matrix .no-perm { color: var(--cudly-border-strong); } /* Expandable user rows */ @@ -529,7 +529,7 @@ select.dirty { .expand-panel-perms h5 { margin: 0 0 0.5rem; font-size: 0.85rem; - font-weight: 600; + font-weight: var(--cudly-fw-semibold); color: var(--cudly-text); } .group-checkbox-list { @@ -636,7 +636,7 @@ tr.override-disabled { display: block; margin-top: 0.125rem; color: var(--cudly-text-muted); - font-size: 0.75rem; + font-size: var(--cudly-fs-xs); white-space: nowrap; } @media (max-width: 768px) { diff --git a/frontend/src/styles/tables.css b/frontend/src/styles/tables.css index f33d00a55..da36965c3 100644 --- a/frontend/src/styles/tables.css +++ b/frontend/src/styles/tables.css @@ -135,7 +135,7 @@ tr.history-row-highlight:hover { } .planned-purchase-row .plan-name { - font-weight: 500; + font-weight: var(--cudly-fw-medium); display: block; } @@ -173,7 +173,7 @@ tr.history-row-highlight:hover { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem; - font-weight: 500; + font-weight: var(--cudly-fw-medium); text-transform: capitalize; } diff --git a/frontend/tests-e2e/typography.spec.ts b/frontend/tests-e2e/typography.spec.ts new file mode 100644 index 000000000..a95c4d431 --- /dev/null +++ b/frontend/tests-e2e/typography.spec.ts @@ -0,0 +1,44 @@ +/** + * Typography foundation (issue #741). jsdom does not cascade stylesheets, so + * the jest suite can only read the CSS text; these checks read what Chromium + * actually computes for a real form (Create API Key modal). + */ +import { test, expect } from '@playwright/test'; +import { mockApi, seedAuth } from './fixtures/recs'; + +test.beforeEach(async ({ page }) => { + await seedAuth(page); + await mockApi(page); + await page.route('**/api/users', (r) => r.fulfill({ json: { users: [] } })); + await page.route('**/api/groups', (r) => r.fulfill({ json: { groups: [] } })); + await page.route('**/api/api-keys', (r) => r.fulfill({ json: { keys: [] } })); + await page.goto('/admin/users'); + await page.locator('#create-apikey-btn').click(); + await expect(page.locator('#create-apikey-modal')).toBeVisible(); +}); + +test('form inputs use the page font at the same size as the adjacent buttons', async ({ page }) => { + const font = (selector: string) => + page.locator(selector).evaluate((el) => { + const cs = getComputedStyle(el); + return { family: cs.fontFamily, size: cs.fontSize }; + }); + const body = await page.evaluate(() => getComputedStyle(document.body).fontFamily); + const button = await font('#close-create-apikey-modal-btn'); + + // Text, password and date inputs used to get three different sizes + // (14.4px from a modal rule, 13.33px browser default for the others). + for (const id of ['#apikey-name', '#apikey-password', '#apikey-expires-at']) { + const input = await font(id); + expect(input.family, id).toBe(body); + expect(input.size, id).toBe(button.size); + } +}); + +test('body text uses the normal line-height token', async ({ page }) => { + const { lineHeight, fontSize } = await page.evaluate(() => { + const cs = getComputedStyle(document.body); + return { lineHeight: parseFloat(cs.lineHeight), fontSize: parseFloat(cs.fontSize) }; + }); + expect(lineHeight / fontSize).toBeCloseTo(1.5, 2); +});