nuxt-spec 0.4.0-alpha.2 → 0.4.0-alpha.3

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.
Files changed (44) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -13
  3. package/app/app.vue +10 -10
  4. package/app/components/NuxtSpecApiTestComponent.vue +24 -24
  5. package/app/components/NuxtSpecTestComponent.vue +9 -9
  6. package/app/components/index.d.ts +7 -7
  7. package/app/components/index.ts +2 -2
  8. package/app/utils/vitest-utils.ts +5 -5
  9. package/config/index.d.ts +36 -36
  10. package/config/index.mjs +147 -147
  11. package/config/report/report-entry.html +6 -6
  12. package/config/report/report-head.html +34 -34
  13. package/config/report/report-screenshot.html +8 -8
  14. package/config/report/report-tail.html +4 -4
  15. package/config/templates/.github/workflows/ci-test-update.yml.template +105 -105
  16. package/config/templates/.github/workflows/ci-test-verify.yml.template +68 -68
  17. package/config/templates/.gitlab/ci/ci-test-update.yml.template +69 -69
  18. package/config/templates/.gitlab/ci/ci-test-verify.yml.template +41 -41
  19. package/config/templates/.gitlab-ci.yml.template +6 -6
  20. package/config/templates/pnpm-workspace.yaml.template +19 -19
  21. package/config/templates/vitest.config.ts.template +5 -5
  22. package/config/utils/merge.mjs +43 -43
  23. package/config/utils/options.d.mts +59 -59
  24. package/config/utils/options.mjs +116 -116
  25. package/config/utils/reporter.mjs +409 -409
  26. package/config/utils/warnings.mjs +38 -38
  27. package/modules/spec-options.ts +23 -23
  28. package/nuxt.config.ts +20 -20
  29. package/package.json +17 -23
  30. package/utils/e2e.ts +131 -131
  31. package/utils/helpers/check-params.ts +44 -44
  32. package/utils/helpers/location-hint.ts +30 -30
  33. package/utils/helpers/screenshot-utils.ts +27 -27
  34. package/utils/index.ts +12 -12
  35. package/utils/playwright.ts +28 -28
  36. package/utils/screenshot.ts +201 -201
  37. package/bin/_internal/prepublish-check.js +0 -16
  38. package/bin/cli.js +0 -53
  39. package/bin/commands/ci.js +0 -124
  40. package/bin/commands/setup.js +0 -367
  41. package/bin/commands/update.js +0 -126
  42. package/bin/helpers/commands.js +0 -82
  43. package/bin/helpers/constants.js +0 -8
  44. package/bin/helpers/git.js +0 -73
