nuxt-spec 0.2.4-alpha.3 → 0.2.4
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 +417 -413
- 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/cli.js +53 -53
- package/bin/prepublish-check.js +16 -16
- package/bin/setup.js +251 -251
- package/config/index.d.ts +17 -17
- package/config/index.mjs +95 -85
- package/config/templates/pnpm-workspace.yaml.template +4 -4
- package/config/templates/vitest.config.ts.template +5 -5
- package/config/utils/merge.mjs +43 -43
- package/config/utils/warnings.mjs +33 -33
- package/nuxt.config.ts +19 -19
- package/package.json +15 -17
- package/utils/e2e.ts +30 -30
- package/utils/index.d.ts +70 -70
- package/utils/index.ts +12 -12
- package/utils/screenshot/report-entry.html +8 -8
- package/utils/screenshot/report-head.html +24 -24
- package/utils/screenshot/report-tail.html +4 -4
- package/utils/screenshot/report-utils.ts +227 -211
- package/utils/screenshot.ts +104 -104
package/utils/screenshot.ts
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
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 { NuxtPage } from '@nuxt/test-utils'
|
|
8
|
-
|
|
9
|
-
export interface CompareScreenshotOptions {
|
|
10
|
-
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
11
|
-
fileName?: string
|
|
12
|
-
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
13
|
-
targetDir?: string
|
|
14
|
-
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
15
|
-
selector?: string
|
|
16
|
-
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
17
|
-
maxDiffPixelRatio?: number
|
|
18
|
-
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
19
|
-
maxDiffPixels?: number
|
|
20
|
-
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
21
|
-
threshold?: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
25
|
-
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
26
|
-
const root = process.cwd()
|
|
27
|
-
|
|
28
|
-
// ensure the target directory stays within the project root
|
|
29
|
-
const dir = resolveWithin(root, options?.targetDir ?? 'test/e2e')
|
|
30
|
-
|
|
31
|
-
// create report file on first call
|
|
32
|
-
ensureReportCreated(dir)
|
|
33
|
-
|
|
34
|
-
const baselineDir = resolve(dir, '__baseline__')
|
|
35
|
-
const currentDir = resolve(dir, '__current__')
|
|
36
|
-
|
|
37
|
-
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
38
|
-
const fileName = options?.fileName ?? `${route}.png`
|
|
39
|
-
|
|
40
|
-
// warning on custom non-png file extensions
|
|
41
|
-
if (!fileName.toLowerCase().endsWith('.png')) {
|
|
42
|
-
console.warn(`Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${fileName}'.`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ensure the file name cannot escape its target directory
|
|
46
|
-
const baselinePath = resolveWithin(baselineDir, fileName)
|
|
47
|
-
const currentPath = resolveWithin(currentDir, fileName)
|
|
48
|
-
|
|
49
|
-
// capture element specified by locator or a full-page screenshot as PNG
|
|
50
|
-
const screenshot = options?.selector
|
|
51
|
-
? await page.locator(options.selector).screenshot()
|
|
52
|
-
: await page.screenshot({ fullPage: true })
|
|
53
|
-
|
|
54
|
-
// always save the current screenshot for inspection
|
|
55
|
-
mkdirSync(currentDir, { recursive: true })
|
|
56
|
-
writeFileSync(currentPath, screenshot)
|
|
57
|
-
|
|
58
|
-
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
59
|
-
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
60
|
-
if (updating || !existsSync(baselinePath)) {
|
|
61
|
-
// save new baseline screenshot
|
|
62
|
-
mkdirSync(baselineDir, { recursive: true })
|
|
63
|
-
writeFileSync(baselinePath, screenshot)
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// compare against stored baseline PNG using pixelmatch
|
|
68
|
-
const baseline = readFileSync(baselinePath)
|
|
69
|
-
const baselineImg = decode(baseline)
|
|
70
|
-
const actualImg = decode(screenshot)
|
|
71
|
-
const { width, height } = baselineImg
|
|
72
|
-
|
|
73
|
-
if (actualImg.width !== width || actualImg.height !== height) {
|
|
74
|
-
const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
|
|
75
|
-
appendToReport(fileName, message, baseline, screenshot)
|
|
76
|
-
expect.fail(message)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
80
|
-
threshold: options?.threshold ?? 0.1,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const totalPixels = width * height
|
|
84
|
-
const maxAllowed = options?.maxDiffPixels ?? Math.ceil(totalPixels * (options?.maxDiffPixelRatio ?? 0))
|
|
85
|
-
|
|
86
|
-
if (diffCount > maxAllowed) {
|
|
87
|
-
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
88
|
-
const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
|
|
89
|
-
appendToReport(fileName, message, baseline, screenshot)
|
|
90
|
-
expect.fail(message)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Vitest globalSetup entry point
|
|
97
|
-
// - computes stable timestamp values and exposes them via env variables
|
|
98
|
-
// - the report file itself is created lazily on first compareScreenshot call
|
|
99
|
-
// - provides a callback to close the HTML report once tests are finished (if it was created)
|
|
100
|
-
export default function setup() {
|
|
101
|
-
// the function itself is defined in the helper file
|
|
102
|
-
// to avoid imports here and there
|
|
103
|
-
return screenshotSetup()
|
|
104
|
-
}
|
|
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 { NuxtPage } from '@nuxt/test-utils'
|
|
8
|
+
|
|
9
|
+
export interface CompareScreenshotOptions {
|
|
10
|
+
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
11
|
+
fileName?: string
|
|
12
|
+
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
13
|
+
targetDir?: string
|
|
14
|
+
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
15
|
+
selector?: string
|
|
16
|
+
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
17
|
+
maxDiffPixelRatio?: number
|
|
18
|
+
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
19
|
+
maxDiffPixels?: number
|
|
20
|
+
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
21
|
+
threshold?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
25
|
+
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
26
|
+
const root = process.cwd()
|
|
27
|
+
|
|
28
|
+
// ensure the target directory stays within the project root
|
|
29
|
+
const dir = resolveWithin(root, options?.targetDir ?? 'test/e2e')
|
|
30
|
+
|
|
31
|
+
// create report file on first call
|
|
32
|
+
ensureReportCreated(dir)
|
|
33
|
+
|
|
34
|
+
const baselineDir = resolve(dir, '__baseline__')
|
|
35
|
+
const currentDir = resolve(dir, '__current__')
|
|
36
|
+
|
|
37
|
+
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
38
|
+
const fileName = options?.fileName ?? `${route}.png`
|
|
39
|
+
|
|
40
|
+
// warning on custom non-png file extensions
|
|
41
|
+
if (!fileName.toLowerCase().endsWith('.png')) {
|
|
42
|
+
console.warn(`Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${fileName}'.`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ensure the file name cannot escape its target directory
|
|
46
|
+
const baselinePath = resolveWithin(baselineDir, fileName)
|
|
47
|
+
const currentPath = resolveWithin(currentDir, fileName)
|
|
48
|
+
|
|
49
|
+
// capture element specified by locator or a full-page screenshot as PNG
|
|
50
|
+
const screenshot = options?.selector
|
|
51
|
+
? await page.locator(options.selector).screenshot()
|
|
52
|
+
: await page.screenshot({ fullPage: true })
|
|
53
|
+
|
|
54
|
+
// always save the current screenshot for inspection
|
|
55
|
+
mkdirSync(currentDir, { recursive: true })
|
|
56
|
+
writeFileSync(currentPath, screenshot)
|
|
57
|
+
|
|
58
|
+
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
59
|
+
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
60
|
+
if (updating || !existsSync(baselinePath)) {
|
|
61
|
+
// save new baseline screenshot
|
|
62
|
+
mkdirSync(baselineDir, { recursive: true })
|
|
63
|
+
writeFileSync(baselinePath, screenshot)
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// compare against stored baseline PNG using pixelmatch
|
|
68
|
+
const baseline = readFileSync(baselinePath)
|
|
69
|
+
const baselineImg = decode(baseline)
|
|
70
|
+
const actualImg = decode(screenshot)
|
|
71
|
+
const { width, height } = baselineImg
|
|
72
|
+
|
|
73
|
+
if (actualImg.width !== width || actualImg.height !== height) {
|
|
74
|
+
const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
|
|
75
|
+
appendToReport(fileName, message, baseline, screenshot)
|
|
76
|
+
expect.fail(message)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
80
|
+
threshold: options?.threshold ?? 0.1,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const totalPixels = width * height
|
|
84
|
+
const maxAllowed = options?.maxDiffPixels ?? Math.ceil(totalPixels * (options?.maxDiffPixelRatio ?? 0))
|
|
85
|
+
|
|
86
|
+
if (diffCount > maxAllowed) {
|
|
87
|
+
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
88
|
+
const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
|
|
89
|
+
appendToReport(fileName, message, baseline, screenshot)
|
|
90
|
+
expect.fail(message)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Vitest globalSetup entry point
|
|
97
|
+
// - computes stable timestamp values and exposes them via env variables
|
|
98
|
+
// - the report file itself is created lazily on first compareScreenshot call
|
|
99
|
+
// - provides a callback to close the HTML report once tests are finished (if it was created)
|
|
100
|
+
export default function setup() {
|
|
101
|
+
// the function itself is defined in the helper file
|
|
102
|
+
// to avoid imports here and there
|
|
103
|
+
return screenshotSetup()
|
|
104
|
+
}
|