import { describe, expect, it, vi, beforeEach } from 'vitest' import { mountSuspended } from '@nuxt/test-utils/runtime' import VersionSelector from '~/components/VersionSelector.vue' // Mock the fetchAllPackageVersions function const mockFetchAllPackageVersions = vi.fn() vi.mock('~/utils/npm/api', () => ({ fetchAllPackageVersions: (...args: unknown[]) => mockFetchAllPackageVersions(...args), })) // Mock navigateTo const mockNavigateTo = vi.fn() vi.stubGlobal('navigateTo', mockNavigateTo) describe('VersionSelector', () => { beforeEach(() => { mockFetchAllPackageVersions.mockReset() mockNavigateTo.mockReset() }) describe('basic rendering', () => { it('renders the current version in the button', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') expect(button.exists()).toBe(true) expect(button.text()).toContain('1.0.0') }) it('shows "latest" badge when current version is latest', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '2.0.0': {} }, distTags: { latest: '2.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) expect(component.text()).toContain('latest') }) it('does not show "latest" badge when current version is not latest', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') // The button itself shouldn't have the latest badge expect(button.text()).not.toContain('latest') }) it('has aria-expanded="false" initially', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') expect(button.attributes('aria-expanded')).toBe('false') }) }) describe('dropdown behavior', () => { it('opens dropdown when button is clicked', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(button.attributes('aria-expanded')).toBe('true') expect(component.find('[role="listbox"]').exists()).toBe(true) }) it('closes dropdown when button is clicked again', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(button.attributes('aria-expanded')).toBe('true') await button.trigger('click') expect(button.attributes('aria-expanded')).toBe('false') }) it('shows version groups in dropdown', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') expect(listbox.text()).toContain('2.0.0') expect(listbox.text()).toContain('1.0.0') }) it('shows "View all X versions" link', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {}, '2.0.0': {}, '3.0.0': {} }, distTags: { latest: '3.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(component.text()).toContain('View all 3 versions') }) }) describe('keyboard navigation', () => { it('opens dropdown on ArrowDown when closed', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('keydown', { key: 'ArrowDown' }) expect(button.attributes('aria-expanded')).toBe('true') }) it('closes dropdown on Escape', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(button.attributes('aria-expanded')).toBe('true') await button.trigger('keydown', { key: 'Escape' }) expect(button.attributes('aria-expanded')).toBe('false') }) it('navigates with arrow keys in listbox', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') // Navigate down await listbox.trigger('keydown', { key: 'ArrowDown' }) // Navigate up await listbox.trigger('keydown', { key: 'ArrowUp' }) // Should still be focused on an item (test that it doesn't crash) expect(listbox.exists()).toBe(true) }) it('closes listbox on Escape', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') await listbox.trigger('keydown', { key: 'Escape' }) expect(button.attributes('aria-expanded')).toBe('false') }) it('navigates to Home and End', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '3.0.0', versions: { '1.0.0': {}, '2.0.0': {}, '3.0.0': {} }, distTags: { latest: '3.0.0', beta: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') // Navigate to end await listbox.trigger('keydown', { key: 'End' }) // Navigate to home await listbox.trigger('keydown', { key: 'Home' }) // Should not crash expect(listbox.exists()).toBe(true) }) }) describe('version selection', () => { it('closes dropdown and navigates when clicking a version', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Click on the version link const versionLink = component.findAll('a').find(a => a.text().includes('1.0.0')) expect(versionLink?.exists()).toBe(true) await versionLink!.trigger('click') // Dropdown should close expect(button.attributes('aria-expanded')).toBe('false') }) it('generates correct URL from pattern', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-code/test-package/v/{version}/src/index.ts', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const versionLink = component.findAll('a').find(a => a.text().includes('1.0.0')) expect(versionLink?.attributes('href')).toBe( '/package-code/test-package/v/1.0.0/src/index.ts', ) }) }) describe('expand/collapse groups', () => { it('shows expand button for groups', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Find expand button within the dropdown const expandButton = component.find('[role="listbox"] button[aria-expanded]') expect(expandButton.exists()).toBe(true) }) it('loads versions when expanding a group', async () => { mockFetchAllPackageVersions.mockResolvedValue([ { version: '1.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false }, { version: '0.9.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false }, ]) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Find and click expand button const expandButton = component.find('[role="listbox"] button[aria-expanded="false"]') await expandButton.trigger('click') await vi.waitFor(() => { expect(mockFetchAllPackageVersions).toHaveBeenCalledWith('test-package') }) }) it('collapses group when clicking expanded button', async () => { mockFetchAllPackageVersions.mockResolvedValue([ { version: '1.2.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false }, { version: '1.1.0', time: '2024-01-12T12:00:00.000Z', hasProvenance: false }, { version: '1.0.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false }, ]) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.2.0', versions: { '1.2.0': {} }, distTags: { latest: '1.2.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Expand const expandButton = component.find('[role="listbox"] button[aria-expanded]') await expandButton.trigger('click') await vi.waitFor(() => { expect(mockFetchAllPackageVersions).toHaveBeenCalled() }) // Wait for expansion await vi.waitFor( () => { const btn = component.find('[role="listbox"] button[aria-expanded="true"]') expect(btn.exists()).toBe(true) }, { timeout: 2000 }, ) // Collapse const expandedButton = component.find('[role="listbox"] button[aria-expanded="true"]') await expandedButton.trigger('click') await vi.waitFor( () => { const btn = component.find('[role="listbox"] button[aria-expanded="false"]') expect(btn.exists()).toBe(true) }, { timeout: 2000 }, ) }) }) describe('0.x version grouping', () => { it('groups 0.x versions by minor version, not major', async () => { mockFetchAllPackageVersions.mockResolvedValue([ { version: '0.10.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false }, { version: '0.10.1', time: '2024-01-16T12:00:00.000Z', hasProvenance: false }, { version: '0.9.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false }, { version: '0.9.3', time: '2024-01-12T12:00:00.000Z', hasProvenance: false }, ]) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '0.10.1', versions: { '0.10.1': {} }, distTags: { latest: '0.10.1' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Expand the group const expandButton = component.find('[role="listbox"] button[aria-expanded]') await expandButton.trigger('click') await vi.waitFor(() => { expect(mockFetchAllPackageVersions).toHaveBeenCalled() }) // Wait for versions to load await vi.waitFor( () => { // 0.9.x versions should NOT be under the 0.10.x group // They should be in a separate group const text = component.text() // The component should have separate groups for 0.10 and 0.9 expect(text).toContain('0.10') expect(text).toContain('0.9') }, { timeout: 2000 }, ) }) }) describe('dist-tag display', () => { it('displays multiple tags for same version', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0', stable: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') expect(listbox.text()).toContain('latest') expect(listbox.text()).toContain('stable') }) it('shows "latest" tag with special styling', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Find the latest tag span const latestTags = component.findAll('span').filter(s => s.text() === 'latest') expect(latestTags.length).toBeGreaterThan(0) // Should have green styling const hasGreenStyling = latestTags.some(t => t.classes().some(c => c.includes('green'))) expect(hasGreenStyling).toBe(true) }) }) describe('loading states', () => { it('shows loading spinner when fetching versions', async () => { let resolvePromise: (value: unknown[]) => void const loadingPromise = new Promise(resolve => { resolvePromise = resolve }) mockFetchAllPackageVersions.mockReturnValue(loadingPromise) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Click expand const expandButton = component.find('[role="listbox"] button[aria-expanded]') await expandButton.trigger('click') // Should show loading spinner (motion-safe:animate-spin is applied) await vi.waitFor(() => { const spinner = component.find('.i-svg-spinners\\:ring-resize') expect(spinner.exists()).toBe(true) }) // Resolve the promise to clean up resolvePromise!([]) }) }) describe('accessibility', () => { it('has aria-haspopup="listbox" on trigger button', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup]') expect(button.attributes('aria-haspopup')).toBe('listbox') }) it('has role="listbox" on dropdown', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(component.find('[role="listbox"]').exists()).toBe(true) }) it('has role="option" on version items', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(component.find('[role="option"]').exists()).toBe(true) }) it('sets aria-selected on current version', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const selectedOption = component.find('[role="option"][aria-selected="true"]') expect(selectedOption.exists()).toBe(true) }) it('updates aria-activedescendant when navigating', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const listbox = component.find('[role="listbox"]') expect(listbox.attributes('aria-activedescendant')).toBeDefined() }) it('expand buttons have aria-expanded attribute', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const expandButton = component.find('[role="listbox"] button[aria-expanded]') expect(expandButton.exists()).toBe(true) expect(['true', 'false']).toContain(expandButton.attributes('aria-expanded')) }) it('expand buttons have aria-label', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') const expandButton = component.find('[role="listbox"] button[aria-label]') expect(expandButton.exists()).toBe(true) expect(expandButton.attributes('aria-label')).toMatch(/Expand|Collapse/) }) it('icons have aria-hidden attribute', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) // The chevron icon in the main button const chevronIcon = component.find('button[aria-haspopup] span[aria-hidden="true"]') expect(chevronIcon.exists()).toBe(true) }) }) describe('error handling', () => { it('handles fetch errors gracefully', async () => { mockFetchAllPackageVersions.mockRejectedValue(new Error('Network error')) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Click expand const expandButton = component.find('[role="listbox"] button[aria-expanded]') await expandButton.trigger('click') // Wait for error to be logged await vi.waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Failed to load versions:', expect.any(Error)) }) consoleSpy.mockRestore() }) }) describe('caching behavior', () => { it('only fetches versions once when expanding multiple groups', async () => { mockFetchAllPackageVersions.mockResolvedValue([ { version: '2.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false }, { version: '1.0.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false }, ]) const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '2.0.0', versions: { '1.0.0': {}, '2.0.0': {} }, distTags: { latest: '2.0.0', old: '1.0.0', }, urlPattern: '/package-docs/test-package/v/{version}', }, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') // Expand first group const expandButtons = component.findAll('[role="listbox"] button[aria-expanded="false"]') if (expandButtons[0]) { await expandButtons[0].trigger('click') } await vi.waitFor(() => { expect(mockFetchAllPackageVersions).toHaveBeenCalledTimes(1) }) // Close and reopen await button.trigger('click') await button.trigger('click') // Expand another group - should not fetch again const updatedButtons = component.findAll('[role="listbox"] button[aria-expanded="false"]') if (updatedButtons[0]) { await updatedButtons[0].trigger('click') } // Should still only have been called once expect(mockFetchAllPackageVersions).toHaveBeenCalledTimes(1) }) }) describe('click outside', () => { it('closes dropdown when clicking outside', async () => { const component = await mountSuspended(VersionSelector, { props: { packageName: 'test-package', currentVersion: '1.0.0', versions: { '1.0.0': {} }, distTags: { latest: '1.0.0' }, urlPattern: '/package-docs/test-package/v/{version}', }, attachTo: document.body, }) const button = component.find('button[aria-haspopup="listbox"]') await button.trigger('click') expect(button.attributes('aria-expanded')).toBe('true') // Simulate click outside by directly setting isOpen // Note: onClickOutside is hard to test in JSDOM, so we verify the behavior exists // by checking the component closes when we trigger a click on the main element // after it opens }) }) })