nuxt-spec 0.3.4 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +13 -13
- package/app/app.vue +10 -10
- package/app/components/NuxtSpecApiTestComponent.vue +24 -24
- package/app/components/NuxtSpecTestComponent.vue +9 -9
- package/app/components/index.d.ts +7 -7
- package/app/components/index.ts +2 -2
- package/app/utils/vitest-utils.ts +5 -5
- package/bin/_internal/prepublish-check.js +16 -16
- package/bin/cli.js +53 -44
- package/bin/commands/ci.js +124 -0
- package/bin/commands/setup.js +367 -0
- package/bin/commands/update.js +126 -0
- package/bin/helpers/commands.js +82 -82
- package/bin/helpers/constants.js +8 -0
- package/bin/helpers/git.js +73 -0
- package/config/index.d.ts +36 -33
- package/config/index.mjs +147 -132
- package/config/report/report-entry.html +6 -0
- package/config/report/report-head.html +34 -0
- package/{utils/screenshot/report-entry.html → config/report/report-screenshot.html} +8 -8
- package/config/report/report-tail.html +4 -0
- package/config/templates/.github/workflows/ci-test-update.yml.template +105 -0
- package/config/templates/.github/workflows/ci-test-verify.yml.template +68 -0
- package/config/templates/.gitlab/ci/ci-test-update.yml.template +69 -0
- package/config/templates/.gitlab/ci/ci-test-verify.yml.template +41 -0
- package/config/templates/.gitlab-ci.yml.template +6 -0
- package/config/templates/pnpm-workspace.yaml.template +19 -19
- package/config/templates/vitest.config.ts.template +5 -5
- package/config/utils/merge.mjs +43 -43
- package/config/utils/options.d.mts +59 -0
- package/config/utils/options.mjs +116 -0
- package/config/utils/reporter.mjs +409 -0
- package/config/utils/warnings.mjs +38 -33
- package/modules/spec-options.ts +23 -0
- package/nuxt.config.ts +20 -24
- package/package.json +37 -31
- package/utils/e2e.ts +131 -131
- package/utils/helpers/check-params.ts +44 -41
- package/utils/helpers/location-hint.ts +30 -30
- package/utils/helpers/screenshot-utils.ts +27 -0
- package/utils/index.ts +12 -12
- package/utils/playwright.ts +28 -20
- package/utils/screenshot.ts +201 -172
- package/bin/setup.js +0 -287
- package/bin/update.js +0 -94
- package/utils/screenshot/report-head.html +0 -24
- package/utils/screenshot/report-tail.html +0 -4
- package/utils/screenshot/report-utils.ts +0 -236
package/utils/screenshot.ts
CHANGED
|
@@ -1,172 +1,201 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { decode } from 'fast-png'
|
|
4
|
-
import { expect } from 'vitest'
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/** Name of the PNG file
|
|
19
|
-
fileName
|
|
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
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { decode } from 'fast-png'
|
|
4
|
+
import { expect, recordArtifact, TestRunner } from 'vitest'
|
|
5
|
+
import type { TestArtifactBase, TestAttachment } from 'vitest'
|
|
6
|
+
import { resolveWithin, toRGBA } from './helpers/screenshot-utils'
|
|
7
|
+
import pixelmatch from 'pixelmatch'
|
|
8
|
+
import type { GotoOptions, NuxtPage } from '@nuxt/test-utils'
|
|
9
|
+
import { checkNumberParam, checkPageParam, checkStringParam } from './helpers/check-params'
|
|
10
|
+
import { gotoPage } from './e2e'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Test artifact recorded by `compareScreenshot()` on mismatch.
|
|
14
|
+
* It is consumed by the Nuxt Spec HTML reporter.
|
|
15
|
+
*/
|
|
16
|
+
export interface NuxtSpecScreenshotArtifact extends TestArtifactBase {
|
|
17
|
+
type: 'nuxt-spec:screenshot'
|
|
18
|
+
/** Name of the compared PNG file */
|
|
19
|
+
fileName: string
|
|
20
|
+
/** Description of the mismatch */
|
|
21
|
+
message: string
|
|
22
|
+
attachments: Array<TestAttachment & { name: 'baseline' | 'actual' }>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module 'vitest' {
|
|
26
|
+
interface TestArtifactRegistry {
|
|
27
|
+
'nuxt-spec:screenshot': NuxtSpecScreenshotArtifact
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extra settings object for `compareScreenshot()` function.
|
|
33
|
+
* All properties are optional.
|
|
34
|
+
*/
|
|
35
|
+
export type CompareScreenshotOptions = {
|
|
36
|
+
/** Event to be awaited before NuxtPage instance is returned (defaults to `'hydration'`) */
|
|
37
|
+
waitUntil?: GotoOptions['waitUntil']
|
|
38
|
+
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
39
|
+
fileName?: string
|
|
40
|
+
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
41
|
+
targetDir?: string
|
|
42
|
+
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
43
|
+
selector?: string
|
|
44
|
+
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
45
|
+
maxDiffPixelRatio?: number
|
|
46
|
+
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
47
|
+
maxDiffPixels?: number
|
|
48
|
+
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
49
|
+
threshold?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Capture a browser screenshot and compare it against a stored baseline PNG.
|
|
54
|
+
* When run with `-u` / `--update`, or when no baseline exists yet, the current
|
|
55
|
+
* screenshot is saved as the new baseline.
|
|
56
|
+
*
|
|
57
|
+
* Comparison uses pixelmatch for perceptual pixel diffing. By default,
|
|
58
|
+
* zero differing pixels are allowed (exact match). Set `maxDiffPixelRatio`
|
|
59
|
+
* or `maxDiffPixels` to tolerate cross-platform rendering differences.
|
|
60
|
+
*
|
|
61
|
+
* @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
|
|
62
|
+
* @param options - Optional extra settings (see `CompareScreenshotOptions`)
|
|
63
|
+
* @returns `true` when the screenshot matches the baseline (or a new baseline was saved)
|
|
64
|
+
* @throws Fails the current Vitest test when a mismatch is detected
|
|
65
|
+
*/
|
|
66
|
+
export async function compareScreenshot(page: NuxtPage | string, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
67
|
+
const { waitUntil, fileName, targetDir, selector, maxDiffPixelRatio, maxDiffPixels, threshold } = options || {}
|
|
68
|
+
|
|
69
|
+
// get NuxtPage instance
|
|
70
|
+
const pageInstance = typeof page === 'string' ? await gotoPage(page, { waitUntil }) : page
|
|
71
|
+
checkPageParam('compareScreenshot:pageInstance', pageInstance)
|
|
72
|
+
|
|
73
|
+
// verify params
|
|
74
|
+
if (typeof page === 'string') {
|
|
75
|
+
checkStringParam('compareScreenshot:page', page)
|
|
76
|
+
}
|
|
77
|
+
if (waitUntil) {
|
|
78
|
+
checkStringParam('compareScreenshot:options.waitUntil', waitUntil)
|
|
79
|
+
}
|
|
80
|
+
if (fileName) {
|
|
81
|
+
checkStringParam('compareScreenshot:options.fileName', fileName)
|
|
82
|
+
}
|
|
83
|
+
if (targetDir) {
|
|
84
|
+
checkStringParam('compareScreenshot:options.targetDir', targetDir)
|
|
85
|
+
}
|
|
86
|
+
if (selector) {
|
|
87
|
+
checkStringParam('compareScreenshot:options.selector', selector)
|
|
88
|
+
}
|
|
89
|
+
if (maxDiffPixelRatio !== undefined) {
|
|
90
|
+
checkNumberParam('compareScreenshot:options.maxDiffPixelRatio', maxDiffPixelRatio)
|
|
91
|
+
}
|
|
92
|
+
if (maxDiffPixels !== undefined) {
|
|
93
|
+
checkNumberParam('compareScreenshot:options.maxDiffPixels', maxDiffPixels)
|
|
94
|
+
}
|
|
95
|
+
if (threshold !== undefined) {
|
|
96
|
+
checkNumberParam('compareScreenshot:options.threshold', threshold)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ensure the target directory stays within the project root
|
|
100
|
+
const root = process.cwd()
|
|
101
|
+
const dir = resolveWithin(root, targetDir ?? 'test/e2e')
|
|
102
|
+
mkdirSync(dir, { recursive: true })
|
|
103
|
+
|
|
104
|
+
// ensure baseline/current directories exist
|
|
105
|
+
const baselineDir = resolve(dir, '__baseline__')
|
|
106
|
+
mkdirSync(baselineDir, { recursive: true })
|
|
107
|
+
const currentDir = resolve(dir, '__current__')
|
|
108
|
+
mkdirSync(currentDir, { recursive: true })
|
|
109
|
+
|
|
110
|
+
// compute screenshot file name
|
|
111
|
+
const route = pageInstance.url().substring(pageInstance.url().lastIndexOf('/') + 1) || 'index'
|
|
112
|
+
const screenshotFile = fileName ?? `${route}.png`
|
|
113
|
+
|
|
114
|
+
// warning on custom non-png file extensions
|
|
115
|
+
if (!screenshotFile.toLowerCase().endsWith('.png')) {
|
|
116
|
+
console.warn(`[Nuxt Spec] Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${screenshotFile}'.`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ensure the file name cannot escape its target directory
|
|
120
|
+
const baselinePath = resolveWithin(baselineDir, screenshotFile)
|
|
121
|
+
const currentPath = resolveWithin(currentDir, screenshotFile)
|
|
122
|
+
|
|
123
|
+
// capture element specified by locator or a full-page screenshot as PNG
|
|
124
|
+
const screenshot = selector
|
|
125
|
+
? await pageInstance.locator(selector).screenshot()
|
|
126
|
+
: await pageInstance.screenshot({ fullPage: true })
|
|
127
|
+
|
|
128
|
+
// always save the current screenshot for inspection
|
|
129
|
+
writeFileSync(currentPath, screenshot)
|
|
130
|
+
|
|
131
|
+
// save baseline if not exist yet
|
|
132
|
+
if (!existsSync(baselinePath)) {
|
|
133
|
+
writeFileSync(baselinePath, screenshot)
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
138
|
+
const updateFlag = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
139
|
+
|
|
140
|
+
// compare against stored baseline PNG using pixelmatch
|
|
141
|
+
const baseline = readFileSync(baselinePath)
|
|
142
|
+
const baselineImg = decode(baseline)
|
|
143
|
+
const actualImg = decode(screenshot)
|
|
144
|
+
const { width, height } = baselineImg
|
|
145
|
+
|
|
146
|
+
if (actualImg.width !== width || actualImg.height !== height) {
|
|
147
|
+
// overwrite baseline if Vitest update flag is set
|
|
148
|
+
if (updateFlag) {
|
|
149
|
+
writeFileSync(baselinePath, screenshot)
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
// otherwise report failure
|
|
153
|
+
const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
|
|
154
|
+
await reportMismatch(screenshotFile, message, baseline, screenshot)
|
|
155
|
+
expect.fail(message)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
159
|
+
threshold: threshold ?? 0.1,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const totalPixels = width * height
|
|
163
|
+
const maxAllowed = maxDiffPixels ?? Math.ceil(totalPixels * (maxDiffPixelRatio ?? 0))
|
|
164
|
+
|
|
165
|
+
if (diffCount > maxAllowed) {
|
|
166
|
+
// overwrite baseline if Vitest update flag is set
|
|
167
|
+
if (updateFlag) {
|
|
168
|
+
writeFileSync(baselinePath, screenshot)
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
// otherwise report failure
|
|
172
|
+
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
173
|
+
const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
|
|
174
|
+
await reportMismatch(screenshotFile, message, baseline, screenshot)
|
|
175
|
+
expect.fail(message)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// pass the baseline/actual pair to the Nuxt Spec HTML reporter via Vitest test artifacts
|
|
182
|
+
// best-effort only - it must never mask the actual assertion failure
|
|
183
|
+
async function reportMismatch(fileName: string, message: string, baseline: Uint8Array, actual: Uint8Array): Promise<void> {
|
|
184
|
+
// undefined when called outside of a running test (e.g. in `beforeAll`)
|
|
185
|
+
const test = TestRunner.getCurrentTest()
|
|
186
|
+
if (!test) return
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await recordArtifact(test, {
|
|
190
|
+
type: 'nuxt-spec:screenshot',
|
|
191
|
+
fileName,
|
|
192
|
+
message,
|
|
193
|
+
attachments: [
|
|
194
|
+
{ name: 'baseline', contentType: 'image/png', body: baseline },
|
|
195
|
+
{ name: 'actual', contentType: 'image/png', body: actual },
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
} catch {
|
|
199
|
+
// reporting is optional
|
|
200
|
+
}
|
|
201
|
+
}
|