[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: reorganise components by use/area

+140 -126
app/components/AccentColorPicker.vue app/components/Settings/AccentColorPicker.vue
+2 -2
app/components/AnnounceTooltip.vue app/components/Tooltip/Announce.vue
··· 10 10 </script> 11 11 12 12 <template> 13 - <BaseTooltip :text :isVisible :position :tooltip-attr="{ 'aria-live': 'polite' }" 13 + <TooltipBase :text :isVisible :position :tooltip-attr="{ 'aria-live': 'polite' }" 14 14 ><slot 15 - /></BaseTooltip> 15 + /></TooltipBase> 16 16 </template>
+2 -2
app/components/AppHeader.vue
··· 138 138 :class="{ 'hidden sm:flex': !isSearchExpanded }" 139 139 > 140 140 <!-- Search bar (hidden on mobile unless expanded) --> 141 - <SearchBox 141 + <HeaderSearchBox 142 142 ref="searchBoxRef" 143 143 :inputClass="isSearchExpanded ? 'w-full' : ''" 144 144 :class="{ 'max-w-md': !isSearchExpanded }" ··· 217 217 </nav> 218 218 219 219 <!-- Mobile menu --> 220 - <MobileMenu v-model:open="showMobileMenu" /> 220 + <HeaderMobileMenu v-model:open="showMobileMenu" /> 221 221 </header> 222 222 </template>
+2 -2
app/components/AppTooltip.vue app/components/Tooltip/App.vue
··· 19 19 </script> 20 20 21 21 <template> 22 - <BaseTooltip 22 + <TooltipBase 23 23 :text 24 24 :isVisible 25 25 :position ··· 30 30 @focusout="hide" 31 31 :aria-describedby="isVisible ? tooltipId : undefined" 32 32 ><slot 33 - /></BaseTooltip> 33 + /></TooltipBase> 34 34 </template>
app/components/AuthModal.client.vue app/components/Header/AuthModal.client.vue
app/components/BaseTooltip.vue app/components/Tooltip/Base.vue
app/components/ChartModal.vue app/components/Package/ChartModal.vue
app/components/ClaimPackageModal.vue app/components/Package/ClaimPackageModal.vue
app/components/CodeDirectoryListing.vue app/components/Code/DirectoryListing.vue
app/components/CodeFileTree.vue app/components/Code/FileTree.vue
app/components/CodeMobileTreeDrawer.vue app/components/Code/MobileTreeDrawer.vue
app/components/CodeViewer.vue app/components/Code/Viewer.vue
+2 -2
app/components/ColumnPicker.vue
··· 117 117 <span class="text-sm text-fg-muted font-mono flex-1"> 118 118 {{ getColumnLabel(column.id) }} 119 119 </span> 120 - <AppTooltip 120 + <TooltipApp 121 121 v-if="column.disabled" 122 122 :id="`${column.id}-disabled-reason`" 123 123 class="text-fg-subtle" ··· 127 127 <span class="size-4 flex justify-center items-center text-xs border rounded-full" 128 128 >i</span 129 129 > 130 - </AppTooltip> 130 + </TooltipApp> 131 131 </label> 132 132 </div> 133 133
+1 -1
app/components/ConnectorModal.vue app/components/Header/ConnectorModal.vue
··· 62 62 </div> 63 63 64 64 <!-- Operations Queue --> 65 - <OperationsQueue /> 65 + <OrgOperationsQueue /> 66 66 67 67 <div v-if="!hasOperations" class="text-sm text-fg-muted"> 68 68 {{ $t('connector.modal.connected_hint') }}
app/components/ExecuteCommandTerminal.vue app/components/Terminal/Execute.vue
app/components/FilterChips.vue app/components/Filter/Chips.vue
app/components/FilterPanel.vue app/components/Filter/Panel.vue
+2 -2
app/components/HeaderAccountMenu.client.vue app/components/Header/AccountMenu.client.vue
··· 247 247 </div> 248 248 </Transition> 249 249 </div> 250 - <ConnectorModal /> 251 - <AuthModal /> 250 + <HeaderConnectorModal /> 251 + <HeaderAuthModal /> 252 252 </template>
app/components/HeaderAccountMenu.server.vue app/components/Header/AccountMenu.server.vue
app/components/HeaderOrgsDropdown.vue app/components/Header/OrgsDropdown.vue
app/components/HeaderPackagesDropdown.vue app/components/Header/PackagesDropdown.vue
app/components/InstallCommandTerminal.vue app/components/Terminal/Install.vue
app/components/MobileMenu.vue app/components/Header/MobileMenu.vue
+1 -1
app/components/OperationsQueue.vue app/components/Org/OperationsQueue.vue
··· 1 1 <script setup lang="ts"> 2 - import type { PendingOperation } from '../../cli/src/types' 2 + import type { PendingOperation } from '~~/cli/src/types' 3 3 4 4 const { 5 5 isConnected,
app/components/OrgMembersPanel.vue app/components/Org/MembersPanel.vue
app/components/OrgTeamsPanel.vue app/components/Org/TeamsPanel.vue
app/components/PackageAccessControls.vue app/components/Package/AccessControls.vue
app/components/PackageCard.vue app/components/Package/Card.vue
app/components/PackageDependencies.vue app/components/Package/Dependencies.vue
app/components/PackageDeprecatedTree.vue app/components/Package/DeprecatedTree.vue
+2 -2
app/components/PackageDownloadAnalytics.vue app/components/Package/DownloadAnalytics.vue
··· 2 2 import type { VueUiXyDatasetItem } from 'vue-data-ui' 3 3 import { VueUiXy } from 'vue-data-ui/vue-ui-xy' 4 4 import { useDebounceFn, useElementSize } from '@vueuse/core' 5 - import { useCssVariables } from '../composables/useColors' 6 - import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '../utils/colors' 5 + import { useCssVariables } from '~/composables/useColors' 6 + import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors' 7 7 8 8 const props = defineProps<{ 9 9 weeklyDownloads: WeeklyDownloadPoint[]
app/components/PackageInstallScripts.vue app/components/Package/InstallScripts.vue
app/components/PackageList.vue app/components/Package/List.vue
app/components/PackageListControls.vue app/components/Package/ListControls.vue
app/components/PackageListToolbar.vue app/components/Package/ListToolbar.vue
app/components/PackageMaintainers.vue app/components/Package/Maintainers.vue
app/components/PackageManagerSelect.vue app/components/Package/ManagerSelect.vue
+6 -6
app/components/PackageMetricsBadges.vue app/components/Package/MetricsBadges.vue
··· 55 55 <ul v-if="analysis" class="flex items-center gap-1.5 list-none m-0 p-0"> 56 56 <!-- TypeScript types badge --> 57 57 <li v-if="!props.isBinary"> 58 - <AppTooltip :text="typesTooltip"> 58 + <TooltipApp :text="typesTooltip"> 59 59 <component 60 60 :is="typesHref ? NuxtLink : 'span'" 61 61 :to="typesHref" ··· 76 76 /> 77 77 {{ $t('package.metrics.types_label') }} 78 78 </component> 79 - </AppTooltip> 79 + </TooltipApp> 80 80 </li> 81 81 82 82 <!-- ESM badge (show with X if missing) --> 83 83 <li> 84 - <AppTooltip :text="hasEsm ? $t('package.metrics.esm') : $t('package.metrics.no_esm')"> 84 + <TooltipApp :text="hasEsm ? $t('package.metrics.esm') : $t('package.metrics.no_esm')"> 85 85 <span 86 86 class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200" 87 87 :class=" ··· 97 97 /> 98 98 ESM 99 99 </span> 100 - </AppTooltip> 100 + </TooltipApp> 101 101 </li> 102 102 103 103 <!-- CJS badge (only show if present) --> 104 104 <li v-if="hasCjs"> 105 - <AppTooltip :text="$t('package.metrics.cjs')"> 105 + <TooltipApp :text="$t('package.metrics.cjs')"> 106 106 <span 107 107 class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs text-fg-muted bg-bg-muted border border-border rounded transition-colors duration-200" 108 108 > 109 109 <span class="i-carbon-checkmark w-3 h-3" aria-hidden="true" /> 110 110 CJS 111 111 </span> 112 - </AppTooltip> 112 + </TooltipApp> 113 113 </li> 114 114 </ul> 115 115 </template>
+4 -4
app/components/PackagePlaygrounds.vue app/components/Package/Playgrounds.vue
··· 116 116 117 117 <div ref="dropdownRef" class="relative"> 118 118 <!-- Single link: direct button --> 119 - <AppTooltip v-if="hasSingleLink && firstLink" :text="firstLink.providerName" class="w-full"> 119 + <TooltipApp v-if="hasSingleLink && firstLink" :text="firstLink.providerName" class="w-full"> 120 120 <a 121 121 :href="firstLink.url" 122 122 target="_blank" ··· 129 129 /> 130 130 <span class="truncate text-fg-muted">{{ decodeHtmlEntities(firstLink.label) }}</span> 131 131 </a> 132 - </AppTooltip> 132 + </TooltipApp> 133 133 134 134 <!-- Multiple links: dropdown button --> 135 135 <button ··· 170 170 class="absolute top-full inset-is-0 inset-ie-0 mt-1 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 py-1 overflow-visible" 171 171 @keydown="handleKeydown" 172 172 > 173 - <AppTooltip v-for="link in links" :key="link.url" :text="link.providerName" class="block"> 173 + <TooltipApp v-for="link in links" :key="link.url" :text="link.providerName" class="block"> 174 174 <a 175 175 :href="link.url" 176 176 target="_blank" ··· 185 185 /> 186 186 <span class="truncate">{{ decodeHtmlEntities(link.label) }}</span> 187 187 </a> 188 - </AppTooltip> 188 + </TooltipApp> 189 189 </div> 190 190 </Transition> 191 191 </div>
app/components/PackageReplacement.vue app/components/Package/Replacement.vue
app/components/PackageSkeleton.vue app/components/Package/Skeleton.vue
app/components/PackageSkillsCard.vue app/components/Package/SkillsCard.vue
app/components/PackageSkillsModal.vue app/components/Package/SkillsModal.vue
app/components/PackageTable.vue app/components/Package/Table.vue
app/components/PackageTableRow.vue app/components/Package/TableRow.vue
app/components/PackageVersions.vue app/components/Package/Versions.vue
app/components/PackageVulnerabilityTree.vue app/components/Package/VulnerabilityTree.vue
+4 -4
app/components/PackageWeeklyDownloadStats.vue app/components/Package/WeeklyDownloadStats.vue
··· 1 1 <script setup lang="ts"> 2 2 import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline' 3 - import { useCssVariables } from '../composables/useColors' 4 - import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '../utils/colors' 3 + import { useCssVariables } from '~/composables/useColors' 4 + import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors' 5 5 6 6 const props = defineProps<{ 7 7 packageName: string ··· 248 248 </CollapsibleSection> 249 249 </div> 250 250 251 - <ChartModal v-if="isChartModalOpen" @close="isChartModalOpen = false"> 251 + <PackageChartModal v-if="isChartModalOpen" @close="isChartModalOpen = false"> 252 252 <PackageDownloadAnalytics 253 253 :weeklyDownloads="weeklyDownloads" 254 254 :inModal="true" 255 255 :packageName="props.packageName" 256 256 :createdIso="createdIso" 257 257 /> 258 - </ChartModal> 258 + </PackageChartModal> 259 259 </template> 260 260 261 261 <style>
app/components/SearchBox.vue app/components/Header/SearchBox.vue
app/components/Toggle.client.vue app/components/Settings/Toggle.client.vue
app/components/Toggle.server.vue app/components/Settings/Toggle.server.vue
app/components/TranslationHelper.vue app/components/Settings/TranslationHelper.vue
+4 -4
app/pages/[...package].vue
··· 408 408 class="text-fg-muted hover:text-fg transition-colors duration-200" 409 409 >@{{ orgName }}</NuxtLink 410 410 ><span v-if="orgName">/</span> 411 - <AnnounceTooltip :text="$t('common.copied')" :isVisible="copiedPkgName"> 411 + <TooltipAnnounce :text="$t('common.copied')" :isVisible="copiedPkgName"> 412 412 <button 413 413 @click="copyPkgName()" 414 414 aria-describedby="copy-pkg-name" ··· 416 416 > 417 417 {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }} 418 418 </button> 419 - </AnnounceTooltip> 419 + </TooltipAnnounce> 420 420 </h1> 421 421 422 422 <span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span> ··· 875 875 :id="`pm-panel-${activePmId}`" 876 876 :aria-labelledby="`pm-tab-${activePmId}`" 877 877 > 878 - <ExecuteCommandTerminal 878 + <TerminalExecute 879 879 :package-name="pkg.name" 880 880 :jsr-info="jsrInfo" 881 881 :is-create-package="isCreatePkg" ··· 909 909 :id="`pm-panel-${activePmId}`" 910 910 :aria-labelledby="`pm-tab-${activePmId}`" 911 911 > 912 - <InstallCommandTerminal 912 + <TerminalInstall 913 913 :package-name="pkg.name" 914 914 :requested-version="requestedVersion" 915 915 :jsr-info="jsrInfo"
+1 -1
app/pages/search.vue
··· 762 762 </div> 763 763 764 764 <!-- Claim package modal --> 765 - <ClaimPackageModal ref="claimPackageModalRef" :package-name="query" /> 765 + <PackageClaimPackageModal ref="claimPackageModalRef" :package-name="query" /> 766 766 </main> 767 767 </template>
+8 -5
app/pages/settings.vue
··· 95 95 <span class="block text-sm text-fg font-medium"> 96 96 {{ $t('settings.accent_colors') }} 97 97 </span> 98 - <AccentColorPicker /> 98 + <SettingsAccentColorPicker /> 99 99 </div> 100 100 </div> 101 101 </section> ··· 107 107 </h2> 108 108 <div class="bg-bg-subtle border border-border rounded-lg p-4 sm:p-6 space-y-4"> 109 109 <!-- Relative dates toggle --> 110 - <Toggle :label="$t('settings.relative_dates')" v-model="settings.relativeDates" /> 110 + <SettingsToggle 111 + :label="$t('settings.relative_dates')" 112 + v-model="settings.relativeDates" 113 + /> 111 114 112 115 <!-- Divider --> 113 116 <div class="border-t border-border" /> 114 117 115 118 <!-- Include @types in install toggle --> 116 119 <div class="space-y-2"> 117 - <Toggle 120 + <SettingsToggle 118 121 :label="$t('settings.include_types')" 119 122 :description="$t('settings.include_types_description')" 120 123 v-model="settings.includeTypesInInstall" ··· 126 129 127 130 <!-- Hide platform-specific packages toggle --> 128 131 <div class="space-y-2"> 129 - <Toggle 132 + <SettingsToggle 130 133 :label="$t('settings.hide_platform_packages')" 131 134 :description="$t('settings.hide_platform_packages')" 132 135 v-model="settings.hidePlatformPackages" ··· 172 175 <!-- Translation helper for non-source locales --> 173 176 <template v-if="currentLocaleStatus && !isSourceLocale"> 174 177 <div class="border-t border-border pt-4"> 175 - <TranslationHelper :status="currentLocaleStatus" /> 178 + <SettingsTranslationHelper :status="currentLocaleStatus" /> 176 179 </div> 177 180 </template> 178 181
+46 -54
test/nuxt/a11y.spec.ts
··· 55 55 // Import components from #components where possible 56 56 // For server/client variants, we need to import directly to test the specific variant 57 57 import { 58 - AccentColorPicker, 59 - AnnounceTooltip, 60 58 AppFooter, 61 59 AppHeader, 62 - AppTooltip, 63 - BaseTooltip, 64 60 BuildEnvironment, 65 - ChartModal, 66 - ClaimPackageModal, 67 61 CodeDirectoryListing, 68 62 CodeFileTree, 69 63 CodeMobileTreeDrawer, ··· 75 69 CompareFacetSelector, 76 70 CompareComparisonGrid, 77 71 ComparePackageSelector, 78 - ConnectorModal, 79 72 DateTime, 80 73 DependencyPathPopup, 81 - ExecuteCommandTerminal, 82 74 FilterChips, 83 75 FilterPanel, 84 76 HeaderAccountMenu, 85 - InstallCommandTerminal, 86 77 LicenseDisplay, 87 78 LoadingSpinner, 88 79 MarkdownText, 89 - OperationsQueue, 80 + PackageChartModal, 81 + PackageClaimPackageModal, 82 + HeaderConnectorModal, 90 83 OrgMembersPanel, 84 + OrgOperationsQueue, 91 85 OrgTeamsPanel, 92 86 PackageAccessControls, 93 87 PackageCard, ··· 112 106 PaginationControls, 113 107 ProvenanceBadge, 114 108 Readme, 115 - SearchBox, 109 + SettingsAccentColorPicker, 110 + SettingsToggle, 111 + TerminalExecute, 112 + TerminalInstall, 113 + TooltipAnnounce, 114 + TooltipApp, 115 + TooltipBase, 116 + HeaderSearchBox, 116 117 SearchSuggestionCard, 117 - Toggle, 118 118 VersionSelector, 119 119 ViewModeToggle, 120 120 } from '#components' 121 121 122 122 // Server variant components must be imported directly to test the server-side render 123 123 // The #components import automatically provides the client variant 124 - import HeaderAccountMenuServer from '~/components/HeaderAccountMenu.server.vue' 125 - import ToggleServer from '~/components/Toggle.server.vue' 124 + import HeaderAccountMenuServer from '~/components/Header/AccountMenu.server.vue' 125 + import ToggleServer from '~/components/Settings/Toggle.server.vue' 126 126 127 127 describe('component accessibility audits', () => { 128 128 describe('DateTime', () => { ··· 210 210 }) 211 211 }) 212 212 213 - describe('AppTooltip', () => { 213 + describe('TooltipApp', () => { 214 214 it('should have no accessibility violations', async () => { 215 - const component = await mountSuspended(AppTooltip, { 215 + const component = await mountSuspended(TooltipApp, { 216 216 props: { text: 'Tooltip content' }, 217 217 slots: { default: '<button>Trigger</button>' }, 218 218 }) ··· 221 221 }) 222 222 }) 223 223 224 - describe('AnnounceTooltip', () => { 224 + describe('TooltipAnnounce', () => { 225 225 it('should have no accessibility violations', async () => { 226 - const component = await mountSuspended(AnnounceTooltip, { 226 + const component = await mountSuspended(TooltipAnnounce, { 227 227 props: { text: 'Tooltip content', isVisible: true }, 228 228 slots: { default: '<button>Trigger</button>' }, 229 229 }) ··· 361 361 // component has issues in the test environment (requires DOM measurements that aren't 362 362 // available during SSR-like test mounting). 363 363 364 - describe('ChartModal', () => { 364 + describe('PackageChartModal', () => { 365 365 it('should have no accessibility violations when closed', async () => { 366 - const component = await mountSuspended(ChartModal, { 366 + const component = await mountSuspended(PackageChartModal, { 367 367 props: { open: false }, 368 368 slots: { title: 'Downloads', default: '<div>Chart content</div>' }, 369 369 }) ··· 701 701 }) 702 702 }) 703 703 704 - describe('ConnectorModal', () => { 704 + describe('HeaderConnectorModal', () => { 705 705 it('should have no accessibility violations when closed', async () => { 706 - const component = await mountSuspended(ConnectorModal, { 706 + const component = await mountSuspended(HeaderConnectorModal, { 707 707 props: { open: false }, 708 708 }) 709 709 const results = await runAxe(component) ··· 711 711 }) 712 712 713 713 it('should have no accessibility violations when open (disconnected)', async () => { 714 - const component = await mountSuspended(ConnectorModal, { 714 + const component = await mountSuspended(HeaderConnectorModal, { 715 715 props: { open: true }, 716 716 }) 717 717 const results = await runAxe(component) ··· 735 735 }) 736 736 }) 737 737 738 - describe('ClaimPackageModal', () => { 738 + describe('PackageClaimPackageModal', () => { 739 739 it('should have no accessibility violations when closed', async () => { 740 - const component = await mountSuspended(ClaimPackageModal, { 740 + const component = await mountSuspended(PackageClaimPackageModal, { 741 741 props: { 742 742 packageName: 'test-package', 743 743 open: false, ··· 748 748 }) 749 749 750 750 it('should have no accessibility violations when open', async () => { 751 - const component = await mountSuspended(ClaimPackageModal, { 751 + const component = await mountSuspended(PackageClaimPackageModal, { 752 752 props: { 753 753 packageName: 'test-package', 754 754 open: true, ··· 759 759 }) 760 760 }) 761 761 762 - describe('OperationsQueue', () => { 762 + describe('OrgOperationsQueue', () => { 763 763 it('should have no accessibility violations', async () => { 764 - const component = await mountSuspended(OperationsQueue) 764 + const component = await mountSuspended(OrgOperationsQueue) 765 765 const results = await runAxe(component) 766 766 expect(results.violations).toEqual([]) 767 767 }) ··· 1424 1424 }) 1425 1425 }) 1426 1426 1427 - describe('AccentColorPicker', () => { 1428 - it('should have no accessibility violations', async () => { 1429 - const component = await mountSuspended(AccentColorPicker) 1430 - const results = await runAxe(component) 1431 - expect(results.violations).toEqual([]) 1432 - }) 1433 - }) 1434 - 1435 - describe('AuthButton.server', () => { 1427 + describe('SettingsAccentColorPicker', () => { 1436 1428 it('should have no accessibility violations', async () => { 1437 - const component = await mountSuspended(AuthButtonServer) 1429 + const component = await mountSuspended(SettingsAccentColorPicker) 1438 1430 const results = await runAxe(component) 1439 1431 expect(results.violations).toEqual([]) 1440 1432 }) 1441 1433 }) 1442 1434 1443 - describe('BaseTooltip', () => { 1435 + describe('TooltipBase', () => { 1444 1436 it('should have no accessibility violations when hidden', async () => { 1445 - const component = await mountSuspended(BaseTooltip, { 1437 + const component = await mountSuspended(TooltipBase, { 1446 1438 props: { text: 'Tooltip text', isVisible: false }, 1447 1439 slots: { default: '<button>Trigger</button>' }, 1448 1440 }) ··· 1451 1443 }) 1452 1444 1453 1445 it('should have no accessibility violations when visible', async () => { 1454 - const component = await mountSuspended(BaseTooltip, { 1446 + const component = await mountSuspended(TooltipBase, { 1455 1447 props: { text: 'Tooltip text', isVisible: true }, 1456 1448 slots: { default: '<button>Trigger</button>' }, 1457 1449 }) ··· 1505 1497 }) 1506 1498 }) 1507 1499 1508 - describe('ExecuteCommandTerminal', () => { 1500 + describe('TerminalExecute', () => { 1509 1501 it('should have no accessibility violations', async () => { 1510 - const component = await mountSuspended(ExecuteCommandTerminal, { 1502 + const component = await mountSuspended(TerminalExecute, { 1511 1503 props: { packageName: 'create-vite' }, 1512 1504 }) 1513 1505 const results = await runAxe(component) ··· 1515 1507 }) 1516 1508 1517 1509 it('should have no accessibility violations for create package', async () => { 1518 - const component = await mountSuspended(ExecuteCommandTerminal, { 1510 + const component = await mountSuspended(TerminalExecute, { 1519 1511 props: { packageName: 'create-vite', isCreatePackage: true }, 1520 1512 }) 1521 1513 const results = await runAxe(component) ··· 1523 1515 }) 1524 1516 }) 1525 1517 1526 - describe('InstallCommandTerminal', () => { 1518 + describe('TerminalInstall', () => { 1527 1519 it('should have no accessibility violations', async () => { 1528 - const component = await mountSuspended(InstallCommandTerminal, { 1520 + const component = await mountSuspended(TerminalInstall, { 1529 1521 props: { packageName: 'vue' }, 1530 1522 }) 1531 1523 const results = await runAxe(component) ··· 1533 1525 }) 1534 1526 1535 1527 it('should have no accessibility violations with version', async () => { 1536 - const component = await mountSuspended(InstallCommandTerminal, { 1528 + const component = await mountSuspended(TerminalInstall, { 1537 1529 props: { packageName: 'vue', requestedVersion: '3.5.0' }, 1538 1530 }) 1539 1531 const results = await runAxe(component) ··· 1541 1533 }) 1542 1534 1543 1535 it('should have no accessibility violations with types package', async () => { 1544 - const component = await mountSuspended(InstallCommandTerminal, { 1536 + const component = await mountSuspended(TerminalInstall, { 1545 1537 props: { packageName: 'lodash', typesPackageName: '@types/lodash' }, 1546 1538 }) 1547 1539 const results = await runAxe(component) ··· 1549 1541 }) 1550 1542 1551 1543 it('should have no accessibility violations with executable info', async () => { 1552 - const component = await mountSuspended(InstallCommandTerminal, { 1544 + const component = await mountSuspended(TerminalInstall, { 1553 1545 props: { 1554 1546 packageName: 'eslint', 1555 1547 executableInfo: { hasExecutable: true, primaryCommand: 'eslint' }, ··· 1697 1689 }) 1698 1690 }) 1699 1691 1700 - describe('SearchBox', () => { 1692 + describe('HeaderSearchBox', () => { 1701 1693 it('should have no accessibility violations', async () => { 1702 - const component = await mountSuspended(SearchBox) 1694 + const component = await mountSuspended(HeaderSearchBox) 1703 1695 const results = await runAxe(component) 1704 1696 expect(results.violations).toEqual([]) 1705 1697 }) ··· 1751 1743 1752 1744 describe('Toggle', () => { 1753 1745 it('should have no accessibility violations', async () => { 1754 - const component = await mountSuspended(Toggle, { 1746 + const component = await mountSuspended(SettingsToggle, { 1755 1747 props: { label: 'Enable feature' }, 1756 1748 }) 1757 1749 const results = await runAxe(component) ··· 1759 1751 }) 1760 1752 1761 1753 it('should have no accessibility violations with description', async () => { 1762 - const component = await mountSuspended(Toggle, { 1754 + const component = await mountSuspended(SettingsToggle, { 1763 1755 props: { label: 'Enable feature', description: 'This enables the feature' }, 1764 1756 }) 1765 1757 const results = await runAxe(component) ··· 1767 1759 }) 1768 1760 1769 1761 it('should have no accessibility violations when checked', async () => { 1770 - const component = await mountSuspended(Toggle, { 1762 + const component = await mountSuspended(SettingsToggle, { 1771 1763 props: { label: 'Enable feature', modelValue: true }, 1772 1764 }) 1773 1765 const results = await runAxe(component)
+1 -1
test/nuxt/components/PackageVersions.spec.ts
··· 1 1 import { describe, expect, it, vi, beforeEach } from 'vitest' 2 2 import { mountSuspended } from '@nuxt/test-utils/runtime' 3 - import PackageVersions from '~/components/PackageVersions.vue' 3 + import PackageVersions from '~/components/Package/Versions.vue' 4 4 import type { PackumentVersion } from '#shared/types' 5 5 6 6 // Mock the fetchAllPackageVersions function
+52 -33
test/unit/a11y-component-coverage.spec.ts
··· 27 27 'OgImage/Package.vue': 'OG Image component - server-rendered image, not interactive UI', 28 28 29 29 // Client-only components with complex dependencies 30 - 'AuthModal.client.vue': 'Complex auth modal with navigation - requires full app context', 30 + 'Header/AuthModal.client.vue': 'Complex auth modal with navigation - requires full app context', 31 31 32 32 // Complex components requiring full app context or specific runtime conditions 33 - 'HeaderOrgsDropdown.vue': 'Requires connector context and API calls', 34 - 'HeaderPackagesDropdown.vue': 'Requires connector context and API calls', 35 - 'MobileMenu.vue': 'Requires Teleport and full navigation context', 33 + 'Header/OrgsDropdown.vue': 'Requires connector context and API calls', 34 + 'Header/PackagesDropdown.vue': 'Requires connector context and API calls', 35 + 'Header/MobileMenu.vue': 'Requires Teleport and full navigation context', 36 36 'Modal.client.vue': 37 37 'Base modal component - tested via specific modals like ChartModal, ConnectorModal', 38 - 'PackageSkillsModal.vue': 'Complex modal with tabs - requires modal context and state', 38 + 'Package/SkillsModal.vue': 'Complex modal with tabs - requires modal context and state', 39 39 'ScrollToTop.vue': 'Requires scroll position and CSS scroll-state queries', 40 - 'TranslationHelper.vue': 'i18n helper component - requires specific locale status data', 41 - 'PackageWeeklyDownloadStats.vue': 40 + 'Settings/TranslationHelper.vue': 'i18n helper component - requires specific locale status data', 41 + 'Package/WeeklyDownloadStats.vue': 42 42 'Uses vue-data-ui VueUiSparkline - has DOM measurement issues in test environment', 43 + 'UserCombobox.vue': 'Unused component - intended for future admin features', 43 44 } 44 45 45 46 /** ··· 63 64 } 64 65 65 66 /** 67 + * Parse .nuxt/components.d.ts to get the mapping from component names to file paths. 68 + * This uses Nuxt's actual component resolution, so we don't have to guess the naming convention. 69 + * 70 + * Returns a Map of component name -> array of file paths (relative to app/components/) 71 + */ 72 + function parseComponentsDeclaration(dtsPath: string): Map<string, string[]> { 73 + const content = fs.readFileSync(dtsPath, 'utf-8') 74 + const componentMap = new Map<string, string[]>() 75 + 76 + // Match lines like: 77 + // export const ComponentName: typeof import("../app/components/Path/File.vue").default 78 + const exportRegex = 79 + /export const (\w+): typeof import\("\.\.\/app\/components\/([^"]+\.vue)"\)\.default/g 80 + 81 + let match 82 + while ((match = exportRegex.exec(content)) !== null) { 83 + const componentName = match[1]! 84 + const filePath = match[2]! 85 + 86 + const existing = componentMap.get(componentName) || [] 87 + if (!existing.includes(filePath)) { 88 + existing.push(filePath) 89 + } 90 + componentMap.set(componentName, existing) 91 + } 92 + 93 + return componentMap 94 + } 95 + 96 + /** 66 97 * Extract tested component names from the test file. 67 98 * Handles both #components imports and direct ~/components/ imports. 68 99 */ 69 - function getTestedComponents(testFileContent: string): Set<string> { 100 + function getTestedComponents( 101 + testFileContent: string, 102 + componentMap: Map<string, string[]>, 103 + ): Set<string> { 70 104 const tested = new Set<string>() 71 105 72 106 // Match direct imports like: 73 107 // import ComponentName from '~/components/ComponentName.vue' 74 108 // import ComponentName from '~/components/subdir/ComponentName.vue' 75 - const directImportRegex = /import\s+\w+\s+from\s+['"]~\/components\/(.+\.vue)['"]/g 109 + const directImportRegex = /import\s+\w+\s+from\s+['"]~\/components\/([^"']+\.vue)['"]/g 76 110 let match 77 111 78 112 while ((match = directImportRegex.exec(testFileContent)) !== null) { ··· 91 125 .filter(name => name.length > 0) 92 126 93 127 for (const name of componentNames) { 94 - // Map #components name to file path(s) 95 - const filePaths = mapComponentNameToFiles(name) 128 + // Look up the file paths from Nuxt's component map 129 + const filePaths = componentMap.get(name) || [] 96 130 for (const filePath of filePaths) { 97 131 tested.add(filePath) 98 132 } ··· 102 136 return tested 103 137 } 104 138 105 - /** 106 - * Map a #components export name to the actual file path(s). 107 - * Handles various naming conventions. 108 - * 109 - * Returns an array because importing from #components can cover multiple files: 110 - * - `HeaderAccountMenu` from #components -> tests HeaderAccountMenu.client.vue 111 - * (Nuxt auto-resolves to client variant when both .server and .client exist) 112 - */ 113 - function mapComponentNameToFiles(name: string): string[] { 114 - // Handle Compare* prefix -> compare/ subdirectory 115 - if (name.startsWith('Compare')) { 116 - const baseName = name.slice('Compare'.length) 117 - return [`compare/${baseName}.vue`] 118 - } 119 - 120 - // Regular component - could be .vue or .client.vue 121 - // When importing from #components, Nuxt resolves to the client variant if it exists 122 - return [`${name}.vue`, `${name}.client.vue`] 123 - } 124 - 125 139 describe('a11y component test coverage', () => { 126 140 const componentsDir = fileURLToPath(new URL('../../app/components', import.meta.url)) 141 + const componentsDtsPath = fileURLToPath(new URL('../../.nuxt/components.d.ts', import.meta.url)) 127 142 const testFilePath = fileURLToPath(new URL('../nuxt/a11y.spec.ts', import.meta.url)) 128 143 129 144 it('should have accessibility tests for all components (or be explicitly skipped)', () => { 130 145 // Get all Vue components 131 146 const allComponents = getVueFiles(componentsDir) 132 147 148 + // Parse Nuxt's component declarations to get name -> path mapping 149 + const componentMap = parseComponentsDeclaration(componentsDtsPath) 150 + 133 151 // Get components that are tested 134 152 const testFileContent = fs.readFileSync(testFilePath, 'utf-8') 135 - const testedComponents = getTestedComponents(testFileContent) 153 + const testedComponents = getTestedComponents(testFileContent, componentMap) 136 154 137 155 // Find components that are neither tested nor skipped 138 156 const missingTests = allComponents.filter( ··· 155 173 }) 156 174 157 175 it('should not skip components that are actually tested', () => { 176 + const componentMap = parseComponentsDeclaration(componentsDtsPath) 158 177 const testFileContent = fs.readFileSync(testFilePath, 'utf-8') 159 - const testedComponents = getTestedComponents(testFileContent) 178 + const testedComponents = getTestedComponents(testFileContent, componentMap) 160 179 161 180 const unnecessarySkips = Object.keys(SKIPPED_COMPONENTS).filter(component => 162 181 testedComponents.has(component),