@@ -1,27 +1,27 @@
1
- import { resolve, sep } from 'node:path'
2
- import type { DecodedPng } from 'fast-png'
3
-
4
- // helper to keep user-provided targetDir inside the current project root
5
- export function resolveWithin(base: string, segment: string): string {
6
- const target = resolve(base, segment)
7
- if (target !== base && !target.startsWith(base + sep)) {
8
- throw new Error(`[Nuxt Spec] Invalid path: "${segment}" resolves outside of "${base}"`)
9
- }
10
- return target
11
- }
12
-
13
- // helper for bridging difference between Vitest PNG saving and fast-png encoding
14
- export function toRGBA(img: DecodedPng): Uint8Array {
15
- const { width, height, data, channels = 4 } = img
16
- if (channels === 4) return data as Uint8Array
17
- const pixels = width * height
18
- const rgba = new Uint8Array(pixels * 4)
19
- for (let i = 0; i < pixels; i++) {
20
- const src = i * 3
21
- rgba[i * 4 + 0] = data[src + 0] ?? 0
22
- rgba[i * 4 + 1] = data[src + 1] ?? 0
23
- rgba[i * 4 + 2] = data[src + 2] ?? 0
24
- rgba[i * 4 + 3] = 255
25
- }
26
- return rgba
27
- }
1
+ import { resolve, sep } from 'node:path'
2
+ import type { DecodedPng } from 'fast-png'
3
+
4
+ // helper to keep user-provided targetDir inside the current project root
5
+ export function resolveWithin(base: string, segment: string): string {
6
+ const target = resolve(base, segment)
7
+ if (target !== base && !target.startsWith(base + sep)) {
8
+ throw new Error(`[Nuxt Spec] Invalid path: "${segment}" resolves outside of "${base}"`)
9
+ }
10
+ return target
11
+ }
12
+
13
+ // helper for bridging difference between Vitest PNG saving and fast-png encoding
14
+ export function toRGBA(img: DecodedPng): Uint8Array {
15
+ const { width, height, data, channels = 4 } = img
16
+ if (channels === 4) return data as Uint8Array
17
+ const pixels = width * height
18
+ const rgba = new Uint8Array(pixels * 4)
19
+ for (let i = 0; i < pixels; i++) {
20
+ const src = i * 3
21
+ rgba[i * 4 + 0] = data[src + 0] ?? 0
22
+ rgba[i * 4 + 1] = data[src + 1] ?? 0
23
+ rgba[i * 4 + 2] = data[src + 2] ?? 0
24
+ rgba[i * 4 + 3] = 255
25
+ }
26
+ return rgba
27
+ }
package/utils/index.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { gotoPage, getDataHtml, getAPIResultHtml } from './e2e'
2
- import { compareScreenshot } from './screenshot'
3
- import type { CompareScreenshotOptions } from './screenshot'
4
-
5
- export {
6
- compareScreenshot,
7
- gotoPage,
8
- getDataHtml,
9
- getAPIResultHtml,
10
- }
11
-
12
- export type { CompareScreenshotOptions }
1
+ import { gotoPage, getDataHtml, getAPIResultHtml } from './e2e'
2
+ import { compareScreenshot } from './screenshot'
3
+ import type { CompareScreenshotOptions } from './screenshot'
4
+
5
+ export {
6
+ compareScreenshot,
7
+ gotoPage,
8
+ getDataHtml,
9
+ getAPIResultHtml,
10
+ }
11
+
12
+ export type { CompareScreenshotOptions }
@@ -1,28 +1,28 @@
1
- // Vitest setup file for the `e2e` project
2
- // allows connecting to an external Playwright instance,
3
- // if `spec.externalPlaywright` or NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
4
-
5
- import { inject } from 'vitest'
6
- import { chromium, firefox, webkit } from 'playwright-core'
7
- import type { Browser, BrowserType } from 'playwright-core'
8
-
9
- declare module 'vitest' {
10
- interface ProvidedContext {
11
- nuxtSpecExternalPlaywright?: string
12
- }
13
- }
14
-
15
- // resolved and provided by `loadVitestConfig` (config/index.mjs)
16
- const externalPlaywright = inject('nuxtSpecExternalPlaywright')
17
-
18
- if (externalPlaywright) {
19
- console.log(`[Nuxt Spec] Using external Playwright instance for e2e tests at: ${externalPlaywright}`)
20
- for (const browserType of [chromium, firefox, webkit] as BrowserType[]) {
21
- // @nuxt/test-utils always calls `playwright[type].launch()` with no way to opt into
22
- // `connect()`, so the launch method is swapped for a connect against the WS endpoint
23
- browserType.launch = (): Promise<Browser> => browserType.connect(externalPlaywright, {
24
- // this allows reaching caller's localhost from within the external Playwright
25
- exposeNetwork: '<loopback>',
26
- })
27
- }
28
- }
1
+ // Vitest setup file for the `e2e` project
2
+ // allows connecting to an external Playwright instance,
3
+ // if `spec.externalPlaywright` or NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
4
+
5
+ import { inject } from 'vitest'
6
+ import { chromium, firefox, webkit } from 'playwright-core'
7
+ import type { Browser, BrowserType } from 'playwright-core'
8
+
9
+ declare module 'vitest' {
10
+ interface ProvidedContext {
11
+ nuxtSpecExternalPlaywright?: string
12
+ }
13
+ }
14
+
15
+ // resolved and provided by `loadVitestConfig` (config/index.mjs)
16
+ const externalPlaywright = inject('nuxtSpecExternalPlaywright')
17
+
18
+ if (externalPlaywright) {
19
+ console.log(`[Nuxt Spec] Using external Playwright instance for e2e tests at: ${externalPlaywright}`)
20
+ for (const browserType of [chromium, firefox, webkit] as BrowserType[]) {
21
+ // @nuxt/test-utils always calls `playwright[type].launch()` with no way to opt into
22
+ // `connect()`, so the launch method is swapped for a connect against the WS endpoint
23
+ browserType.launch = (): Promise<Browser> => browserType.connect(externalPlaywright, {
24
+ // this allows reaching caller's localhost from within the external Playwright
25
+ exposeNetwork: '<loopback>',
26
+ })
27
+ }
28
+ }
@@ -1,201 +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, 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
- }
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
+ }
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createInterface } from 'node:readline'
4
-
5
- const rl = createInterface({ input: process.stdin, output: process.stdout })
6
-
7
- rl.question('Are you sure the current solution was properly tested and verified? (y/n) ', (answer) => {
8
- rl.close()
9
- if (answer.trim().toLowerCase().startsWith('y')) {
10
- console.log('Proceeding with publish...')
11
- process.exit(0)
12
- } else {
13
- console.error('Publish aborted. Please run the tests and try again.')
14
- process.exit(1)
15
- }
16
- })
package/bin/cli.js DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { getCmd, resolvePackageManager } from './helpers/commands.js'
4
-
5
- /**
6
- * CLI tool to scaffold necessary adjustments in project folder.
7
- *
8
- * Allows `setup`, `update` or `ci` to be passed as parameter.
9
- *
10
- * `ci` requires the target Git host (`github` or `gitlab`) as the next parameter,
11
- * the remaining optional parameters are shifted by one position (package manager is not used yet).
12
- *
13
- * Next (optional) parameter might be a boolean to indicate auto mode
14
- * (no prompts, force = true) or manual mode (with prompts, force = false).
15
- *
16
- * Next (optional) parameter allows explicitly passing the package manager
17
- * (`npm`, `pnpm`, `yarn`, `bun` or `deno`), bypassing auto-detection.
18
- */
19
-
20
- // get parameters passed by user
21
- const args = process.argv.slice(2);
22
-
23
- // execute actions based on first param
24
- // additional params might be passed into the called functions
25
- (async () => {
26
- let status = 0
27
- try {
28
- const command = args[0]
29
- const host = command === 'ci' ? args[1] : undefined
30
- const [autoRunArg, packageManagerArg] = args.slice(command === 'ci' ? 2 : 1)
31
- const packageManager = resolvePackageManager(packageManagerArg)
32
- // CLI args are always strings, so 'false' must be parsed explicitly rather than treated as truthy
33
- const autoRun = autoRunArg === 'true'
34
- switch (command) {
35
- case 'setup':
36
- await (await import('./commands/setup.js')).specSetup(autoRun, packageManager)
37
- break
38
- case 'update':
39
- await (await import('./commands/update.js')).specUpdate(autoRun, packageManager)
40
- break
41
- case 'ci':
42
- await (await import('./commands/ci.js')).specCi(host, autoRun)
43
- break
44
- default:
45
- console.log(`[Nuxt Spec] Usage: \`${getCmd(packageManager)} setup [true|false] [npm|pnpm|yarn|bun|deno]\`, \`${getCmd(packageManager)} update [true|false] [npm|pnpm|yarn|bun|deno]\` or \`${getCmd(packageManager)} ci <github|gitlab> [true|false]\``)
46
- status = 1
47
- }
48
- } catch (error) {
49
- console.error('[Nuxt Spec] CLI failed:', error.message)
50
- status = 1
51
- }
52
- process.exit(status)
53
- })()