import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; const read = (path: string) => readFileSync(new URL(path, import.meta.url), 'utf8'); const tokens = read('../../src/styles/ui-tokens.css'); const components = read('../../src/styles/components.css'); const tileContract = read('../../src/styles/tile-contract.css'); const datahub = read('../../src/styles/datahub.css'); const datahubRedesign = read('../../src/styles/datahub-redesign.css'); const legacy = read('../../src/styles/legacy-views.css'); const layout = read('../../src/styles/layout.css'); const uiComponents = read('../../src/styles/ui-components.css'); const policy = read('../../src/styles/policy-analysis.css'); const pathfinder = read('../../src/styles/mitre-graph.css'); const viewer = read('../../src/features/dataset-viewer/DatasetViewer.tsx'); const overview = read('../../src/features/project-import/DatahubHistoryOverview.tsx'); const guidance = read('../../AGENTS.md'); describe('binding segment and tile grammar', () => { it('defines one semantic geometry and surface contract for both themes', () => { for (const token of [ '--segment-surface:', '--segment-border:', '--segment-radius: 10px;', '--segment-inset: var(--space-3);', '--segment-gap: var(--space-3);', '--tile-surface:', '--tile-surface-hover:', '--tile-border:', '--tile-selected-border: var(--marking-color);', '--tile-radius: var(--radius-sm);', '--tile-compact-width: 186px;', '--tile-content-width: 358px;', '--tile-content-min-height: 223px;', '--tile-card-shadow:' ]) expect(tokens).toContain(token); expect(tokens.match(/--segment-surface: transparent;/g)).toHaveLength(2); expect(tokens.match(/--segment-border: transparent;/g)).toHaveLength(2); // User decision 2026-09-25: the dark theme keeps no contour; the light theme uses one neutral hairline. expect(tokens.match(/--tile-border: transparent;/g)).toHaveLength(1); expect(tokens.match(/--tile-border: #DCDDE3;/g)).toHaveLength(1); expect(tokens.indexOf('--tile-border: #DCDDE3;')).toBeGreaterThan(tokens.indexOf(':root[data-theme="light"]')); expect(tokens).toContain('--tile-surface-hover: #F8F8FA;'); expect(tokens.match(/--tile-card-shadow: none;/g)).toHaveLength(2); expect(tokens).toContain('--tile-surface: #F5F5F7;'); expect(tokens.match(/--marking-color: #E9A63C;/g)).toHaveLength(2); expect(tokens.match(/--selection-border: var\(--marking-color\);/g)).toHaveLength(2); expect(tokens.match(/--selection-marker: var\(--marking-color\);/g)).toHaveLength(2); expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);'); expect(tokens).toContain('--donut-card-surface: var(--tile-surface);'); expect(tokens).toContain('--donut-card-shadow: var(--tile-card-shadow);'); }); it('binds the shared grammar to Datahub, dashboard, sidebar, Work, policy and Pathfinder tiles', () => { expect(components).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;[^}]*padding:\s*var\(--segment-inset\);[^}]*border-radius:\s*var\(--segment-radius\);/s); expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s); expect(tileContract).toMatch(/\.datahub-reference-controls,\s*\.datahub-tenant-card-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);/s); expect(components).toMatch(/\.donut-metric-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s); expect(components).toMatch(/\.sidebar-domain-rack\s*\{[^}]*border-top:\s*var\(--border-hair\) solid var\(--border-subtle\);[^}]*background:\s*transparent;/s); expect(components).toMatch(/\.sidebar-domain-button\s*\{[^}]*border-radius:\s*var\(--btn-radius\);[^}]*background:\s*transparent;/s); expect(components).toMatch(/\.sidebar-domain-button\[aria-pressed="true"\]\s*\{[^}]*background:\s*var\(--surface-selected\);/s); expect(datahubRedesign).toMatch(/\.datahub-tenant-card-rack\s*\{[^}]*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s); expect(datahubRedesign).toMatch(/\.datahub-tenant-card\.is-selected\s*\{[^}]*border-color:\s*var\(--tile-selected-border\);[^}]*outline:\s*calc\(var\(--bw-emphasis\) \+ var\(--bw-hairline\)\) solid var\(--tile-selected-border\);/s); expect(overview).toContain('donut-metric-card donut-metric-card--all datahub-tenant-card datahub-tenant-add'); expect(datahub).toMatch(/\.dashboard-status-rack\s*\{[^}]*padding:\s*var\(--segment-inset\);[^}]*background:\s*var\(--segment-surface\);/s); expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s); expect(datahub).toMatch(/\.dashboard-distribution-grid\s*\{[^}]*border-radius:\s*var\(--segment-radius\);/s); expect(datahub).toMatch(/\.db-structure-card\s*\{[^}]*border:\s*var\(--border-hair\) solid var\(--tile-border\);[^}]*border-radius:\s*0;[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*none;/s); expect(layout).toMatch(/\.tile-view-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s); expect(legacy).toMatch(/\.tile-grid\s*\{[^}]*justify-content:\s*start;[^}]*border-radius:\s*var\(--segment-radius\);/s); expect(legacy).toMatch(/\.data-tile\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s); expect(viewer).toContain('const TILE_MIN_WIDTH = 358;'); expect(viewer).toContain('const TILE_MIN_HEIGHT = 223;'); expect(viewer).toContain('gridTemplateColumns: `repeat(${tileColumns}, var(--tile-content-width))`'); expect(viewer).toContain('const TILE_GAP = 12;'); expect(viewer).toContain('const TILE_ROW_HEIGHT = TILE_MIN_HEIGHT + TILE_GAP;'); expect(legacy).toMatch(/\.data-tile\s*\{[^}]*min-height:\s*var\(--tile-content-min-height\);/s); expect(legacy).toMatch(/\.tile-grid > \.data-tile \.data-tile-badges\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*max-content max-content;/s); expect(legacy).toMatch(/\[data-badge-role="status"\],[\s\S]*\[data-badge-role="category"\],[\s\S]*\[data-badge-role="score"\]\s*\{[^}]*grid-column:\s*1 \/ -1;/s); expect(policy).toContain('grid-template-columns: repeat(auto-fill, var(--tile-compact-width));'); expect(pathfinder).toMatch(/\.finding-path-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s); }); it('keeps the rules binding and prevents status-filled or stretched substitute tiles', () => { expect(guidance).toContain('Treat segments and tiles as a binding two-level visual grammar independent of their content.'); expect(guidance).toContain('Segments are visually transparent and have no visible border or shadow.'); expect(guidance).toContain('without a visible normal-state border, radius, or shadow'); expect(guidance).toContain('Hover changes the surface only'); expect(guidance).toContain('persistent marking and selection color is the shared yellow'); expect(guidance).toContain('except for the explicitly blue table-row reading marker'); expect(guidance).toContain('never stretch a partial final row merely to fill the container'); expect(guidance).toContain('Never fill the complete tile with a status color.'); expect(guidance).toContain('Tile content must never cross the tile boundary or cover a neighboring tile.'); expect(guidance).toContain('never split ordinary words or numeric values across lines'); expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s); expect(datahub).toMatch(/\.dashboard-status-copy\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s); expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*span\s*\{[^}]*overflow-wrap:\s*normal;[^}]*word-break:\s*normal;/s); expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s); expect(legacy).toMatch(/\.data-tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s); expect(uiComponents).toMatch(/\.tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s); expect(datahubRedesign).not.toMatch(/\.datahub-tenant-card(?::hover|:focus-within|\s*\{)[^}]*translateY/s); expect(legacy).not.toMatch(/\.data-tile(?::hover|:focus-visible|\s*\{)[^}]*translateY/s); expect(datahub).not.toMatch(/\.dashboard-status-metric(?::hover|\s*\{)[^}]*translateY/s); }); });