-
-
Notifications
You must be signed in to change notification settings - Fork 425
Expand file tree
/
Copy pathtest-utils.ts
More file actions
137 lines (122 loc) · 4.77 KB
/
test-utils.ts
File metadata and controls
137 lines (122 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type { ConsoleMessage, Page, Route } from '@playwright/test'
import { test as base, expect } from '@nuxt/test-utils/playwright'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const mockRoutes = require('../fixtures/mock-routes.cjs')
/**
* Fail the test with a clear error message when an external API request isn't mocked.
*/
function failUnmockedRequest(route: Route, apiName: string): never {
const url = route.request().url()
const error = new Error(
`\n` +
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
`UNMOCKED EXTERNAL API REQUEST DETECTED\n` +
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
`\n` +
`API: ${apiName}\n` +
`URL: ${url}\n` +
`\n` +
`This request would hit a real external API, which is not allowed in tests.\n` +
`\n` +
`To fix this, either:\n` +
` 1. Add a fixture file for this request in test/fixtures/\n` +
` 2. Add handling for this URL pattern in test/fixtures/mock-routes.cjs\n` +
`\n` +
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`,
)
throw error
}
async function setupRouteMocking(page: Page): Promise<void> {
for (const routeDef of mockRoutes.routes) {
await page.route(routeDef.pattern, async (route: Route) => {
const url = route.request().url()
const result = mockRoutes.matchRoute(url)
if (result) {
await route.fulfill({
status: result.response.status,
contentType: result.response.contentType,
body: result.response.body,
})
} else {
failUnmockedRequest(route, routeDef.name)
}
})
}
}
/**
* Patterns that indicate a Vue hydration mismatch in console output.
*
* Vue always emits `console.error("Hydration completed but contains mismatches.")`
* in production builds when a hydration mismatch occurs.
*
* When `debug.hydration: true` is enabled (sets `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__`),
* Vue also emits more detailed warnings (text content mismatch, node mismatch, etc.).
* We catch both the summary error and the detailed warnings.
*/
const HYDRATION_MISMATCH_PATTERNS = [
'Hydration completed but contains mismatches',
'Hydration text content mismatch',
'Hydration node mismatch',
'Hydration children mismatch',
'Hydration attribute mismatch',
'Hydration class mismatch',
'Hydration style mismatch',
]
function isHydrationMismatch(message: ConsoleMessage): boolean {
const text = message.text()
return HYDRATION_MISMATCH_PATTERNS.some(pattern => text.includes(pattern))
}
/**
* Detect Content-Security-Policy violations logged to the console.
*
* Browsers log CSP violations as console errors with a distinctive prefix.
* Catching these in e2e tests ensures new external resources are added to the
* CSP before they land in production.
*/
function isCspViolation(message: ConsoleMessage): boolean {
if (message.type() !== 'error') return false
const text = message.text()
return text.includes('Content-Security-Policy') || text.includes('content security policy')
}
/**
* Extended test fixture with automatic external API mocking and hydration mismatch detection.
*
* All external API requests are intercepted and served from fixtures.
* If a request cannot be mocked, the test will fail with a clear error.
*
* Hydration mismatches are detected via Vue's console.error output, which is always
* emitted in production builds when server-rendered HTML doesn't match client expectations.
*/
export const test = base.extend<{
mockExternalApis: void
hydrationErrors: string[]
cspViolations: string[]
}>({
mockExternalApis: [
async ({ page }, use) => {
await setupRouteMocking(page)
await use()
},
{ auto: true },
],
hydrationErrors: async ({ page }, use) => {
const errors: string[] = []
page.on('console', message => {
if (isHydrationMismatch(message)) {
errors.push(message.text())
}
})
await use(errors)
},
cspViolations: async ({ page }, use) => {
const violations: string[] = []
page.on('console', message => {
if (isCspViolation(message)) {
violations.push(message.text())
}
})
await use(violations)
},
})
export { expect }