code-file-icon.client.spec.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. // @vitest-environment jsdom
  2. import { cleanup, render } from '@testing-library/react'
  3. import { afterEach, describe, expect, it } from 'vitest'
  4. import {
  5. classifyFileType, FileTypeIcon, type CodeFileType, type FileTypeProjectContext,
  6. } from '@deepseek-ai/dsh-client-ui-primitives'
  7. import { CODE_FILE_TYPES } from '../src/code-file-types.ts'
  8. afterEach(cleanup)
  9. const flutter: FileTypeProjectContext = {
  10. files: { 'config/pubspec.yaml': 'name: demo\ndependencies:\n flutter: sdk' },
  11. }
  12. describe('code-file classification', () => {
  13. it.each([
  14. ['app.component.ts', 'angular'],
  15. ['file.c', 'c'],
  16. ['file.clj', 'clojure'],
  17. ['CMakeLists.txt', 'cmake'],
  18. ['file.cc', 'cpp'],
  19. ['file.cs', 'csharp'],
  20. ['file.css', 'css'],
  21. ['file.dart', 'dart'],
  22. ['Dockerfile', 'docker'],
  23. ['file.ex', 'elixir'],
  24. ['.env', 'env'],
  25. ['file.erl', 'erlang'],
  26. ['.gitignore', 'git'],
  27. ['file.go', 'go'],
  28. ['file.graphql', 'graphql'],
  29. ['file.hs', 'haskell'],
  30. ['file.ini', 'ini'],
  31. ['file.java', 'java'],
  32. ['file.js', 'javascript'],
  33. ['file.json', 'json'],
  34. ['file.kt', 'kotlin'],
  35. ['file.lua', 'lua'],
  36. ['Makefile', 'makefile'],
  37. ['package.json', 'node'],
  38. ['file.m', 'objective-c'],
  39. ['file.pl', 'perl'],
  40. ['file.php', 'php'],
  41. ['file.ps1', 'powershell'],
  42. ['file.proto', 'protobuf'],
  43. ['file.py', 'python'],
  44. ['file.r', 'r'],
  45. ['file.jsx', 'react'],
  46. ['Gemfile', 'ruby'],
  47. ['file.rs', 'rust'],
  48. ['file.scala', 'scala'],
  49. ['.bashrc', 'shell'],
  50. ['file.sol', 'solidity'],
  51. ['file.sql', 'sql'],
  52. ['file.svelte', 'svelte'],
  53. ['file.swift', 'swift'],
  54. ['file.toml', 'toml'],
  55. ['file.ts', 'typescript'],
  56. ['file.vue', 'vue'],
  57. ['file.wasm', 'wasm'],
  58. ['file.xml', 'xml'],
  59. ['file.yaml', 'yaml'],
  60. ['file.zig', 'zig'],
  61. ] as [string, CodeFileType][])('%s → %s', (path, type) => {
  62. expect(classifyFileType(path)).toBe(type)
  63. })
  64. it('applies filename priority before generic extensions', () => {
  65. expect(classifyFileType('package.json')).toBe('node')
  66. expect(classifyFileType('docker-compose.yaml')).toBe('docker')
  67. expect(classifyFileType('feature.component.ts')).toBe('angular')
  68. expect(classifyFileType('Dockerfile.local')).toBe('docker')
  69. expect(classifyFileType('.env.production')).toBe('env')
  70. })
  71. it('selects Flutter only when the supplied project snapshot identifies it', () => {
  72. expect(classifyFileType('lib/main.dart')).toBe('dart')
  73. expect(classifyFileType('lib/main.dart', { files: { 'pubspec.yaml': 'name: demo' } })).toBe('dart')
  74. expect(classifyFileType('lib/main.dart', flutter)).toBe('flutter')
  75. const view = render(<FileTypeIcon path="lib/main.dart" context={flutter} />)
  76. const explicit = render(<FileTypeIcon kind="flutter" />)
  77. expect(view.container.innerHTML).toBe(explicit.container.innerHTML)
  78. })
  79. it('keeps Markdown and SVG on the traditional file artwork', () => {
  80. expect(classifyFileType('README.md')).toBe('markdown')
  81. expect(classifyFileType('logo.svg')).toBe('image')
  82. })
  83. })
  84. describe('full-color code-file artwork', () => {
  85. it.each(CODE_FILE_TYPES)('%s renders its supplied square SVG', (type) => {
  86. const { container } = render(<FileTypeIcon kind={type} />)
  87. const svg = container.querySelector('svg')!
  88. expect(svg.getAttribute('viewBox')).toBe('0 0 20 20')
  89. expect(svg.getAttribute('width')).toBe('28')
  90. expect(svg.getAttribute('aria-hidden')).toBe('true')
  91. expect(container.innerHTML).toMatch(/#[0-9a-fA-F]{3,8}/)
  92. expect(container.innerHTML).not.toContain('currentColor')
  93. })
  94. it('keeps every code category visually distinct', () => {
  95. const artwork = CODE_FILE_TYPES.map((type) => {
  96. const { container } = render(<FileTypeIcon kind={type} />)
  97. return container.querySelector('svg')!.innerHTML
  98. })
  99. expect(new Set(artwork).size).toBe(CODE_FILE_TYPES.length)
  100. })
  101. it('uses instance-safe gradient ids and forwards sizing classes', () => {
  102. const { container } = render(<><FileTypeIcon kind="elixir" /><FileTypeIcon kind="elixir" /></>)
  103. const ids = [...container.querySelectorAll('linearGradient')].map(gradient => gradient.id)
  104. expect(new Set(ids).size).toBe(2)
  105. expect([...container.querySelectorAll('rect')].map(rect => rect.getAttribute('fill'))).toEqual(ids.map(id => `url(#${id})`))
  106. const sized = render(<FileTypeIcon path="file.ts" size={16} className="x" />)
  107. const svg = sized.container.querySelector('svg')!
  108. expect(svg.getAttribute('width')).toBe('16')
  109. expect(svg.getAttribute('height')).toBe('16')
  110. expect(svg.classList.contains('x')).toBe(true)
  111. })
  112. })