import { readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' // SDK control refresh, phase 1 base controls (rounds 1 and 10). One rule per // test; a failure lists the offending `file:line`. const here = dirname(fileURLToPath(import.meta.url)) const componentsDir = resolve(here, '../../styles/components') /** Lines of a component sheet with block comments blanked (line numbers kept). */ function lines(file: string): string[] { const text = readFileSync(join(componentsDir, file), 'utf-8') return text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, ' ')).split('\n') } function offenders(files: string[], test: (line: string) => boolean): string[] { return files.flatMap((file) => lines(file).flatMap((line, i) => (test(line) ? [`${file}:${i + 1}: ${line.trim()}`] : [])), ) } const css = (file: string) => lines(file).join('\n') describe('SDK control refresh base-control rules', () => { it('should find no transparent-border focus when scanning select.css (1a)', () => { expect(offenders(['select.css'], (l) => /border-color:\s*transparent/.test(l))).toEqual([]) }) it('should find no soft or plain focus ring when scanning pressable controls (1a)', () => { const pressables = ['checkbox.css', 'radio-group.css', 'toggle.css', 'icon-button.css', 'multi-select.css'] expect(offenders(pressables, (l) => /var\(--focus-ring(-soft)?\)/.test(l))).toEqual([]) }) it.each(['input.css', 'select.css', 'textarea.css'])( 'should use the shared field hover border when scanning %s (1e)', (file) => { expect(css(file)).toContain('border-color: var(--_field-border-hover)') }, ) it('should disable select without opacity when scanning select.css (1e)', () => { expect(css('select.css')).not.toContain('--mint-disabled-opacity') }) it.each(['checkbox.css', 'radio-group.css'])( 'should draw the unchecked control with the strong border when scanning %s (1b)', (file) => { expect(css(file)).toContain('border: 1.5px solid var(--_control-border-strong)') }, ) it('should paint the off track with the strong border color when scanning toggle.css (1b)', () => { expect(css('toggle.css')).toMatch(/\.mint-toggle__track--off \{\s*background-color: var\(--_control-border-strong\);/) }) it('should move the toggle knob with transform only when scanning toggle.css (1b)', () => { expect(offenders(['toggle.css'], (l) => /__knob--on-|transition:\s*left/.test(l))).toEqual([]) }) it('should find no radio input-wrapper size modifiers when scanning radio-group.css (1b)', () => { expect(offenders(['radio-group.css'], (l) => /__input-wrapper--|margin-left:\s*-/.test(l))).toEqual([]) }) it('should find no translateY lift when scanning icon-button.css (1d)', () => { expect(offenders(['icon-button.css'], (l) => /translateY/.test(l))).toEqual([]) }) it('should ring the slider thumb when the native input has keyboard focus (1f)', () => { expect(css('slider.css')).toMatch( /\.mint-slider__input:focus-visible ~ \.mint-slider__thumb \{[^}]*box-shadow: var\(--focus-ring-offset\);/, ) }) it('should find no semantic title color when scanning alert-box.css (1h)', () => { expect(offenders(['alert-box.css'], (l) => /\.mint-alert--\w+ \.mint-alert__title/.test(l))).toEqual([]) }) it('should find no pill radius on the chip when scanning multi-select.css (1i)', () => { expect(offenders(['multi-select.css'], (l) => /9999px|999px/.test(l))).toEqual([]) }) it('should find no literal radius or hand-written shadow when scanning modal.css (1i drift)', () => { expect(offenders(['modal.css'], (l) => /0\.875rem 0|radius:\s*0\.875rem|rgba\(15, 23, 42, 0\.(4|2|08)\)/.test(l))).toEqual([]) }) it('should find no Fira font literal when scanning drift files (1i drift, 10c)', () => { const files = ['modal.css', 'tooltip.css', 'slider.css', 'experiment-code-badge.css'] expect(offenders(files, (l) => /'Fira/.test(l))).toEqual([]) }) it('should find no literal var() fallback when scanning drift files (1i drift, 10c)', () => { const files = ['checkbox.css', 'toggle.css', 'slider.css', 'app-layout.css', 'app-container.css', 'mobile-support-gate.css'] expect(offenders(files, (l) => /var\(--[\w-]+,\s*[^v\s]/.test(l))).toEqual([]) }) it('should keep the collapsible-card header actions still on hover (10a)', () => { expect(offenders(['collapsible-card.css'], (l) => /translateY/.test(l))).toEqual([]) }) it('should keep the resource-card hover shadow at --shadow-sm (10a)', () => { expect(css('resource-card.css')).not.toMatch(/\.mint-resource-card:hover \{[^}]*box-shadow/) }) it('should replace the spinner rotation under reduced motion (10c)', () => { expect(css('loading-spinner.css')).toMatch( /@media \(prefers-reduced-motion: reduce\) \{\s*\.mint-spinner__svg \{\s*animation: mint-spinner-pulse [^;]*!important;/, ) }) it('should size the card segmented control border-box so full-width fits its container (3b)', () => { expect(css('segmented-control.css')).toMatch(/\.mint-segmented-control--card \{[^}]*box-sizing: border-box;/) }) })