Skip to content

Commit 550f30d

Browse files
committed
test: add basic e2e and unit tests
1 parent 451535d commit 550f30d

2 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { parsePackageParam } from '../../shared/utils/npm'
3+
4+
describe('parsePackageParam', () => {
5+
describe('unscoped packages', () => {
6+
it('parses package name without version', () => {
7+
const result = parsePackageParam('vue')
8+
expect(result).toEqual({
9+
packageName: 'vue',
10+
version: undefined,
11+
rest: [],
12+
})
13+
})
14+
15+
it('parses package name with version', () => {
16+
const result = parsePackageParam('vue/v/3.4.0')
17+
expect(result).toEqual({
18+
packageName: 'vue',
19+
version: '3.4.0',
20+
rest: [],
21+
})
22+
})
23+
24+
it('parses package name with prerelease version', () => {
25+
const result = parsePackageParam('nuxt/v/4.0.0-rc.1')
26+
expect(result).toEqual({
27+
packageName: 'nuxt',
28+
version: '4.0.0-rc.1',
29+
rest: [],
30+
})
31+
})
32+
33+
it('parses package name with version and file path', () => {
34+
const result = parsePackageParam('vue/v/3.4.0/src/index.ts')
35+
expect(result).toEqual({
36+
packageName: 'vue',
37+
version: '3.4.0',
38+
rest: ['src', 'index.ts'],
39+
})
40+
})
41+
42+
it('parses package name with version and nested file path', () => {
43+
const result = parsePackageParam('lodash/v/4.17.21/lib/fp/map.js')
44+
expect(result).toEqual({
45+
packageName: 'lodash',
46+
version: '4.17.21',
47+
rest: ['lib', 'fp', 'map.js'],
48+
})
49+
})
50+
})
51+
52+
describe('scoped packages', () => {
53+
it('parses scoped package name without version', () => {
54+
const result = parsePackageParam('@nuxt/kit')
55+
expect(result).toEqual({
56+
packageName: '@nuxt/kit',
57+
version: undefined,
58+
rest: [],
59+
})
60+
})
61+
62+
it('parses scoped package name with version', () => {
63+
const result = parsePackageParam('@nuxt/kit/v/1.0.0')
64+
expect(result).toEqual({
65+
packageName: '@nuxt/kit',
66+
version: '1.0.0',
67+
rest: [],
68+
})
69+
})
70+
71+
it('parses scoped package name with version and file path', () => {
72+
const result = parsePackageParam('@vue/compiler-sfc/v/3.5.0/dist/index.d.ts')
73+
expect(result).toEqual({
74+
packageName: '@vue/compiler-sfc',
75+
version: '3.5.0',
76+
rest: ['dist', 'index.d.ts'],
77+
})
78+
})
79+
80+
it('parses deeply nested scoped packages', () => {
81+
const result = parsePackageParam('@types/node/v/22.0.0')
82+
expect(result).toEqual({
83+
packageName: '@types/node',
84+
version: '22.0.0',
85+
rest: [],
86+
})
87+
})
88+
})
89+
90+
describe('edge cases', () => {
91+
it('handles package name that looks like a version marker', () => {
92+
// Package named "v" shouldn't be confused with version separator
93+
const result = parsePackageParam('v')
94+
expect(result).toEqual({
95+
packageName: 'v',
96+
version: undefined,
97+
rest: [],
98+
})
99+
})
100+
101+
it('handles version segment without actual version', () => {
102+
// "v" at the end without a version after it
103+
const result = parsePackageParam('vue/v')
104+
expect(result).toEqual({
105+
packageName: 'vue/v',
106+
version: undefined,
107+
rest: [],
108+
})
109+
})
110+
111+
it('handles package with "v" in the name followed by version', () => {
112+
const result = parsePackageParam('vueuse/v/12.0.0')
113+
expect(result).toEqual({
114+
packageName: 'vueuse',
115+
version: '12.0.0',
116+
rest: [],
117+
})
118+
})
119+
120+
it('handles empty rest when file path is empty', () => {
121+
const result = parsePackageParam('react/v/18.2.0')
122+
expect(result.rest).toEqual([])
123+
expect(result.rest.length).toBe(0)
124+
})
125+
})
126+
})

