···11import { describe, expect, it, vi, beforeEach } from 'vitest'
22import { mountSuspended } from '@nuxt/test-utils/runtime'
33-import PackageVersions from '~/components/PackageVersions.vue'
33+import PackageVersions from '~/components/Package/Versions.vue'
44import type { PackumentVersion } from '#shared/types'
5566// Mock the fetchAllPackageVersions function
+52-33
test/unit/a11y-component-coverage.spec.ts
···2727 'OgImage/Package.vue': 'OG Image component - server-rendered image, not interactive UI',
28282929 // Client-only components with complex dependencies
3030- 'AuthModal.client.vue': 'Complex auth modal with navigation - requires full app context',
3030+ 'Header/AuthModal.client.vue': 'Complex auth modal with navigation - requires full app context',
31313232 // Complex components requiring full app context or specific runtime conditions
3333- 'HeaderOrgsDropdown.vue': 'Requires connector context and API calls',
3434- 'HeaderPackagesDropdown.vue': 'Requires connector context and API calls',
3535- 'MobileMenu.vue': 'Requires Teleport and full navigation context',
3333+ 'Header/OrgsDropdown.vue': 'Requires connector context and API calls',
3434+ 'Header/PackagesDropdown.vue': 'Requires connector context and API calls',
3535+ 'Header/MobileMenu.vue': 'Requires Teleport and full navigation context',
3636 'Modal.client.vue':
3737 'Base modal component - tested via specific modals like ChartModal, ConnectorModal',
3838- 'PackageSkillsModal.vue': 'Complex modal with tabs - requires modal context and state',
3838+ 'Package/SkillsModal.vue': 'Complex modal with tabs - requires modal context and state',
3939 'ScrollToTop.vue': 'Requires scroll position and CSS scroll-state queries',
4040- 'TranslationHelper.vue': 'i18n helper component - requires specific locale status data',
4141- 'PackageWeeklyDownloadStats.vue':
4040+ 'Settings/TranslationHelper.vue': 'i18n helper component - requires specific locale status data',
4141+ 'Package/WeeklyDownloadStats.vue':
4242 'Uses vue-data-ui VueUiSparkline - has DOM measurement issues in test environment',
4343+ 'UserCombobox.vue': 'Unused component - intended for future admin features',
4344}
44454546/**
···6364}
64656566/**
6767+ * Parse .nuxt/components.d.ts to get the mapping from component names to file paths.
6868+ * This uses Nuxt's actual component resolution, so we don't have to guess the naming convention.
6969+ *
7070+ * Returns a Map of component name -> array of file paths (relative to app/components/)
7171+ */
7272+function parseComponentsDeclaration(dtsPath: string): Map<string, string[]> {
7373+ const content = fs.readFileSync(dtsPath, 'utf-8')
7474+ const componentMap = new Map<string, string[]>()
7575+7676+ // Match lines like:
7777+ // export const ComponentName: typeof import("../app/components/Path/File.vue").default
7878+ const exportRegex =
7979+ /export const (\w+): typeof import\("\.\.\/app\/components\/([^"]+\.vue)"\)\.default/g
8080+8181+ let match
8282+ while ((match = exportRegex.exec(content)) !== null) {
8383+ const componentName = match[1]!
8484+ const filePath = match[2]!
8585+8686+ const existing = componentMap.get(componentName) || []
8787+ if (!existing.includes(filePath)) {
8888+ existing.push(filePath)
8989+ }
9090+ componentMap.set(componentName, existing)
9191+ }
9292+9393+ return componentMap
9494+}
9595+9696+/**
6697 * Extract tested component names from the test file.
6798 * Handles both #components imports and direct ~/components/ imports.
6899 */
6969-function getTestedComponents(testFileContent: string): Set<string> {
100100+function getTestedComponents(
101101+ testFileContent: string,
102102+ componentMap: Map<string, string[]>,
103103+): Set<string> {
70104 const tested = new Set<string>()
7110572106 // Match direct imports like:
73107 // import ComponentName from '~/components/ComponentName.vue'
74108 // import ComponentName from '~/components/subdir/ComponentName.vue'
7575- const directImportRegex = /import\s+\w+\s+from\s+['"]~\/components\/(.+\.vue)['"]/g
109109+ const directImportRegex = /import\s+\w+\s+from\s+['"]~\/components\/([^"']+\.vue)['"]/g
76110 let match
7711178112 while ((match = directImportRegex.exec(testFileContent)) !== null) {
···91125 .filter(name => name.length > 0)
9212693127 for (const name of componentNames) {
9494- // Map #components name to file path(s)
9595- const filePaths = mapComponentNameToFiles(name)
128128+ // Look up the file paths from Nuxt's component map
129129+ const filePaths = componentMap.get(name) || []
96130 for (const filePath of filePaths) {
97131 tested.add(filePath)
98132 }
···102136 return tested
103137}
104138105105-/**
106106- * Map a #components export name to the actual file path(s).
107107- * Handles various naming conventions.
108108- *
109109- * Returns an array because importing from #components can cover multiple files:
110110- * - `HeaderAccountMenu` from #components -> tests HeaderAccountMenu.client.vue
111111- * (Nuxt auto-resolves to client variant when both .server and .client exist)
112112- */
113113-function mapComponentNameToFiles(name: string): string[] {
114114- // Handle Compare* prefix -> compare/ subdirectory
115115- if (name.startsWith('Compare')) {
116116- const baseName = name.slice('Compare'.length)
117117- return [`compare/${baseName}.vue`]
118118- }
119119-120120- // Regular component - could be .vue or .client.vue
121121- // When importing from #components, Nuxt resolves to the client variant if it exists
122122- return [`${name}.vue`, `${name}.client.vue`]
123123-}
124124-125139describe('a11y component test coverage', () => {
126140 const componentsDir = fileURLToPath(new URL('../../app/components', import.meta.url))
141141+ const componentsDtsPath = fileURLToPath(new URL('../../.nuxt/components.d.ts', import.meta.url))
127142 const testFilePath = fileURLToPath(new URL('../nuxt/a11y.spec.ts', import.meta.url))
128143129144 it('should have accessibility tests for all components (or be explicitly skipped)', () => {
130145 // Get all Vue components
131146 const allComponents = getVueFiles(componentsDir)
132147148148+ // Parse Nuxt's component declarations to get name -> path mapping
149149+ const componentMap = parseComponentsDeclaration(componentsDtsPath)
150150+133151 // Get components that are tested
134152 const testFileContent = fs.readFileSync(testFilePath, 'utf-8')
135135- const testedComponents = getTestedComponents(testFileContent)
153153+ const testedComponents = getTestedComponents(testFileContent, componentMap)
136154137155 // Find components that are neither tested nor skipped
138156 const missingTests = allComponents.filter(
···155173 })
156174157175 it('should not skip components that are actually tested', () => {
176176+ const componentMap = parseComponentsDeclaration(componentsDtsPath)
158177 const testFileContent = fs.readFileSync(testFilePath, 'utf-8')
159159- const testedComponents = getTestedComponents(testFileContent)
178178+ const testedComponents = getTestedComponents(testFileContent, componentMap)
160179161180 const unnecessarySkips = Object.keys(SKIPPED_COMPONENTS).filter(component =>
162181 testedComponents.has(component),