Skip to content

Commit 318cc09

Browse files
committed
test: mock client side requests as well
1 parent 6a33bd7 commit 318cc09

9 files changed

Lines changed: 324 additions & 8 deletions

test/e2e/badge.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
function toLocalUrl(baseURL: string | undefined, path: string): string {
44
if (!baseURL) return path

test/e2e/create-command.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
test.describe('Create Command', () => {
44
test.describe('Visibility', () => {

test/e2e/docs.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
test.describe('API Documentation Pages', () => {
44
test('docs page loads and shows content for a package', async ({ page, goto }) => {

test/e2e/interactions.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
test.describe('Search Pages', () => {
44
test('/search?q=vue → keyboard navigation (arrow keys + enter)', async ({ page, goto }) => {

test/e2e/og-image.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
const paths = ['/', '/package/nuxt/v/3.20.2']
44
for (const path of paths) {

test/e2e/package-manager-select.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
test.describe('Package Page', () => {
44
test('/vue → package manager select dropdown works', async ({ page, goto }) => {

test/e2e/test-utils.ts

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import { test as base } from '@nuxt/test-utils/playwright'
2+
import type { Page, Route } from '@playwright/test'
3+
import { readFileSync, existsSync } from 'node:fs'
4+
import { join } from 'node:path'
5+
6+
const FIXTURES_DIR = join(process.cwd(), 'test/fixtures')
7+
8+
/**
9+
* Read a fixture file from disk
10+
*/
11+
function readFixture(relativePath: string): unknown | null {
12+
const fullPath = join(FIXTURES_DIR, relativePath)
13+
if (!existsSync(fullPath)) {
14+
return null
15+
}
16+
try {
17+
return JSON.parse(readFileSync(fullPath, 'utf-8'))
18+
} catch {
19+
return null
20+
}
21+
}
22+
23+
/**
24+
* Convert a package name to a fixture file path
25+
*/
26+
function packageToFixturePath(packageName: string): string {
27+
// Scoped packages: @scope/name -> @scope/name.json (in subdirectory)
28+
if (packageName.startsWith('@')) {
29+
const [scope, name] = packageName.slice(1).split('/')
30+
return `npm-registry/packuments/@${scope}/${name}.json`
31+
}
32+
return `npm-registry/packuments/${packageName}.json`
33+
}
34+
35+
/**
36+
* Handle npm registry requests (registry.npmjs.org)
37+
*/
38+
async function handleNpmRegistry(route: Route): Promise<boolean> {
39+
const url = new URL(route.request().url())
40+
const pathname = decodeURIComponent(url.pathname)
41+
42+
// Search endpoint: /-/v1/search?text=query
43+
if (pathname === '/-/v1/search') {
44+
const query = url.searchParams.get('text')
45+
if (query) {
46+
// Check for maintainer search (user lookup)
47+
const maintainerMatch = query.match(/^maintainer:(.+)$/)
48+
if (maintainerMatch?.[1]) {
49+
const fixture = readFixture(`users/${maintainerMatch[1]}.json`)
50+
if (fixture) {
51+
await route.fulfill({ json: fixture })
52+
return true
53+
}
54+
// Return empty results for unknown users
55+
await route.fulfill({
56+
json: { objects: [], total: 0, time: new Date().toISOString() },
57+
})
58+
return true
59+
}
60+
61+
// Regular search
62+
const searchName = query.replace(/:/g, '-')
63+
const fixture = readFixture(`npm-registry/search/${searchName}.json`)
64+
if (fixture) {
65+
await route.fulfill({ json: fixture })
66+
return true
67+
}
68+
69+
// Return empty results for searches without fixtures
70+
await route.fulfill({
71+
json: { objects: [], total: 0, time: new Date().toISOString() },
72+
})
73+
return true
74+
}
75+
}
76+
77+
// Org packages: /-/org/{orgname}/package
78+
const orgMatch = pathname.match(/^\/-\/org\/([^/]+)\/package$/)
79+
if (orgMatch?.[1]) {
80+
const fixture = readFixture(`npm-registry/orgs/${orgMatch[1]}.json`)
81+
if (fixture) {
82+
await route.fulfill({ json: fixture })
83+
return true
84+
}
85+
}
86+
87+
// Packument: /{package} or /{package}/{version}
88+
if (!pathname.startsWith('/-/')) {
89+
let packageName = pathname.slice(1)
90+
91+
// Strip version if present
92+
if (packageName.startsWith('@')) {
93+
const parts = packageName.split('/')
94+
if (parts.length > 2) {
95+
packageName = `${parts[0]}/${parts[1]}`
96+
}
97+
} else {
98+
const slashIndex = packageName.indexOf('/')
99+
if (slashIndex !== -1) {
100+
packageName = packageName.slice(0, slashIndex)
101+
}
102+
}
103+
104+
const fixturePath = packageToFixturePath(packageName)
105+
const fixture = readFixture(fixturePath)
106+
if (fixture) {
107+
await route.fulfill({ json: fixture })
108+
return true
109+
}
110+
111+
// Return 404 for unknown packages
112+
await route.fulfill({
113+
status: 404,
114+
json: { error: 'Not found' },
115+
})
116+
return true
117+
}
118+
119+
return false
120+
}
121+
122+
/**
123+
* Handle npm API requests (api.npmjs.org)
124+
*/
125+
async function handleNpmApi(route: Route): Promise<boolean> {
126+
const url = new URL(route.request().url())
127+
const pathname = decodeURIComponent(url.pathname)
128+
129+
// Downloads point: /downloads/point/{period}/{package}
130+
const pointMatch = pathname.match(/^\/downloads\/point\/[^/]+\/(.+)$/)
131+
if (pointMatch?.[1]) {
132+
const packageName = pointMatch[1]
133+
const fixturePath = `npm-api/downloads/${packageName}.json`
134+
const fixture = readFixture(fixturePath)
135+
if (fixture) {
136+
await route.fulfill({ json: fixture })
137+
return true
138+
}
139+
// Return zero downloads for unknown packages
140+
await route.fulfill({
141+
json: { downloads: 0, start: '2025-01-01', end: '2025-01-31', package: packageName },
142+
})
143+
return true
144+
}
145+
146+
// Downloads range: /downloads/range/{period}/{package}
147+
// This is used for download charts - return empty data
148+
const rangeMatch = pathname.match(/^\/downloads\/range\/[^/]+\/(.+)$/)
149+
if (rangeMatch?.[1]) {
150+
const packageName = rangeMatch[1]
151+
// Return empty downloads array for range requests
152+
await route.fulfill({
153+
json: { downloads: [], start: '2025-01-01', end: '2025-01-31', package: packageName },
154+
})
155+
return true
156+
}
157+
158+
return false
159+
}
160+
161+
/**
162+
* Handle OSV API requests (api.osv.dev)
163+
*/
164+
async function handleOsvApi(route: Route): Promise<boolean> {
165+
const url = new URL(route.request().url())
166+
167+
if (url.pathname === '/v1/querybatch') {
168+
await route.fulfill({ json: { results: [] } })
169+
return true
170+
}
171+
172+
if (url.pathname.startsWith('/v1/query')) {
173+
await route.fulfill({ json: { vulns: [] } })
174+
return true
175+
}
176+
177+
return false
178+
}
179+
180+
/**
181+
* Handle fast-npm-meta requests (npm.antfu.dev)
182+
*/
183+
async function handleFastNpmMeta(route: Route): Promise<boolean> {
184+
const url = new URL(route.request().url())
185+
let packageName = decodeURIComponent(url.pathname.slice(1))
186+
187+
if (!packageName) return false
188+
189+
// Handle @version syntax (vue@3.4.0, @nuxt/kit@3.0.0)
190+
let specifier = 'latest'
191+
if (packageName.startsWith('@')) {
192+
const atIndex = packageName.indexOf('@', 1)
193+
if (atIndex !== -1) {
194+
specifier = packageName.slice(atIndex + 1)
195+
packageName = packageName.slice(0, atIndex)
196+
}
197+
} else {
198+
const atIndex = packageName.indexOf('@')
199+
if (atIndex !== -1) {
200+
specifier = packageName.slice(atIndex + 1)
201+
packageName = packageName.slice(0, atIndex)
202+
}
203+
}
204+
205+
const fixturePath = packageToFixturePath(packageName)
206+
const packument = readFixture(fixturePath) as Record<string, unknown> | null
207+
208+
if (!packument) return false
209+
210+
const distTags = packument['dist-tags'] as Record<string, string> | undefined
211+
const versions = packument.versions as Record<string, unknown> | undefined
212+
const time = packument.time as Record<string, string> | undefined
213+
214+
let version: string | undefined
215+
if (specifier === 'latest' || !specifier) {
216+
version = distTags?.latest
217+
} else if (distTags?.[specifier]) {
218+
version = distTags[specifier]
219+
} else if (versions?.[specifier]) {
220+
version = specifier
221+
} else {
222+
version = distTags?.latest
223+
}
224+
225+
if (!version) return false
226+
227+
await route.fulfill({
228+
json: {
229+
name: packageName,
230+
specifier,
231+
version,
232+
publishedAt: time?.[version] || new Date().toISOString(),
233+
lastSynced: Date.now(),
234+
},
235+
})
236+
return true
237+
}
238+
239+
/**
240+
* Handle JSR registry requests (jsr.io)
241+
*/
242+
async function handleJsrRegistry(route: Route): Promise<boolean> {
243+
const url = new URL(route.request().url())
244+
245+
if (url.pathname.endsWith('/meta.json')) {
246+
// Most npm packages aren't on JSR, return null
247+
await route.fulfill({ json: null })
248+
return true
249+
}
250+
251+
return false
252+
}
253+
254+
/**
255+
* Setup route mocking for a page
256+
*/
257+
async function setupRouteMocking(page: Page): Promise<void> {
258+
// Mock npm registry (registry.npmjs.org)
259+
await page.route('https://registry.npmjs.org/**', async route => {
260+
const handled = await handleNpmRegistry(route)
261+
if (!handled) {
262+
// Log unhandled requests for debugging
263+
console.warn(`[mock] Unhandled npm registry request: ${route.request().url()}`)
264+
await route.continue()
265+
}
266+
})
267+
268+
// Mock npm API (api.npmjs.org)
269+
await page.route('https://api.npmjs.org/**', async route => {
270+
const handled = await handleNpmApi(route)
271+
if (!handled) {
272+
console.warn(`[mock] Unhandled npm API request: ${route.request().url()}`)
273+
await route.continue()
274+
}
275+
})
276+
277+
// Mock OSV API (api.osv.dev)
278+
await page.route('https://api.osv.dev/**', async route => {
279+
const handled = await handleOsvApi(route)
280+
if (!handled) {
281+
await route.continue()
282+
}
283+
})
284+
285+
// Mock fast-npm-meta (npm.antfu.dev)
286+
await page.route('https://npm.antfu.dev/**', async route => {
287+
const handled = await handleFastNpmMeta(route)
288+
if (!handled) {
289+
console.warn(`[mock] Unhandled fast-npm-meta request: ${route.request().url()}`)
290+
await route.continue()
291+
}
292+
})
293+
294+
// Mock JSR registry (jsr.io)
295+
await page.route('https://jsr.io/**', async route => {
296+
const handled = await handleJsrRegistry(route)
297+
if (!handled) {
298+
await route.continue()
299+
}
300+
})
301+
}
302+
303+
/**
304+
* Extended test fixture with external API mocking
305+
*/
306+
export const test = base.extend<{ mockExternalApis: void }>({
307+
mockExternalApis: [
308+
async ({ page }, use) => {
309+
await setupRouteMocking(page)
310+
await use()
311+
},
312+
{ auto: true }, // Automatically use this fixture for all tests
313+
],
314+
})
315+
316+
export { expect } from '@nuxt/test-utils/playwright'

test/e2e/url-compatibility.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
test.describe('npmjs.com URL Compatibility', () => {
44
test.describe('Package Pages', () => {

test/e2e/vulnerabilities.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@nuxt/test-utils/playwright'
1+
import { expect, test } from './test-utils'
22

33
function toLocalUrl(baseURL: string | undefined, path: string): string {
44
if (!baseURL) return path

0 commit comments

Comments
 (0)