tests/docs.spec.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { expect, test } from '@nuxt/test-utils/playwright'
2+
3+
test.describe('API Documentation Pages', () => {
4+
test('docs page loads and shows content for a package', async ({ page, goto }) => {
5+
// Use a small, stable package with TypeScript types
6+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
7+
8+
// Page title should include package name
9+
await expect(page).toHaveTitle(/ufo.*docs/i)
10+
11+
// Header should show package name and version
12+
await expect(page.locator('header').getByText('ufo')).toBeVisible()
13+
await expect(page.locator('header').getByText('1.6.3')).toBeVisible()
14+
15+
// API Docs badge should be visible
16+
await expect(page.locator('text=API Docs')).toBeVisible()
17+
18+
// Should have documentation content
19+
const docsContent = page.locator('.docs-content')
20+
await expect(docsContent).toBeVisible()
21+
22+
// Should have at least one function documented
23+
await expect(page.locator('.docs-badge--function').first()).toBeVisible()
24+
})
25+
26+
test('docs page shows TOC sidebar on desktop', async ({ page, goto }) => {
27+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
28+
29+
// TOC sidebar should be visible (on desktop viewport)
30+
const tocSidebar = page.locator('aside')
31+
await expect(tocSidebar).toBeVisible()
32+
33+
// Should have "Contents" heading
34+
await expect(tocSidebar.getByText('Contents')).toBeVisible()
35+
36+
// Should have section links (Functions, etc.)
37+
await expect(tocSidebar.locator('a[href="#section-function"]')).toBeVisible()
38+
})
39+
40+
test('TOC links navigate to sections', async ({ page, goto }) => {
41+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
42+
43+
// Click on Functions in TOC
44+
const functionsLink = page.locator('aside a[href="#section-function"]')
45+
await functionsLink.click()
46+
47+
// URL should have the hash
48+
await expect(page).toHaveURL(/#section-function/)
49+
50+
// Section should be scrolled into view
51+
const functionSection = page.locator('#section-function')
52+
await expect(functionSection).toBeInViewport()
53+
})
54+
55+
test('clicking symbol name scrolls to symbol', async ({ page, goto }) => {
56+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
57+
58+
// Find a symbol link in the TOC
59+
const symbolLink = page.locator('aside a[href^="#function-"]').first()
60+
const href = await symbolLink.getAttribute('href')
61+
62+
// Click the symbol link
63+
await symbolLink.click()
64+
65+
// URL should have the hash
66+
await expect(page).toHaveURL(new RegExp(href!.replace('#', '#')))
67+
})
68+
69+
test('docs page without version redirects to latest', async ({ page, goto }) => {
70+
await goto('/docs/ufo', { waitUntil: 'networkidle' })
71+
72+
// Should redirect to include version
73+
await expect(page).toHaveURL(/\/docs\/ufo\/v\//)
74+
})
75+
76+
test('package link in header navigates to package page', async ({ page, goto }) => {
77+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
78+
79+
// Click on package name in header
80+
const packageLink = page.locator('header a').filter({ hasText: 'ufo' })
81+
await packageLink.click()
82+
83+
// Should navigate to package page (URL ends with /ufo)
84+
await expect(page).toHaveURL(/\/ufo$/)
85+
})
86+
87+
test('docs page handles package gracefully when types unavailable', async ({ page, goto }) => {
88+
// Use a simple JS package - the page should load without crashing
89+
// regardless of whether it has types or shows an error state
90+
await goto('/docs/is-odd/v/3.0.1', { waitUntil: 'networkidle' })
91+
92+
// Header should always show the package name
93+
await expect(page.locator('header').getByText('is-odd')).toBeVisible()
94+
95+
// Page should be in one of two states:
96+
// 1. Shows "not available" / error message
97+
// 2. Shows actual docs content (if types were found)
98+
const errorState = page.locator('text=/not available|could not generate/i')
99+
const docsContent = page.locator('.docs-content')
100+
101+
// One of these should be visible
102+
await expect(errorState.or(docsContent)).toBeVisible()
103+
})
104+
})
105+
106+
test.describe('Version Selector', () => {
107+
test('version selector dropdown shows versions', async ({ page, goto }) => {
108+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
109+
110+
// Find and click the version selector button
111+
const versionButton = page.locator('header button').filter({ hasText: '1.6.3' })
112+
113+
// Skip if version selector not present (data might not be loaded)
114+
if (!(await versionButton.isVisible())) {
115+
test.skip()
116+
return
117+
}
118+
119+
await versionButton.click()
120+
121+
// Dropdown should appear with version options
122+
const dropdown = page.locator('[role="listbox"]')
123+
await expect(dropdown).toBeVisible()
124+
125+
// Should show multiple versions
126+
const versionOptions = dropdown.locator('[role="option"]')
127+
await expect(versionOptions.first()).toBeVisible()
128+
})
129+
130+
test('selecting a version navigates to that version', async ({ page, goto }) => {
131+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
132+
133+
// Find and click the version selector button
134+
const versionButton = page.locator('header button').filter({ hasText: '1.6.3' })
135+
136+
// Skip if version selector not present
137+
if (!(await versionButton.isVisible())) {
138+
test.skip()
139+
return
140+
}
141+
142+
await versionButton.click()
143+
144+
// Find a different version and click it
145+
const differentVersion = page.locator('[role="option"]').filter({ hasNotText: '1.6.3' }).first()
146+
147+
// Skip if no other versions available
148+
if (!(await differentVersion.isVisible())) {
149+
test.skip()
150+
return
151+
}
152+
153+
const versionText = await differentVersion.textContent()
154+
await differentVersion.click()
155+
156+
// URL should change to the new version
157+
if (versionText) {
158+
const versionMatch = versionText.match(/\d+\.\d+\.\d+/)
159+
if (versionMatch) {
160+
await expect(page).toHaveURL(new RegExp(`/docs/ufo/v/${versionMatch[0]}`))
161+
}
162+
}
163+
})
164+
165+
test('escape key closes version dropdown', async ({ page, goto }) => {
166+
await goto('/docs/ufo/v/1.6.3', { waitUntil: 'networkidle' })
167+
168+
const versionButton = page.locator('header button').filter({ hasText: '1.6.3' })
169+
170+
if (!(await versionButton.isVisible())) {
171+
test.skip()
172+
return
173+
}
174+
175+
await versionButton.click()
176+
177+
const dropdown = page.locator('[role="listbox"]')
178+
await expect(dropdown).toBeVisible()
179+
180+
// Press escape
181+
await page.keyboard.press('Escape')
182+
183+
// Dropdown should close
184+
await expect(dropdown).not.toBeVisible()
185+
})
186+
})

0 commit comments

Comments
 (0)