import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import {
applyIconSet,
assignFamilies,
buildIconSet,
normalizeExportName,
readIconSource,
validatePathData,
} from './lib.mts'
async function fixture(t, lane, name, svg) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'icon-codegen-'))
t.after(() => fs.rm(root, {recursive: true, force: true}))
await fs.mkdir(path.join(root, lane), {recursive: true})
await fs.writeFile(path.join(root, lane, name), svg)
return {relative: path.join(lane, name), root}
}
test('validates SVG path grammar', () => {
validatePathData('M0 0h2v2H0Z')
validatePathData('M0,0 2,2')
assert.throws(() => validatePathData('M0 0 L nope'), /L has no arguments/)
assert.throws(() => validatePathData('M0'), /too few arguments/)
assert.throws(() => validatePathData('M0 0A1 1 0 2 0 3 4'), /large-arc-flag/)
assert.throws(() => validatePathData('M0,,0'), /misplaced comma/)
assert.throws(() => validatePathData('M,0 0'), /misplaced comma/)
assert.throws(() => validatePathData('M0 0,'), /misplaced comma/)
})
test('normalizes accidental legacy export spelling', () => {
assert.equal(
normalizeExportName('Envelope_Open_Stoke2_Corner0_Rounded'),
'EnvelopeOpen_Stroke2_Corner0_Rounded',
)
})
test('groups families deterministically', () => {
const icons = [
{exportName: 'CircleCheck_Stroke2_Corner0_Rounded', namespace: ''},
{exportName: 'CircleInfo_Stroke2_Corner0_Rounded', namespace: ''},
{exportName: 'MagnifyingGlass_Stroke2_Corner0_Rounded', namespace: ''},
{exportName: 'MagnifyingGlassX_Stroke2_Corner0_Rounded', namespace: ''},
]
assignFamilies(icons)
assert.deepEqual(icons.map(icon => icon.family), [
'Circle',
'Circle',
'MagnifyingGlass',
'MagnifyingGlass',
])
})
test('accepts fill and supported stroke icons', async t => {
const fill = await fixture(
t,
'ui',
'Fill_Filled_Corner0_Rounded.svg',
'',
)
assert.equal((await readIconSource(fill.root, fill.relative)).description.strokeWidth, 0)
const stroke = await fixture(
t,
'ui',
'StrokeIcon_Stroke2_Corner0_Rounded.svg',
'',
)
assert.equal((await readIconSource(stroke.root, stroke.relative)).description.strokeWidth, 1.5)
const nonstandard = await fixture(
t,
'ui',
'Nonstandard_Stroke2_Corner0_Rounded.svg',
'',
)
assert.deepEqual((await readIconSource(nonstandard.root, nonstandard.relative)).warnings, [
`${nonstandard.relative}: non-standard viewBox 0 0 20 18; expected 24x24 or 64x64`,
])
})
test('rejects malformed and suspicious SVG input', async t => {
const malformed = await fixture(t, 'ui', 'Bad.svg', '')
await assert.rejects(readIconSource(malformed.root, malformed.relative), /malformed SVG/)
const noViewBox = await fixture(t, 'ui', 'NoViewBox.svg', '')
await assert.rejects(readIconSource(noViewBox.root, noViewBox.relative), /missing viewBox/)
const invalidPath = await fixture(
t,
'ui',
'InvalidPath.svg',
'',
)
await assert.rejects(readIconSource(invalidPath.root, invalidPath.relative), /L has no arguments/)
const external = await fixture(
t,
'ui',
'External.svg',
'',
)
await assert.rejects(readIconSource(external.root, external.relative), /unsupported /)
const script = await fixture(
t,
'custom',
'Raw.svg',
'',
)
await assert.rejects(readIconSource(script.root, script.relative), /unsupported