forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { ConsoleMessage, Page, Route } from '@playwright/test'
2import { test as base, expect } from '@nuxt/test-utils/playwright'
3import { createRequire } from 'node:module'
4
5const require = createRequire(import.meta.url)
6const mockRoutes = require('../fixtures/mock-routes.cjs')
7
8/**
9 * Fail the test with a clear error message when an external API request isn't mocked.
10 */
11function failUnmockedRequest(route: Route, apiName: string): never {
12 const url = route.request().url()
13 const error = new Error(
14 `\n` +
15 `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
16 `UNMOCKED EXTERNAL API REQUEST DETECTED\n` +
17 `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
18 `\n` +
19 `API: ${apiName}\n` +
20 `URL: ${url}\n` +
21 `\n` +
22 `This request would hit a real external API, which is not allowed in tests.\n` +
23 `\n` +
24 `To fix this, either:\n` +
25 ` 1. Add a fixture file for this request in test/fixtures/\n` +
26 ` 2. Add handling for this URL pattern in test/fixtures/mock-routes.cjs\n` +
27 `\n` +
28 `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`,
29 )
30 throw error
31}
32
33async function setupRouteMocking(page: Page): Promise<void> {
34 for (const routeDef of mockRoutes.routes) {
35 await page.route(routeDef.pattern, async (route: Route) => {
36 const url = route.request().url()
37 const result = mockRoutes.matchRoute(url)
38
39 if (result) {
40 await route.fulfill({
41 status: result.response.status,
42 contentType: result.response.contentType,
43 body: result.response.body,
44 })
45 } else {
46 failUnmockedRequest(route, routeDef.name)
47 }
48 })
49 }
50}
51
52/**
53 * Patterns that indicate a Vue hydration mismatch in console output.
54 *
55 * Vue always emits `console.error("Hydration completed but contains mismatches.")`
56 * in production builds when a hydration mismatch occurs.
57 *
58 * When `debug.hydration: true` is enabled (sets `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__`),
59 * Vue also emits more detailed warnings (text content mismatch, node mismatch, etc.).
60 * We catch both the summary error and the detailed warnings.
61 */
62const HYDRATION_MISMATCH_PATTERNS = [
63 'Hydration completed but contains mismatches',
64 'Hydration text content mismatch',
65 'Hydration node mismatch',
66 'Hydration children mismatch',
67 'Hydration attribute mismatch',
68 'Hydration class mismatch',
69 'Hydration style mismatch',
70]
71
72function isHydrationMismatch(message: ConsoleMessage): boolean {
73 const text = message.text()
74 return HYDRATION_MISMATCH_PATTERNS.some(pattern => text.includes(pattern))
75}
76
77/**
78 * Extended test fixture with automatic external API mocking and hydration mismatch detection.
79 *
80 * All external API requests are intercepted and served from fixtures.
81 * If a request cannot be mocked, the test will fail with a clear error.
82 *
83 * Hydration mismatches are detected via Vue's console.error output, which is always
84 * emitted in production builds when server-rendered HTML doesn't match client expectations.
85 */
86export const test = base.extend<{ mockExternalApis: void; hydrationErrors: string[] }>({
87 mockExternalApis: [
88 async ({ page }, use) => {
89 await setupRouteMocking(page)
90 await use()
91 },
92 { auto: true },
93 ],
94
95 hydrationErrors: async ({ page }, use) => {
96 const errors: string[] = []
97
98 page.on('console', message => {
99 if (isHydrationMismatch(message)) {
100 errors.push(message.text())
101 }
102 })
103
104 await use(errors)
105 },
106})
107
108export { expect }