Files
HMDesign/scripts/publisher-components.test.mjs

110 lines
6.5 KiB
JavaScript

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, /<section[^>]*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(/<span id="([^"]+)">페이지당/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(/<button[^>]*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 })), /<button[^>]*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%\)/);
});