nuxt-spec 0.2.0-alpha.8 → 0.2.0
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 +364 -364
- package/app/app.vue +10 -10
- package/app/components/NuxtSpecApiTestComponent.vue +24 -24
- package/app/components/NuxtSpecTestComponent.vue +9 -9
- package/app/components/index.ts +2 -0
- package/app/utils/vitest-utils.ts +5 -5
- package/bin/cli.js +53 -53
- package/bin/setup.js +243 -243
- package/config/index.d.ts +17 -17
- package/config/index.mjs +79 -79
- 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 +32 -26
- package/nuxt.config.ts +19 -14
- package/package.json +12 -4
- package/utils/e2e.ts +30 -30
- package/utils/index.d.ts +70 -70
- package/utils/index.ts +12 -12
- package/utils/screenshot.ts +89 -89
package/utils/screenshot.ts
CHANGED
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { expect } from 'vitest'
|
|
4
|
-
import { decode, type DecodedPng } from 'fast-png'
|
|
5
|
-
import pixelmatch from 'pixelmatch'
|
|
6
|
-
import type { NuxtPage } from '@nuxt/test-utils'
|
|
7
|
-
|
|
8
|
-
export interface CompareScreenshotOptions {
|
|
9
|
-
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
10
|
-
fileName?: string
|
|
11
|
-
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
12
|
-
targetDir?: string
|
|
13
|
-
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
14
|
-
selector?: string
|
|
15
|
-
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
16
|
-
maxDiffPixelRatio?: number
|
|
17
|
-
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
18
|
-
maxDiffPixels?: number
|
|
19
|
-
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
20
|
-
threshold?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
24
|
-
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
25
|
-
const dir = resolve(process.cwd(), options?.targetDir ?? 'test/e2e')
|
|
26
|
-
const baselineDir = resolve(dir, '__baseline__')
|
|
27
|
-
const currentDir = resolve(dir, '__current__')
|
|
28
|
-
|
|
29
|
-
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
30
|
-
const fileName = options?.fileName ?? `${route}.png`
|
|
31
|
-
|
|
32
|
-
// capture element specified by locator or a full-page screenshot as PNG
|
|
33
|
-
const screenshot = options?.selector
|
|
34
|
-
? await page.locator(options.selector).screenshot()
|
|
35
|
-
: await page.screenshot({ fullPage: true })
|
|
36
|
-
const baselinePath = resolve(baselineDir, fileName)
|
|
37
|
-
|
|
38
|
-
// always save the current screenshot for inspection
|
|
39
|
-
mkdirSync(currentDir, { recursive: true })
|
|
40
|
-
writeFileSync(resolve(currentDir, fileName), screenshot)
|
|
41
|
-
|
|
42
|
-
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
43
|
-
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
44
|
-
if (updating || !existsSync(baselinePath)) {
|
|
45
|
-
// save new baseline screenshot
|
|
46
|
-
mkdirSync(baselineDir, { recursive: true })
|
|
47
|
-
writeFileSync(baselinePath, screenshot)
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// compare against stored baseline PNG using pixelmatch
|
|
52
|
-
const baseline = readFileSync(baselinePath)
|
|
53
|
-
const baselineImg = decode(baseline)
|
|
54
|
-
const actualImg = decode(screenshot)
|
|
55
|
-
const { width, height } = baselineImg
|
|
56
|
-
|
|
57
|
-
if (actualImg.width !== width || actualImg.height !== height) {
|
|
58
|
-
expect.fail(`Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${resolve(currentDir, fileName)}`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
62
|
-
threshold: options?.threshold ?? 0.1,
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const totalPixels = width * height
|
|
66
|
-
const maxAllowed = options?.maxDiffPixels ?? Math.ceil(totalPixels * (options?.maxDiffPixelRatio ?? 0))
|
|
67
|
-
|
|
68
|
-
if (diffCount > maxAllowed) {
|
|
69
|
-
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
70
|
-
expect.fail(`Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${resolve(currentDir, fileName)}`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return true
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
77
|
-
function toRGBA(img: DecodedPng): Uint8Array {
|
|
78
|
-
const { width, height, data, channels = 4 } = img
|
|
79
|
-
if (channels === 4) return data as Uint8Array
|
|
80
|
-
const pixels = width * height
|
|
81
|
-
const rgba = new Uint8Array(pixels * 4)
|
|
82
|
-
for (let i = 0; i < pixels; i++) {
|
|
83
|
-
rgba[i * 4] = data[i * 3]
|
|
84
|
-
rgba[i * 4 + 1] = data[i * 3 + 1]
|
|
85
|
-
rgba[i * 4 + 2] = data[i * 3 + 2]
|
|
86
|
-
rgba[i * 4 + 3] = 255
|
|
87
|
-
}
|
|
88
|
-
return rgba
|
|
89
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { expect } from 'vitest'
|
|
4
|
+
import { decode, type DecodedPng } from 'fast-png'
|
|
5
|
+
import pixelmatch from 'pixelmatch'
|
|
6
|
+
import type { NuxtPage } from '@nuxt/test-utils'
|
|
7
|
+
|
|
8
|
+
export interface CompareScreenshotOptions {
|
|
9
|
+
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
10
|
+
fileName?: string
|
|
11
|
+
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
12
|
+
targetDir?: string
|
|
13
|
+
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
14
|
+
selector?: string
|
|
15
|
+
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
16
|
+
maxDiffPixelRatio?: number
|
|
17
|
+
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
18
|
+
maxDiffPixels?: number
|
|
19
|
+
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
20
|
+
threshold?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
24
|
+
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
25
|
+
const dir = resolve(process.cwd(), options?.targetDir ?? 'test/e2e')
|
|
26
|
+
const baselineDir = resolve(dir, '__baseline__')
|
|
27
|
+
const currentDir = resolve(dir, '__current__')
|
|
28
|
+
|
|
29
|
+
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
30
|
+
const fileName = options?.fileName ?? `${route}.png`
|
|
31
|
+
|
|
32
|
+
// capture element specified by locator or a full-page screenshot as PNG
|
|
33
|
+
const screenshot = options?.selector
|
|
34
|
+
? await page.locator(options.selector).screenshot()
|
|
35
|
+
: await page.screenshot({ fullPage: true })
|
|
36
|
+
const baselinePath = resolve(baselineDir, fileName)
|
|
37
|
+
|
|
38
|
+
// always save the current screenshot for inspection
|
|
39
|
+
mkdirSync(currentDir, { recursive: true })
|
|
40
|
+
writeFileSync(resolve(currentDir, fileName), screenshot)
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
43
|
+
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
44
|
+
if (updating || !existsSync(baselinePath)) {
|
|
45
|
+
// save new baseline screenshot
|
|
46
|
+
mkdirSync(baselineDir, { recursive: true })
|
|
47
|
+
writeFileSync(baselinePath, screenshot)
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// compare against stored baseline PNG using pixelmatch
|
|
52
|
+
const baseline = readFileSync(baselinePath)
|
|
53
|
+
const baselineImg = decode(baseline)
|
|
54
|
+
const actualImg = decode(screenshot)
|
|
55
|
+
const { width, height } = baselineImg
|
|
56
|
+
|
|
57
|
+
if (actualImg.width !== width || actualImg.height !== height) {
|
|
58
|
+
expect.fail(`Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${resolve(currentDir, fileName)}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
62
|
+
threshold: options?.threshold ?? 0.1,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const totalPixels = width * height
|
|
66
|
+
const maxAllowed = options?.maxDiffPixels ?? Math.ceil(totalPixels * (options?.maxDiffPixelRatio ?? 0))
|
|
67
|
+
|
|
68
|
+
if (diffCount > maxAllowed) {
|
|
69
|
+
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
70
|
+
expect.fail(`Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${resolve(currentDir, fileName)}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
77
|
+
function toRGBA(img: DecodedPng): Uint8Array {
|
|
78
|
+
const { width, height, data, channels = 4 } = img
|
|
79
|
+
if (channels === 4) return data as Uint8Array
|
|
80
|
+
const pixels = width * height
|
|
81
|
+
const rgba = new Uint8Array(pixels * 4)
|
|
82
|
+
for (let i = 0; i < pixels; i++) {
|
|
83
|
+
rgba[i * 4] = data[i * 3]
|
|
84
|
+
rgba[i * 4 + 1] = data[i * 3 + 1]
|
|
85
|
+
rgba[i * 4 + 2] = data[i * 3 + 2]
|
|
86
|
+
rgba[i * 4 + 3] = 255
|
|
87
|
+
}
|
|
88
|
+
return rgba
|
|
89
|
+
}
|