nuxt-spec 0.3.0 → 0.3.1-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.
@@ -1,172 +1,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 } from 'vitest'
5
- import { appendToReport, ensureReportCreated, resolveWithin, screenshotSetup, toRGBA } from './screenshot/report-utils'
6
- import pixelmatch from 'pixelmatch'
7
- import type { GotoOptions, NuxtPage } from '@nuxt/test-utils'
8
- import { checkNumberParam, checkPageParam, checkStringParam } from './helpers/check-params'
9
- import { gotoPage } from './e2e'
10
-
11
- /**
12
- * Extra settings object for `compareScreenshot()` function.
13
- * All properties are optional.
14
- */
15
- export type CompareScreenshotOptions = {
16
- /** Event to be awaited before NuxtPage instance is returned (defaults to `'hydration'`) */
17
- waitUntil?: GotoOptions['waitUntil']
18
- /** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
19
- fileName?: string
20
- /** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
21
- targetDir?: string
22
- /** CSS selector for a specific element to capture (defaults to full page) */
23
- selector?: string
24
- /** Max ratio of different pixels (0–1). Default: 0 (exact match) */
25
- maxDiffPixelRatio?: number
26
- /** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
27
- maxDiffPixels?: number
28
- /** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
29
- threshold?: number
30
- }
31
-
32
- /**
33
- * Capture a browser screenshot and compare it against a stored baseline PNG.
34
- * When run with `-u` / `--update`, or when no baseline exists yet, the current
35
- * screenshot is saved as the new baseline.
36
- *
37
- * Comparison uses pixelmatch for perceptual pixel diffing. By default,
38
- * zero differing pixels are allowed (exact match). Set `maxDiffPixelRatio`
39
- * or `maxDiffPixels` to tolerate cross-platform rendering differences.
40
- *
41
- * @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
42
- * @param options - Optional extra settings (see `CompareScreenshotOptions`)
43
- * @returns `true` when the screenshot matches the baseline (or a new baseline was saved)
44
- * @throws Fails the current Vitest test when a mismatch is detected
45
- */
46
- export async function compareScreenshot(page: NuxtPage | string, options?: CompareScreenshotOptions): Promise<boolean> {
47
- const { waitUntil, fileName, targetDir, selector, maxDiffPixelRatio, maxDiffPixels, threshold } = options || {}
48
-
49
- // get NuxtPage instance
50
- const pageInstance = typeof page === 'string' ? await gotoPage(page, { waitUntil }) : page
51
- checkPageParam('compareScreenshot:pageInstance', pageInstance)
52
-
53
- // verify params
54
- if (typeof page === 'string') {
55
- checkStringParam('compareScreenshot:page', page)
56
- }
57
- if (waitUntil) {
58
- checkStringParam('compareScreenshot:options.waitUntil', waitUntil)
59
- }
60
- if (fileName) {
61
- checkStringParam('compareScreenshot:options.fileName', fileName)
62
- }
63
- if (targetDir) {
64
- checkStringParam('compareScreenshot:options.targetDir', targetDir)
65
- }
66
- if (selector) {
67
- checkStringParam('compareScreenshot:options.selector', selector)
68
- }
69
- if (maxDiffPixelRatio !== undefined) {
70
- checkNumberParam('compareScreenshot:options.maxDiffPixelRatio', maxDiffPixelRatio)
71
- }
72
- if (maxDiffPixels !== undefined) {
73
- checkNumberParam('compareScreenshot:options.maxDiffPixels', maxDiffPixels)
74
- }
75
- if (threshold !== undefined) {
76
- checkNumberParam('compareScreenshot:options.threshold', threshold)
77
- }
78
-
79
- // ensure the target directory stays within the project root
80
- const root = process.cwd()
81
- const dir = resolveWithin(root, targetDir ?? 'test/e2e')
82
- mkdirSync(dir, { recursive: true })
83
-
84
- // ensure baseline/current directories exist
85
- const baselineDir = resolve(dir, '__baseline__')
86
- mkdirSync(baselineDir, { recursive: true })
87
- const currentDir = resolve(dir, '__current__')
88
- mkdirSync(currentDir, { recursive: true })
89
-
90
- // create report file on first call
91
- ensureReportCreated(dir)
92
-
93
- // compute screenshot file name
94
- const route = pageInstance.url().substring(pageInstance.url().lastIndexOf('/') + 1) || 'index'
95
- const screenshotFile = fileName ?? `${route}.png`
96
-
97
- // warning on custom non-png file extensions
98
- if (!screenshotFile.toLowerCase().endsWith('.png')) {
99
- console.warn(`Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${screenshotFile}'.`)
100
- }
101
-
102
- // ensure the file name cannot escape its target directory
103
- const baselinePath = resolveWithin(baselineDir, screenshotFile)
104
- const currentPath = resolveWithin(currentDir, screenshotFile)
105
-
106
- // capture element specified by locator or a full-page screenshot as PNG
107
- const screenshot = selector
108
- ? await pageInstance.locator(selector).screenshot()
109
- : await pageInstance.screenshot({ fullPage: true })
110
-
111
- // always save the current screenshot for inspection
112
- writeFileSync(currentPath, screenshot)
113
-
114
- // save baseline if not exist yet
115
- if (!existsSync(baselinePath)) {
116
- writeFileSync(baselinePath, screenshot)
117
- return true
118
- }
119
-
120
- // @ts-expect-error - this is reliable way of reading Vitest "update" flag
121
- const updateFlag = expect.getState().snapshotState?._updateSnapshot === 'all'
122
-
123
- // compare against stored baseline PNG using pixelmatch
124
- const baseline = readFileSync(baselinePath)
125
- const baselineImg = decode(baseline)
126
- const actualImg = decode(screenshot)
127
- const { width, height } = baselineImg
128
-
129
- if (actualImg.width !== width || actualImg.height !== height) {
130
- // overwrite baseline if Vitest update flag is set
131
- if (updateFlag) {
132
- writeFileSync(baselinePath, screenshot)
133
- return true
134
- }
135
- // otherwise report failure
136
- const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
137
- appendToReport(screenshotFile, message, baseline, screenshot)
138
- expect.fail(message)
139
- }
140
-
141
- const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
142
- threshold: threshold ?? 0.1,
143
- })
144
-
145
- const totalPixels = width * height
146
- const maxAllowed = maxDiffPixels ?? Math.ceil(totalPixels * (maxDiffPixelRatio ?? 0))
147
-
148
- if (diffCount > maxAllowed) {
149
- // overwrite baseline if Vitest update flag is set
150
- if (updateFlag) {
151
- writeFileSync(baselinePath, screenshot)
152
- return true
153
- }
154
- // otherwise report failure
155
- const ratio = (diffCount / totalPixels * 100).toFixed(2)
156
- const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
157
- appendToReport(screenshotFile, message, baseline, screenshot)
158
- expect.fail(message)
159
- }
160
-
161
- return true
162
- }
163
-
164
- // Vitest globalSetup entry point
165
- // - computes stable timestamp values and exposes them via env variables
166
- // - the report file itself is created lazily on first compareScreenshot call
167
- // - provides a callback to close the HTML report once tests are finished (if it was created)
168
- export default function setup() {
169
- // the function itself is defined in the helper file
170
- // to avoid imports here and there
171
- return screenshotSetup()
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 } from 'vitest'
5
+ import { appendToReport, ensureReportCreated, resolveWithin, screenshotSetup, toRGBA } from './screenshot/report-utils'
6
+ import pixelmatch from 'pixelmatch'
7
+ import type { GotoOptions, NuxtPage } from '@nuxt/test-utils'
8
+ import { checkNumberParam, checkPageParam, checkStringParam } from './helpers/check-params'
9
+ import { gotoPage } from './e2e'
10
+
11
+ /**
12
+ * Extra settings object for `compareScreenshot()` function.
13
+ * All properties are optional.
14
+ */
15
+ export type CompareScreenshotOptions = {
16
+ /** Event to be awaited before NuxtPage instance is returned (defaults to `'hydration'`) */
17
+ waitUntil?: GotoOptions['waitUntil']
18
+ /** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
19
+ fileName?: string
20
+ /** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
21
+ targetDir?: string
22
+ /** CSS selector for a specific element to capture (defaults to full page) */
23
+ selector?: string
24
+ /** Max ratio of different pixels (0–1). Default: 0 (exact match) */
25
+ maxDiffPixelRatio?: number
26
+ /** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
27
+ maxDiffPixels?: number
28
+ /** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
29
+ threshold?: number
30
+ }
31
+
32
+ /**
33
+ * Capture a browser screenshot and compare it against a stored baseline PNG.
34
+ * When run with `-u` / `--update`, or when no baseline exists yet, the current
35
+ * screenshot is saved as the new baseline.
36
+ *
37
+ * Comparison uses pixelmatch for perceptual pixel diffing. By default,
38
+ * zero differing pixels are allowed (exact match). Set `maxDiffPixelRatio`
39
+ * or `maxDiffPixels` to tolerate cross-platform rendering differences.
40
+ *
41
+ * @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
42
+ * @param options - Optional extra settings (see `CompareScreenshotOptions`)
43
+ * @returns `true` when the screenshot matches the baseline (or a new baseline was saved)
44
+ * @throws Fails the current Vitest test when a mismatch is detected
45
+ */
46
+ export async function compareScreenshot(page: NuxtPage | string, options?: CompareScreenshotOptions): Promise<boolean> {
47
+ const { waitUntil, fileName, targetDir, selector, maxDiffPixelRatio, maxDiffPixels, threshold } = options || {}
48
+
49
+ // get NuxtPage instance
50
+ const pageInstance = typeof page === 'string' ? await gotoPage(page, { waitUntil }) : page
51
+ checkPageParam('compareScreenshot:pageInstance', pageInstance)
52
+
53
+ // verify params
54
+ if (typeof page === 'string') {
55
+ checkStringParam('compareScreenshot:page', page)
56
+ }
57
+ if (waitUntil) {
58
+ checkStringParam('compareScreenshot:options.waitUntil', waitUntil)
59
+ }
60
+ if (fileName) {
61
+ checkStringParam('compareScreenshot:options.fileName', fileName)
62
+ }
63
+ if (targetDir) {
64
+ checkStringParam('compareScreenshot:options.targetDir', targetDir)
65
+ }
66
+ if (selector) {
67
+ checkStringParam('compareScreenshot:options.selector', selector)
68
+ }
69
+ if (maxDiffPixelRatio !== undefined) {
70
+ checkNumberParam('compareScreenshot:options.maxDiffPixelRatio', maxDiffPixelRatio)
71
+ }
72
+ if (maxDiffPixels !== undefined) {
73
+ checkNumberParam('compareScreenshot:options.maxDiffPixels', maxDiffPixels)
74
+ }
75
+ if (threshold !== undefined) {
76
+ checkNumberParam('compareScreenshot:options.threshold', threshold)
77
+ }
78
+
79
+ // ensure the target directory stays within the project root
80
+ const root = process.cwd()
81
+ const dir = resolveWithin(root, targetDir ?? 'test/e2e')
82
+ mkdirSync(dir, { recursive: true })
83
+
84
+ // ensure baseline/current directories exist
85
+ const baselineDir = resolve(dir, '__baseline__')
86
+ mkdirSync(baselineDir, { recursive: true })
87
+ const currentDir = resolve(dir, '__current__')
88
+ mkdirSync(currentDir, { recursive: true })
89
+
90
+ // create report file on first call
91
+ ensureReportCreated(dir)
92
+
93
+ // compute screenshot file name
94
+ const route = pageInstance.url().substring(pageInstance.url().lastIndexOf('/') + 1) || 'index'
95
+ const screenshotFile = fileName ?? `${route}.png`
96
+
97
+ // warning on custom non-png file extensions
98
+ if (!screenshotFile.toLowerCase().endsWith('.png')) {
99
+ console.warn(`Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${screenshotFile}'.`)
100
+ }
101
+
102
+ // ensure the file name cannot escape its target directory
103
+ const baselinePath = resolveWithin(baselineDir, screenshotFile)
104
+ const currentPath = resolveWithin(currentDir, screenshotFile)
105
+
106
+ // capture element specified by locator or a full-page screenshot as PNG
107
+ const screenshot = selector
108
+ ? await pageInstance.locator(selector).screenshot()
109
+ : await pageInstance.screenshot({ fullPage: true })
110
+
111
+ // always save the current screenshot for inspection
112
+ writeFileSync(currentPath, screenshot)
113
+
114
+ // save baseline if not exist yet
115
+ if (!existsSync(baselinePath)) {
116
+ writeFileSync(baselinePath, screenshot)
117
+ return true
118
+ }
119
+
120
+ // @ts-expect-error - this is reliable way of reading Vitest "update" flag
121
+ const updateFlag = expect.getState().snapshotState?._updateSnapshot === 'all'
122
+
123
+ // compare against stored baseline PNG using pixelmatch
124
+ const baseline = readFileSync(baselinePath)
125
+ const baselineImg = decode(baseline)
126
+ const actualImg = decode(screenshot)
127
+ const { width, height } = baselineImg
128
+
129
+ if (actualImg.width !== width || actualImg.height !== height) {
130
+ // overwrite baseline if Vitest update flag is set
131
+ if (updateFlag) {
132
+ writeFileSync(baselinePath, screenshot)
133
+ return true
134
+ }
135
+ // otherwise report failure
136
+ const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
137
+ appendToReport(screenshotFile, message, baseline, screenshot)
138
+ expect.fail(message)
139
+ }
140
+
141
+ const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
142
+ threshold: threshold ?? 0.1,
143
+ })
144
+
145
+ const totalPixels = width * height
146
+ const maxAllowed = maxDiffPixels ?? Math.ceil(totalPixels * (maxDiffPixelRatio ?? 0))
147
+
148
+ if (diffCount > maxAllowed) {
149
+ // overwrite baseline if Vitest update flag is set
150
+ if (updateFlag) {
151
+ writeFileSync(baselinePath, screenshot)
152
+ return true
153
+ }
154
+ // otherwise report failure
155
+ const ratio = (diffCount / totalPixels * 100).toFixed(2)
156
+ const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
157
+ appendToReport(screenshotFile, message, baseline, screenshot)
158
+ expect.fail(message)
159
+ }
160
+
161
+ return true
162
+ }
163
+
164
+ // Vitest globalSetup entry point
165
+ // - computes stable timestamp values and exposes them via env variables
166
+ // - the report file itself is created lazily on first compareScreenshot call
167
+ // - provides a callback to close the HTML report once tests are finished (if it was created)
168
+ export default function setup() {
169
+ // the function itself is defined in the helper file
170
+ // to avoid imports here and there
171
+ return screenshotSetup()
172
+ }