import assert from 'node:assert/strict'; import test from 'node:test'; import React from 'react'; import { renderToStaticMarkup as render } from 'react-dom/server'; import { build } from 'vite'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; // Load the actual TSX through the project's Vite transforms; assertions cover // consumer contracts rather than matching implementation text. const cache = new Map(); const load = async source => { if (!cache.has(source)) { const name = path.basename(source, '.tsx'); await build({ configFile: false, logLevel: 'silent', build: { outDir: `_local/component-test/${name}`, emptyOutDir: false, lib: { entry: source, formats: ['es'], fileName: 'module' }, rolldownOptions: { external: id => !id.startsWith('.') && !path.isAbsolute(id) } } }); cache.set(source, import(pathToFileURL(path.resolve(`_local/component-test/${name}/module.js`)).href)); } return cache.get(source); }; const element = React.createElement; test('FilterBar list composition is explicit and leaves default consumers intact', async () => { const { FilterBar } = await load('packages/ui/src/patterns/filter-bar/filter-bar.tsx'); const original = render(element(FilterBar, { id: 'default', 'aria-label': 'Default' }, 'Content')); assert.doesNotMatch(original, /data-layout/); for (const layout of ['list-basic', 'list-extended']) { const html = render(element(FilterBar, { layout, id: layout, style: { marginTop: 12 } }, 'Content')); assert.match(html, new RegExp(`data-layout="${layout}"`)); assert.match(html, /margin-top:12px/); assert.match(html, /Content/); assert.doesNotMatch(html, / layout=/); } }); test('C2 Checkbox asChild preserves the consumer root and content, including checked indicator', async () => { const { Checkbox } = await load('packages/ui/src/components/ui/checkbox.tsx'); const html = render(element(Checkbox, { asChild: true, checked: true }, element('button', { id: 'consumer-check' }, element('span', null, 'Consumer child')))); assert.match(html, /id="consumer-check"/); assert.match(html, /Consumer child/); assert.match(html, /data-slot="checkbox-indicator"/); assert.match(html, /role="checkbox"/); }); test('C2 Progress asChild preserves the consumer root/content and progress semantics', async () => { const { Progress } = await load('packages/ui/src/components/ui/progress.tsx'); const html = render(element(Progress, { asChild: true, value: 35 }, element('section', { id: 'consumer-progress' }, 'Consumer progress'))); assert.match(html, /]*id="consumer-progress"/); assert.match(html, /Consumer progress/); assert.match(html, /aria-valuenow="35"/); assert.match(html, /data-slot="progress-indicator"/); }); test('C3 Toggle Group combines consumer styles with the requested spacing', async () => { const { ToggleGroup } = await load('packages/ui/src/components/ui/toggle-group.tsx'); const html = render(element(ToggleGroup, { type: 'multiple', spacing: 3, style: { color: 'red' } })); assert.match(html, /--hm-toggle-group-gap:12px/); assert.match(html, /color:red/); }); test('C4 every railway state forwards DOM identity, ARIA and style to the final root', async () => { const { RouteLine } = await load('packages/center-graphic/src/components/CenterSpatialInformation.tsx'); for (const state of ['existing', 'planned', 'underConstruction']) { const html = render(element(RouteLine, { routeClass: 'railway', state, id: 'route', 'aria-label': 'Railway', style: { opacity: 0.7 } })); assert.match(html, /id="route"/); assert.match(html, /aria-label="Railway"/); assert.match(html, /opacity:0.7/); assert.equal((html.match(/id="route"/g) ?? []).length, 1); } }); test('PT1 two pagination instances have distinct labels and matching references', async () => { const { DataTablePagination, useDataTable } = await load('packages/ui/src/patterns/data-table/data-table.tsx'); function Tables() { const table = useDataTable({ columns: [{ accessorKey: 'name' }], data: [{ name: 'one' }] }); return element(React.Fragment, null, element(DataTablePagination, { table }), element(DataTablePagination, { table })); } const html = render(element(Tables)); const ids = [...html.matchAll(/페이지당/g)].map(match => match[1]); assert.equal(ids.length, 2); assert.equal(new Set(ids).size, 2); for (const id of ids) assert.ok(html.includes(`aria-labelledby="${id}"`)); }); test('PT2 readOnly and disabled searches expose no mutating clear control', async () => { const { FilterBarSearch } = await load('packages/ui/src/patterns/filter-bar/filter-bar.tsx'); for (const protection of [{ readOnly: true }, { disabled: true }]) { const html = render(element(FilterBarSearch, { value: 'Keep', onValueChange() { throw Error('must not mutate'); }, ...protection })); assert.doesNotMatch(html, /data-slot="filter-bar-search-clear"/); assert.match(html, /value="Keep"/); } assert.match(render(element(FilterBarSearch, { value: 'Editable', onValueChange() {} })), /data-slot="filter-bar-search-clear"/); }); test('C5 repeated popup triggers have unique generated IDs and retain consumer identity', async () => { const { Combobox } = await load('packages/ui/src/components/ui/combobox.tsx'); const { DatePicker } = await load('packages/ui/src/components/ui/date-picker.tsx'); const html = render(element(React.Fragment, null, element(Combobox, { options: [], 'aria-label': 'Department' }), element(Combobox, { options: [], 'aria-label': 'Owner' }), element(DatePicker, { 'aria-label': 'Start' }), element(DatePicker, { 'aria-label': 'End' }), element(DatePicker, { id: 'consumer-date', 'aria-labelledby': 'date-label' }))); const ids = [...html.matchAll(/]*id="([^"]+)"/g)].map(match => match[1]); assert.equal(ids.length, 5); assert.equal(new Set(ids).size, 5); assert.ok(ids.includes('consumer-date')); assert.match(html, /aria-labelledby="date-label"/); }); test('C2 default roots and external Checkbox form remain backward compatible', async () => { const { Checkbox } = await load('packages/ui/src/components/ui/checkbox.tsx'); const { Progress } = await load('packages/ui/src/components/ui/progress.tsx'); assert.match(render(element(Checkbox, { form: 'external', name: 'approved', checked: true })), /]*form="external"/); const progress = render(element(Progress, { value: 50, max: 200, 'aria-label': 'Upload' })); assert.match(progress, /aria-valuemax="200"/); assert.match(progress, /translateX\(-75%\)/); });