// @vitest-environment jsdom
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import {
classifyFileType, FileTypeIcon, type CodeFileType, type FileTypeProjectContext,
} from '@deepseek-ai/dsh-client-ui-primitives'
import { CODE_FILE_ARTWORK, CODE_FILE_ICON_ID_TOKEN } from '../src/code-file-icon-artwork.ts'
import { CODE_FILE_TYPES } from '../src/code-file-types.ts'
afterEach(cleanup)
const flutter: FileTypeProjectContext = {
files: { 'config/pubspec.yaml': 'name: demo\ndependencies:\n flutter: sdk' },
}
function normalizeCodeIconIds(html: string): string {
return html.replace(/dsh-code-icon-[A-Za-z0-9_-]+/gu, 'dsh-code-icon-instance')
}
describe('code-file classification', () => {
it.each([
['app.component.ts', 'angular'],
['file.c', 'c'],
['file.clj', 'clojure'],
['CMakeLists.txt', 'cmake'],
['file.cc', 'cpp'],
['file.cs', 'csharp'],
['file.css', 'css'],
['file.dart', 'dart'],
['Dockerfile', 'docker'],
['file.ex', 'elixir'],
['.env', 'env'],
['file.erl', 'erlang'],
['.gitignore', 'git'],
['file.go', 'go'],
['file.graphql', 'graphql'],
['file.hs', 'haskell'],
['file.ini', 'ini'],
['file.java', 'java'],
['file.js', 'javascript'],
['file.json', 'json'],
['file.kt', 'kotlin'],
['file.lua', 'lua'],
['Makefile', 'makefile'],
['package.json', 'node'],
['file.m', 'objective-c'],
['file.pl', 'perl'],
['file.php', 'php'],
['file.ps1', 'powershell'],
['file.proto', 'protobuf'],
['file.py', 'python'],
['file.r', 'r'],
['file.jsx', 'react'],
['Gemfile', 'ruby'],
['file.rs', 'rust'],
['file.scala', 'scala'],
['.bashrc', 'shell'],
['file.sol', 'solidity'],
['file.sql', 'sql'],
['file.svelte', 'svelte'],
['file.swift', 'swift'],
['file.toml', 'toml'],
['file.ts', 'typescript'],
['file.vue', 'vue'],
['file.wasm', 'wasm'],
['file.xml', 'xml'],
['file.yaml', 'yaml'],
['file.zig', 'zig'],
] as [string, CodeFileType][])('%s → %s', (path, type) => {
expect(classifyFileType(path)).toBe(type)
})
it('applies filename priority before generic extensions', () => {
expect(classifyFileType('package.json')).toBe('node')
expect(classifyFileType('docker-compose.yaml')).toBe('docker')
expect(classifyFileType('feature.component.ts')).toBe('angular')
expect(classifyFileType('Dockerfile.local')).toBe('docker')
expect(classifyFileType('.env.production')).toBe('env')
})
it('selects Flutter only when the supplied project snapshot identifies it', () => {
expect(classifyFileType('lib/main.dart')).toBe('dart')
expect(classifyFileType('lib/main.dart', { files: { 'pubspec.yaml': 'name: demo' } })).toBe('dart')
expect(classifyFileType('lib/main.dart', flutter)).toBe('flutter')
const view = render()
const explicit = render()
expect(normalizeCodeIconIds(view.container.innerHTML))
.toBe(normalizeCodeIconIds(explicit.container.innerHTML))
})
it('keeps Markdown and SVG on the traditional file artwork', () => {
expect(classifyFileType('README.md')).toBe('markdown')
expect(classifyFileType('logo.svg')).toBe('image')
})
})
describe('full-color code-file artwork', () => {
it('embeds exactly the established 48 code categories', () => {
expect(Object.keys(CODE_FILE_ARTWORK)).toEqual([...CODE_FILE_TYPES])
expect(CODE_FILE_ARTWORK).not.toHaveProperty('markdown')
for (const [type, artwork] of Object.entries(CODE_FILE_ARTWORK)) {
expect(artwork).not.toMatch(/<(?:script|style|foreignObject)\b|\son[a-z]+\s*=/iu)
expect(artwork).not.toMatch(/(?:href|xlink:href)="(?!#)/iu)
const ids = [...artwork.matchAll(/\bid="([^"]+)"/gu)].map(match => match[1]!)
expect(new Set(ids).size, `${type} has duplicate SVG ids`).toBe(ids.length)
for (const id of ids) expect(id).toMatch(new RegExp(`^${CODE_FILE_ICON_ID_TOKEN}`))
const references = [
...[...artwork.matchAll(/url\(#([^)]+)\)/gu)].map(match => match[1]!),
...[...artwork.matchAll(/(?:href|xlink:href)="#([^"]+)"/gu)].map(match => match[1]!),
]
for (const reference of references) {
expect(reference).toMatch(new RegExp(`^${CODE_FILE_ICON_ID_TOKEN}`))
expect(ids).toContain(reference)
}
}
})
it('records the design exports that supply the closed artwork set', () => {
const path = resolve('packages/client/ui-primitives/src/code-file-icon-artwork.manifest.json')
const manifest = JSON.parse(readFileSync(path, 'utf8')) as {
owner: string
sources: Array<{ kind: string; sha256: string; icons: string[] }>
excluded: string[]
}
expect(manifest.owner).toBe('DeepSeek Harness product design')
expect(manifest.sources.every(source => source.kind === 'internal-design-export')).toBe(true)
expect(manifest.sources.every(source => /^[0-9a-f]{64}$/u.test(source.sha256))).toBe(true)
expect(manifest.sources.flatMap(source => source.icons).sort()).toEqual([...CODE_FILE_TYPES].sort())
expect(manifest.excluded).toEqual(['markdown'])
})
it.each(CODE_FILE_TYPES)('%s renders its supplied square SVG', (type) => {
const { container } = render()
const svg = container.querySelector('svg')!
expect(svg.getAttribute('viewBox')).toBe('0 0 20 20')
expect(svg.getAttribute('width')).toBe('28')
expect(svg.getAttribute('aria-hidden')).toBe('true')
expect(container.innerHTML).toMatch(/#[0-9a-fA-F]{3,8}/)
expect(container.innerHTML).not.toContain('currentColor')
})
it('keeps every code category visually distinct', () => {
const artwork = CODE_FILE_TYPES.map((type) => {
const { container } = render()
return container.querySelector('svg')!.innerHTML
})
expect(new Set(artwork).size).toBe(CODE_FILE_TYPES.length)
})
it('uses instance-safe gradient ids and forwards sizing classes', () => {
const { container } = render(<>>)
const ids = [...container.querySelectorAll('[id]')].map(element => element.id)
expect(ids.length).toBeGreaterThan(2)
expect(new Set(ids).size).toBe(ids.length)
expect(container.innerHTML).not.toContain(CODE_FILE_ICON_ID_TOKEN)
const references = [...container.querySelectorAll('*')].flatMap(element =>
[...element.attributes].flatMap(attribute =>
[...attribute.value.matchAll(/url\(#([^)]+)\)/gu)].map(match => match[1])),
)
expect(references.length).toBeGreaterThan(0)
for (const reference of references) expect(ids).toContain(reference)
const sized = render()
const svg = sized.container.querySelector('svg')!
expect(svg.getAttribute('width')).toBe('16')
expect(svg.getAttribute('height')).toBe('16')
expect(svg.classList.contains('x')).toBe(true)
})
})