nuxt-spec 0.2.2 → 0.2.4-alpha
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/README.md +30 -10
- package/bin/prepublish-check.js +16 -0
- package/bin/setup.js +40 -32
- package/config/index.mjs +6 -0
- package/config/utils/warnings.mjs +4 -3
- package/nuxt.config.ts +1 -1
- package/package.json +33 -31
- package/utils/html/report-entry.html +8 -0
- package/utils/html/report-head.html +24 -0
- package/utils/html/report-tail.html +4 -0
- package/utils/index.ts +2 -2
- package/utils/{screenshot.ts → screenshot-compare.ts} +70 -8
- package/utils/screenshot-report.ts +116 -0
package/README.md
CHANGED
|
@@ -38,10 +38,12 @@ If you don't want to use the CLI tool, or you want to understand its flow better
|
|
|
38
38
|
**1)** - Add following dependency into your `package.json`:
|
|
39
39
|
|
|
40
40
|
```json
|
|
41
|
-
"nuxt-spec": "0.2.
|
|
41
|
+
"nuxt-spec": "0.2.4-alpha"
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
It is advised to remove explicit `nuxt`, `vue` and `vue-router` dependencies, if present. The `nuxt-spec` layer brings them and there might be version clashes if defined in both places.
|
|
45
|
+
|
|
46
|
+
**2)** - Add following section into your `nuxt.config.ts`:
|
|
45
47
|
|
|
46
48
|
```ts
|
|
47
49
|
extends: [
|
|
@@ -49,13 +51,13 @@ extends: [
|
|
|
49
51
|
]
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
**3)** -
|
|
54
|
+
**3)** - If `pnpm` is used, add `pnpm-workspace.yaml` file with following content (if you don't have it yet):
|
|
53
55
|
|
|
54
56
|
```yaml
|
|
55
57
|
shamefullyHoist: true
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
**4)** Add `vitest.config.ts` file with following content (if you don't have it yet):
|
|
59
61
|
|
|
60
62
|
```ts
|
|
61
63
|
import { loadVitestConfig } from 'nuxt-spec/config'
|
|
@@ -65,7 +67,13 @@ export default loadVitestConfig({
|
|
|
65
67
|
})
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
**
|
|
70
|
+
**5)** Add `.nuxtrc` file with following content (if you don't have it yet):
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
setups.@nuxt/test-utils="4.0.3"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**6)** - (Optional) Add following scripts into your `package.json`:
|
|
69
77
|
|
|
70
78
|
```json
|
|
71
79
|
"scripts": {
|
|
@@ -75,7 +83,7 @@ export default loadVitestConfig({
|
|
|
75
83
|
}
|
|
76
84
|
```
|
|
77
85
|
|
|
78
|
-
**
|
|
86
|
+
**7)** - (Optional) Setup file structures for tests as follows:
|
|
79
87
|
|
|
80
88
|
```text
|
|
81
89
|
test/
|
|
@@ -90,7 +98,7 @@ test/
|
|
|
90
98
|
└── vitest-unit.test.ts
|
|
91
99
|
```
|
|
92
100
|
|
|
93
|
-
You can use sample files from the [project repository](https://github.com/AloisSeckar/nuxt-spec/tree/v0.2.
|
|
101
|
+
You can use sample files from the [project repository](https://github.com/AloisSeckar/nuxt-spec/tree/v0.2.4-alpha/test).
|
|
94
102
|
|
|
95
103
|
### Install and execute
|
|
96
104
|
|
|
@@ -98,6 +106,8 @@ Whether you used the CLI tool or did the manual setup, you are ready to install
|
|
|
98
106
|
|
|
99
107
|
**1)** - Install the dependencies:
|
|
100
108
|
|
|
109
|
+
It is advised to remove `node_modules` and delete lock file first. Then proceed with fresh installation.
|
|
110
|
+
|
|
101
111
|
<!-- tabs:start -->
|
|
102
112
|
|
|
103
113
|
#### Install using **npm**
|
|
@@ -253,11 +263,11 @@ Planned future development:
|
|
|
253
263
|
- reason about (not) using Vitest browser mode (or make it optional)
|
|
254
264
|
- solution for visual regression testing - (currently there is experimental custom solution)
|
|
255
265
|
|
|
256
|
-
See [CHANGELOG.md](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.
|
|
266
|
+
See [CHANGELOG.md](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.4-alpha/CHANGELOG.md) for the latest updates and features.
|
|
257
267
|
|
|
258
268
|
## Configuration
|
|
259
269
|
|
|
260
|
-
By default, `nuxt-spec` uses Vitest configuration defined in [`/config/index.mjs`](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.
|
|
270
|
+
By default, `nuxt-spec` uses Vitest configuration defined in [`/config/index.mjs`](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.4-alpha/config/index.mjs). The configuration is based on [Nuxt team recommendations](https://nuxt.com/docs/4.x/getting-started/testing) and our best judgement.
|
|
261
271
|
|
|
262
272
|
To add/override your custom config, you can create (or scaffold via CLI tool) a file named `vitest.config.ts` in the root of your project with the following content:
|
|
263
273
|
|
|
@@ -305,6 +315,14 @@ export default loadVitestConfig({
|
|
|
305
315
|
|
|
306
316
|
Alternatively, if you don't want to use any part of the `nuxt-spec` default configuration at all, you can override `vitest.config.ts` file completely and define your own [Vitest configuration](https://vitest.dev/config/) from scratch.
|
|
307
317
|
|
|
318
|
+
### Filtering out log messages
|
|
319
|
+
|
|
320
|
+
Some tedious and unrelevant log messages may keep appearing in running tests creating noise and hiding the real issues.
|
|
321
|
+
|
|
322
|
+
Via `NUXT_SPEC_MESSAGE_FILTERS` env variable you may pass a comma-separated list of (plain) text patterns that should be omited.
|
|
323
|
+
|
|
324
|
+
It only applies to logs processed by `vitest` though, so some messages might still prevail.
|
|
325
|
+
|
|
308
326
|
## Utilities
|
|
309
327
|
|
|
310
328
|
Nuxt Spec offers a couple of utility functions that are exported via `nuxt-spec/utils` subpackage.
|
|
@@ -362,7 +380,9 @@ const html: string = await getAPIResultHtml('/', '#api-fetch', '/your-api', '#ap
|
|
|
362
380
|
const html: string = await getAPIResultHtml(page, '#api-fetch', '/your-api', '#api-result')
|
|
363
381
|
```
|
|
364
382
|
|
|
365
|
-
For detailed description, see [utils.d.ts](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.
|
|
383
|
+
For detailed description, see [utils.d.ts](https://github.com/AloisSeckar/nuxt-spec/blob/v0.2.4-alpha/utils/index.d.ts).
|
|
384
|
+
|
|
385
|
+
The `compareScreenshot` function usage results into HTML report file being automatically created. The file is generated within the specified `__current__` directory as `report_YYYYMMDDHHMMSS.html`. It contains all failed screenshots comparison. When test suite is over, file is attempted to be opened in system default browser (unless Node operates in `CI` mode).
|
|
366
386
|
|
|
367
387
|
## Contact
|
|
368
388
|
|
|
@@ -0,0 +1,16 @@
|
|
|
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/setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
3
4
|
import {
|
|
4
5
|
createFileFromWebTemplate, deletePath, getPackageManager, hasJsonKey,
|
|
5
6
|
pathExists, promptUser, removeFromJsonFile, showMessage,
|
|
@@ -16,9 +17,11 @@ import {
|
|
|
16
17
|
* 2) adds `extends: ['nuxt-spec']` to `nuxt.config.ts`
|
|
17
18
|
* 3) creates/updates `pnpm-workspace.yaml` file (only if pnpm is used)
|
|
18
19
|
* 4) creates default `vitest.config.ts` file
|
|
19
|
-
* 5)
|
|
20
|
-
* 6)
|
|
21
|
-
* 7)
|
|
20
|
+
* 5) creates default `.nuxtrc` file
|
|
21
|
+
* 6) adds test-related scripts in `package.json`
|
|
22
|
+
* 7) creates sample test files
|
|
23
|
+
* 8) clear node_modules and lock file(s)
|
|
24
|
+
* 9) run install command
|
|
22
25
|
*
|
|
23
26
|
* @param {boolean} autoRun - Whether to run the setup automatically without any prompts (defaults to false).
|
|
24
27
|
*/
|
|
@@ -37,7 +40,7 @@ export async function specSetup(autoRun = false) {
|
|
|
37
40
|
// add nuxt-spec
|
|
38
41
|
try {
|
|
39
42
|
await updateJsonFile('package.json', 'dependencies', {
|
|
40
|
-
'nuxt-spec': '0.2.
|
|
43
|
+
'nuxt-spec': '0.2.4-alpha',
|
|
41
44
|
}, isAutoRun, 'This will add \'nuxt-spec\' dependency to your \'package.json\'. Continue?')
|
|
42
45
|
} catch (error) {
|
|
43
46
|
console.error('Error adding \'nuxt-spec\' dependency:\n', error.message)
|
|
@@ -107,7 +110,7 @@ export async function specSetup(autoRun = false) {
|
|
|
107
110
|
if (pathExists('pnpm-workspace.yaml')) {
|
|
108
111
|
await updateTextFile('pnpm-workspace.yaml', ['shamefully-hoist: true'], isAutoRun, 'This will adjust \'pnpm-workspace.yaml\' file in your project. Continue?')
|
|
109
112
|
} else {
|
|
110
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
113
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/config/templates/pnpm-workspace.yaml.template',
|
|
111
114
|
'pnpm-workspace.yaml', isAutoRun, 'This will add \'pnpm-workspace.yaml\' file for your project. Continue?')
|
|
112
115
|
}
|
|
113
116
|
} catch (error) {
|
|
@@ -117,15 +120,22 @@ export async function specSetup(autoRun = false) {
|
|
|
117
120
|
|
|
118
121
|
// 4) create vitest.config.ts
|
|
119
122
|
try {
|
|
120
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
123
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/config/templates/vitest.config.ts.template',
|
|
121
124
|
'vitest.config.ts', isAutoRun, 'This will create a new \'vitest.config.ts\' file for your project. Continue?')
|
|
122
125
|
} catch (error) {
|
|
123
126
|
console.error('Error setting up \'vitest.config.ts\':\n', error.message)
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
// 5)
|
|
129
|
+
// 5) create .nuxtrc to prevent @nuxt/test-utils setup from running automatically on first start
|
|
130
|
+
if (!pathExists('.nuxtrc')) {
|
|
131
|
+
try {
|
|
132
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/.nuxtrc', '.nuxtrc', isAutoRun, 'This will create a \'.nuxtrc\' file to prevent @nuxt/test-utils setup from running automatically when dev server starts. Continue?')
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Error creating \'.nuxtrc\':\n', error.message)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
127
137
|
|
|
128
|
-
//
|
|
138
|
+
// 6) modify package.json with test scripts
|
|
129
139
|
try {
|
|
130
140
|
await updateJsonFile('package.json', 'scripts', {
|
|
131
141
|
'test': 'vitest run',
|
|
@@ -136,57 +146,42 @@ export async function specSetup(autoRun = false) {
|
|
|
136
146
|
console.error('Error adjusting scripts in \'package.json\':\n', error.message)
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
//
|
|
140
|
-
if (packageManager === 'pnpm') {
|
|
141
|
-
try {
|
|
142
|
-
await updateJsonFile('package.json', 'pnpm', {
|
|
143
|
-
onlyBuiltDependencies: [
|
|
144
|
-
'@parcel/watcher',
|
|
145
|
-
'esbuild',
|
|
146
|
-
'unrs-resolver',
|
|
147
|
-
],
|
|
148
|
-
}, isAutoRun, 'This will adjust pnpm approved build scripts in your \'package.json\'. Continue?')
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.error('Error adjusting pnpm approved build scripts in \'package.json\':\n', error.message)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 6) create sample test files
|
|
149
|
+
// 7) create sample test files
|
|
155
150
|
const createSampleTests = isAutoRun || await promptUser('Do you want to create sample tests in \'/test\' folder?')
|
|
156
151
|
if (createSampleTests) {
|
|
157
152
|
try {
|
|
158
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
153
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/test/browser/vitest-browser.test.ts',
|
|
159
154
|
'test/browser/vitest-browser.test.ts', true)
|
|
160
155
|
} catch (error) {
|
|
161
156
|
console.error('Error setting up \'vitest-browser.test.ts\':\n', error.message)
|
|
162
157
|
}
|
|
163
158
|
try {
|
|
164
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
159
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/test/e2e/nuxt-e2e.test.ts',
|
|
165
160
|
'test/e2e/nuxt-e2e.test.ts', true)
|
|
166
161
|
} catch (error) {
|
|
167
162
|
console.error('Error setting up \'nuxt-e2e.test.ts\':\n', error.message)
|
|
168
163
|
}
|
|
169
164
|
try {
|
|
170
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
165
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/test/e2e/nuxt-visual.test.ts',
|
|
171
166
|
'test/e2e/nuxt-visual.test.ts', true)
|
|
172
167
|
} catch (error) {
|
|
173
168
|
console.error('Error setting up \'nuxt-visual.test.ts\':\n', error.message)
|
|
174
169
|
}
|
|
175
170
|
try {
|
|
176
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
171
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/test/nuxt/nuxt-unit.test.ts',
|
|
177
172
|
'test/nuxt/nuxt-unit.test.ts', true)
|
|
178
173
|
} catch (error) {
|
|
179
174
|
console.error('Error setting up \'nuxt-unit.test.ts\':\n', error.message)
|
|
180
175
|
}
|
|
181
176
|
try {
|
|
182
|
-
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.
|
|
177
|
+
await createFileFromWebTemplate('https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v0.2.4-alpha/test/unit/vitest-unit.test.ts',
|
|
183
178
|
'test/unit/vitest-unit.test.ts', true)
|
|
184
179
|
} catch (error) {
|
|
185
180
|
console.error('Error setting up \'vitest-unit.test.ts\':\n', error.message)
|
|
186
181
|
}
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
//
|
|
184
|
+
// 8) clear node_modules and lock file(s)
|
|
190
185
|
const prepareForReinstall = isAutoRun || await promptUser('Dependencies should be re-installed now. Do you want to remove node_modules and the lock file?')
|
|
191
186
|
if (prepareForReinstall) {
|
|
192
187
|
if (pathExists('node_modules')) {
|
|
@@ -233,10 +228,23 @@ export async function specSetup(autoRun = false) {
|
|
|
233
228
|
}
|
|
234
229
|
}
|
|
235
230
|
|
|
236
|
-
//
|
|
231
|
+
// 9) run install command
|
|
232
|
+
const runInstall = isAutoRun || await promptUser(`Fresh \`${packageManager} install\` is required. Do you want to run it now?`)
|
|
233
|
+
if (runInstall) {
|
|
234
|
+
try {
|
|
235
|
+
showMessage(`Running \`${packageManager} install\`...`)
|
|
236
|
+
execSync(`${packageManager} install`, { stdio: 'inherit' })
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(`Error running \`${packageManager} install\`:\n`, error.message)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 10) inform user
|
|
237
243
|
showMessage('')
|
|
238
244
|
showMessage('NUXT SPEC SETUP COMPLETE', 2)
|
|
239
|
-
|
|
245
|
+
if (!runInstall) {
|
|
246
|
+
showMessage(`Proceed with \`${packageManager} install\` to get started.`)
|
|
247
|
+
}
|
|
240
248
|
|
|
241
249
|
// force exit to prevent #20
|
|
242
250
|
process.exit(0)
|
package/config/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// based on https://nuxt.com/docs/4.x/getting-started/testing#setup
|
|
3
3
|
// `projects=false` can be used to suspend the default usage of "projects" in Vitest config
|
|
4
4
|
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
5
6
|
import { onConsoleLog } from './utils/warnings.mjs' // filter out unnecessary logs
|
|
6
7
|
import { mergeConfig } from './utils/merge.mjs' // defu-based merge function
|
|
7
8
|
import { defineConfig } from 'vitest/config'
|
|
@@ -9,6 +10,9 @@ import { defineVitestProject } from '@nuxt/test-utils/config'
|
|
|
9
10
|
import { playwright } from '@vitest/browser-playwright'
|
|
10
11
|
import vue from '@vitejs/plugin-vue'
|
|
11
12
|
|
|
13
|
+
// absolute path so it works from nuxt-ignis package
|
|
14
|
+
const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot-report.ts', import.meta.url))
|
|
15
|
+
|
|
12
16
|
export async function loadVitestConfig(userVitestConfig, projects = true) {
|
|
13
17
|
const baseConfig = {
|
|
14
18
|
test: {
|
|
@@ -51,6 +55,8 @@ export async function loadVitestConfig(userVitestConfig, projects = true) {
|
|
|
51
55
|
name: 'e2e',
|
|
52
56
|
include: ['test/e2e/**/*.{test,spec}.ts'],
|
|
53
57
|
environment: 'node',
|
|
58
|
+
// create report file for visual regression testing
|
|
59
|
+
globalSetup: [screenshotReportSetup],
|
|
54
60
|
},
|
|
55
61
|
},
|
|
56
62
|
// proposed setup for browser component tests (with Playwright runner)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const messageFilters = [
|
|
2
|
-
// remove once @nuxt/test-utils starts depending on Vitest 4.1
|
|
3
|
-
// https://github.com/nuxt/test-utils/pull/1620
|
|
4
|
-
'Importing from "vitest/environments" is deprecated',
|
|
5
2
|
// Node+Windows false positive
|
|
6
3
|
// https://github.com/nuxt/icon/issues/140
|
|
7
4
|
'Use of deprecated trailing slash pattern mapping',
|
|
8
5
|
// remove once Vue stops considering <Suspense> experimental
|
|
9
6
|
'<Suspense> is an experimental feature',
|
|
7
|
+
// merge defaults with user-defined filters from NUXT_SPEC_MESSAGE_FILTERS env variable
|
|
8
|
+
...(process.env.NUXT_SPEC_MESSAGE_FILTERS
|
|
9
|
+
? process.env.NUXT_SPEC_MESSAGE_FILTERS.split(',')
|
|
10
|
+
: []),
|
|
10
11
|
]
|
|
11
12
|
|
|
12
13
|
// 1) filter-out unnecessary stderr/stdout logs coming from Vitest
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-spec",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4-alpha",
|
|
4
4
|
"description": "Test-pack layer for Nuxt Applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "./nuxt.config.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"analyze": "nuxt analyze",
|
|
14
|
+
"eslint": "eslint .",
|
|
15
|
+
"build": "pnpm test && nuxt build",
|
|
16
|
+
"dev": "nuxt dev",
|
|
17
|
+
"generate": "nuxt generate",
|
|
18
|
+
"prepublishOnly": "node bin/prepublish-check.js",
|
|
19
|
+
"preview": "nuxt preview",
|
|
20
|
+
"start": "nuxt start",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test-u": "vitest run -u",
|
|
23
|
+
"test-i": "vitest"
|
|
24
|
+
},
|
|
12
25
|
"bin": {
|
|
13
26
|
"nuxt-spec": "bin/cli.js"
|
|
14
27
|
},
|
|
@@ -38,35 +51,24 @@
|
|
|
38
51
|
"nuxt.config.ts"
|
|
39
52
|
],
|
|
40
53
|
"dependencies": {
|
|
41
|
-
"@nuxt/eslint": "1.
|
|
42
|
-
"@nuxt/test-utils": "4.0.
|
|
43
|
-
"@vitejs/plugin-vue": "6.0.
|
|
44
|
-
"@vitest/browser": "4.1.
|
|
45
|
-
"@vitest/browser-playwright": "4.1.
|
|
46
|
-
"@vitest/ui": "4.1.
|
|
47
|
-
"@vue/test-utils": "2.4.
|
|
48
|
-
"elrh-cosca": "0.3.
|
|
49
|
-
"fast-png": "8.0.0",
|
|
50
|
-
"happy-dom": "20.
|
|
51
|
-
"nuxt": "4.4.
|
|
52
|
-
"pixelmatch": "7.
|
|
53
|
-
"playwright-core": "1.
|
|
54
|
-
"typescript": "6.0.3",
|
|
55
|
-
"vitest": "4.1.
|
|
54
|
+
"@nuxt/eslint": "~1.16.0",
|
|
55
|
+
"@nuxt/test-utils": "4.0.3",
|
|
56
|
+
"@vitejs/plugin-vue": "~6.0.7",
|
|
57
|
+
"@vitest/browser": "4.1.9",
|
|
58
|
+
"@vitest/browser-playwright": "4.1.9",
|
|
59
|
+
"@vitest/ui": "4.1.9",
|
|
60
|
+
"@vue/test-utils": "2.4.11",
|
|
61
|
+
"elrh-cosca": "0.3.6",
|
|
62
|
+
"fast-png": "~8.0.0",
|
|
63
|
+
"happy-dom": "~20.10.6",
|
|
64
|
+
"nuxt": "4.4.8",
|
|
65
|
+
"pixelmatch": "~7.2.0",
|
|
66
|
+
"playwright-core": "~1.61.1",
|
|
67
|
+
"typescript": "~6.0.3",
|
|
68
|
+
"vitest": "4.1.9",
|
|
56
69
|
"vitest-browser-vue": "2.1.0",
|
|
57
|
-
"vue": "3.5.
|
|
58
|
-
"vue-router": "5.0
|
|
70
|
+
"vue": "~3.5.39",
|
|
71
|
+
"vue-router": "~5.1.0"
|
|
59
72
|
},
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
"eslint": "eslint .",
|
|
63
|
-
"build": "pnpm test && nuxt build",
|
|
64
|
-
"dev": "nuxt dev",
|
|
65
|
-
"generate": "nuxt generate",
|
|
66
|
-
"preview": "nuxt preview",
|
|
67
|
-
"start": "nuxt start",
|
|
68
|
-
"test": "vitest run",
|
|
69
|
-
"test-u": "vitest run -u",
|
|
70
|
-
"test-i": "vitest"
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
+
"packageManager": "pnpm@11.9.0"
|
|
74
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="failure">
|
|
2
|
+
<h2>{{FILE_NAME}}</h2>
|
|
3
|
+
<p class="message">{{MESSAGE}}</p>
|
|
4
|
+
<div class="pair">
|
|
5
|
+
<figure><figcaption>Baseline image</figcaption><img src="{{BASELINE_URI}}" alt="Baseline image for reference"></figure>
|
|
6
|
+
<figure><figcaption>Actual screenshot</figcaption><img src="{{ACTUAL_URI}}" alt="Actual screenshot taken"></figure>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Visual Regression Report</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; background: #f5f5f5; color: #222; }
|
|
8
|
+
h1 { font-size: 1.4rem; }
|
|
9
|
+
.failure { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; box-shadow: 0 1px 3px rgba(0, 0, 0, .1); }
|
|
10
|
+
.failure h2 { font-size: 1rem; margin: 0 0 .5rem; }
|
|
11
|
+
.failure .message { color: #b00; font-size: .85rem; margin: 0 0 .75rem; }
|
|
12
|
+
.pair { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
13
|
+
.pair figure { margin: 0; flex: 1 1 0; min-width: 240px; }
|
|
14
|
+
.pair figcaption { font-size: .8rem; margin-bottom: .25rem; font-weight: bold; }
|
|
15
|
+
.pair figure:first-child figcaption { color: #0b0; }
|
|
16
|
+
.pair figure:last-child figcaption { color: #b00; }
|
|
17
|
+
.pair img { max-width: 100%; background: #fff; }
|
|
18
|
+
.pair figure:first-child img { border: 2px solid #0b0; }
|
|
19
|
+
.pair figure:last-child img { border: 2px solid #b00; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<h1>Visual Regression Report - {{TIMESTAMP}}</h1>
|
|
24
|
+
<!-- BEGIN failed screenshot entries -->
|
package/utils/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { gotoPage, getDataHtml, getAPIResultHtml } from './e2e'
|
|
2
|
-
import { compareScreenshot } from './screenshot'
|
|
3
|
-
import type { CompareScreenshotOptions } from './screenshot'
|
|
2
|
+
import { compareScreenshot } from './screenshot-compare'
|
|
3
|
+
import type { CompareScreenshotOptions } from './screenshot-compare'
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
compareScreenshot,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import {
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve, sep } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { decode, type DecodedPng } from 'fast-png'
|
|
5
|
+
import { expect } from 'vitest'
|
|
6
|
+
import { ensureReportCreated, getInjection } from './screenshot-report'
|
|
5
7
|
import pixelmatch from 'pixelmatch'
|
|
6
8
|
import type { NuxtPage } from '@nuxt/test-utils'
|
|
7
9
|
|
|
@@ -22,22 +24,37 @@ export interface CompareScreenshotOptions {
|
|
|
22
24
|
|
|
23
25
|
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
24
26
|
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
25
|
-
|
|
27
|
+
// create report file on first call
|
|
28
|
+
ensureReportCreated()
|
|
29
|
+
|
|
30
|
+
const root = process.cwd()
|
|
31
|
+
|
|
32
|
+
// ensure the target directory stays within the project root
|
|
33
|
+
const dir = resolveWithin(root, options?.targetDir ?? 'test/e2e')
|
|
34
|
+
|
|
26
35
|
const baselineDir = resolve(dir, '__baseline__')
|
|
27
36
|
const currentDir = resolve(dir, '__current__')
|
|
28
37
|
|
|
29
38
|
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
30
39
|
const fileName = options?.fileName ?? `${route}.png`
|
|
31
40
|
|
|
41
|
+
// warning on custom non-png file extensions
|
|
42
|
+
if (!fileName.toLowerCase().endsWith('.png')) {
|
|
43
|
+
console.warn(`Screenshots from \`compareScreenshot\` are always saved as PNG. Consider different file name than '${fileName}'.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ensure the file name cannot escape its target directory
|
|
47
|
+
const baselinePath = resolveWithin(baselineDir, fileName)
|
|
48
|
+
const currentPath = resolveWithin(currentDir, fileName)
|
|
49
|
+
|
|
32
50
|
// capture element specified by locator or a full-page screenshot as PNG
|
|
33
51
|
const screenshot = options?.selector
|
|
34
52
|
? await page.locator(options.selector).screenshot()
|
|
35
53
|
: await page.screenshot({ fullPage: true })
|
|
36
|
-
const baselinePath = resolve(baselineDir, fileName)
|
|
37
54
|
|
|
38
55
|
// always save the current screenshot for inspection
|
|
39
56
|
mkdirSync(currentDir, { recursive: true })
|
|
40
|
-
writeFileSync(
|
|
57
|
+
writeFileSync(currentPath, screenshot)
|
|
41
58
|
|
|
42
59
|
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
43
60
|
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
@@ -55,7 +72,9 @@ export async function compareScreenshot(page: NuxtPage, options?: CompareScreens
|
|
|
55
72
|
const { width, height } = baselineImg
|
|
56
73
|
|
|
57
74
|
if (actualImg.width !== width || actualImg.height !== height) {
|
|
58
|
-
|
|
75
|
+
const message = `Screenshot size mismatch: expected ${width}x${height}, got ${actualImg.width}x${actualImg.height}. Actual saved to: ${currentPath}`
|
|
76
|
+
appendToReport(fileName, message, baseline, screenshot)
|
|
77
|
+
expect.fail(message)
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
@@ -67,12 +86,55 @@ export async function compareScreenshot(page: NuxtPage, options?: CompareScreens
|
|
|
67
86
|
|
|
68
87
|
if (diffCount > maxAllowed) {
|
|
69
88
|
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
70
|
-
|
|
89
|
+
const message = `Screenshot mismatch: ${diffCount} pixels differ (${ratio}%), allowed ${maxAllowed}. Actual saved to: ${currentPath}`
|
|
90
|
+
appendToReport(fileName, message, baseline, screenshot)
|
|
91
|
+
expect.fail(message)
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
return true
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
// helper to make sure passed options are not escaping from cwd
|
|
98
|
+
export function resolveWithin(base: string, segment: string): string {
|
|
99
|
+
const target = resolve(base, segment)
|
|
100
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
101
|
+
throw new Error(`Invalid path: "${segment}" resolves outside of "${base}"`)
|
|
102
|
+
}
|
|
103
|
+
return target
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// escape a string for safe interpolation into the HTML report
|
|
107
|
+
function escapeHtml(value: string): string {
|
|
108
|
+
return value.replace(/[&<>"']/g, char => ({
|
|
109
|
+
'&': '&',
|
|
110
|
+
'<': '<',
|
|
111
|
+
'>': '>',
|
|
112
|
+
'"': '"',
|
|
113
|
+
'\'': ''',
|
|
114
|
+
}[char] ?? char))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const HTML_DIR = resolve(fileURLToPath(import.meta.url), '..', 'html')
|
|
118
|
+
const REPORT_ENTRY = readFileSync(resolve(HTML_DIR, 'report-entry.html'), 'utf-8')
|
|
119
|
+
|
|
120
|
+
// template for screenshot comparison entry
|
|
121
|
+
// append a side-by-side baseline/actual comparison entry to the HTML report
|
|
122
|
+
function appendToReport(fileName: string, message: string, baseline: Uint8Array, actual: Uint8Array): void {
|
|
123
|
+
const reportPath = getInjection('screenshotReportPath')
|
|
124
|
+
if (!reportPath || !existsSync(reportPath)) return
|
|
125
|
+
|
|
126
|
+
const baselineUri = `data:image/png;base64,${Buffer.from(baseline).toString('base64')}`
|
|
127
|
+
const actualUri = `data:image/png;base64,${Buffer.from(actual).toString('base64')}`
|
|
128
|
+
|
|
129
|
+
const entry = REPORT_ENTRY
|
|
130
|
+
.replace('{{FILE_NAME}}', escapeHtml(fileName))
|
|
131
|
+
.replace('{{MESSAGE}}', escapeHtml(message))
|
|
132
|
+
.replace('{{BASELINE_URI}}', baselineUri)
|
|
133
|
+
.replace('{{ACTUAL_URI}}', actualUri)
|
|
134
|
+
|
|
135
|
+
appendFileSync(reportPath, entry)
|
|
136
|
+
}
|
|
137
|
+
|
|
76
138
|
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
77
139
|
function toRGBA(img: DecodedPng): Uint8Array {
|
|
78
140
|
const { width, height, data, channels = 4 } = img
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { platform } from 'node:os'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { inject } from 'vitest'
|
|
7
|
+
import type { ProvidedContext } from 'vitest'
|
|
8
|
+
import type { TestProject } from 'vitest/node'
|
|
9
|
+
|
|
10
|
+
declare module 'vitest' {
|
|
11
|
+
interface ProvidedContext {
|
|
12
|
+
screenshotReportPath: string
|
|
13
|
+
screenshotReportTitle: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const templatesDir = resolve(fileURLToPath(import.meta.url), '..', 'html')
|
|
18
|
+
|
|
19
|
+
// timestamp string
|
|
20
|
+
// separators=false produces YYYYMMDDHHMMSS for report file name
|
|
21
|
+
// separators=true produces YYYY-MM-DD HH:MM:SS for report title
|
|
22
|
+
function reportTimestamp(date: Date, separators = false): string {
|
|
23
|
+
const pad2 = (n: number) => String(n).padStart(2, '0')
|
|
24
|
+
const dateSeparator = separators ? '-' : ''
|
|
25
|
+
const midSeparator = separators ? ' ' : ''
|
|
26
|
+
const timeSeparator = separators ? ':' : ''
|
|
27
|
+
return [
|
|
28
|
+
date.getFullYear(),
|
|
29
|
+
dateSeparator,
|
|
30
|
+
pad2(date.getMonth() + 1),
|
|
31
|
+
dateSeparator,
|
|
32
|
+
pad2(date.getDate()),
|
|
33
|
+
midSeparator,
|
|
34
|
+
pad2(date.getHours()),
|
|
35
|
+
timeSeparator,
|
|
36
|
+
pad2(date.getMinutes()),
|
|
37
|
+
timeSeparator,
|
|
38
|
+
pad2(date.getSeconds()),
|
|
39
|
+
].join('')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// read the header template and replace the timestamp placeholder
|
|
43
|
+
function reportScaffold(titleTimestamp: string): string {
|
|
44
|
+
const template = readFileSync(resolve(templatesDir, 'report-head.html'), 'utf-8')
|
|
45
|
+
return template.replace('{{TIMESTAMP}}', titleTimestamp)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// resolve the value created in the Vitest globalSetup helper
|
|
49
|
+
// (provide/inject is preferred, with process.env kept as a fallback)
|
|
50
|
+
export function getInjection(key: keyof ProvidedContext): string | undefined {
|
|
51
|
+
try {
|
|
52
|
+
const injected = inject(key)
|
|
53
|
+
if (injected) return injected
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// `inject` is unavailable outside of the Vitest worker context
|
|
56
|
+
console.debug(`(nuxt-spec) getInjection(${key}) - error during Vitest injection`)
|
|
57
|
+
console.error((e as Error).message || e)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.debug(`(nuxt-spec) getInjection(${key}) - falling back to process.env`)
|
|
61
|
+
|
|
62
|
+
// env variable fallbacks
|
|
63
|
+
switch (key) {
|
|
64
|
+
case 'screenshotReportPath':
|
|
65
|
+
return process.env.SCREENSHOT_REPORT_PATH
|
|
66
|
+
case 'screenshotReportTitle':
|
|
67
|
+
return process.env.SCREENSHOT_REPORT_TITLE
|
|
68
|
+
default:
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// create report file on first call, using the path computed in setup()
|
|
74
|
+
export function ensureReportCreated(): void {
|
|
75
|
+
const reportPath = getInjection('screenshotReportPath')
|
|
76
|
+
if (!reportPath || existsSync(reportPath)) return
|
|
77
|
+
|
|
78
|
+
const titleTimestamp = getInjection('screenshotReportTitle') ?? reportTimestamp(new Date(), true)
|
|
79
|
+
|
|
80
|
+
writeFileSync(reportPath, reportScaffold(titleTimestamp))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Vitest globalSetup entry point
|
|
84
|
+
// - computes the report path and exposes it via provide/inject
|
|
85
|
+
// - the report file is created lazily on first compareScreenshot call
|
|
86
|
+
// - closes the HTML report via a callback once tests are finished (if it was created)
|
|
87
|
+
export default function setup({ provide }: TestProject) {
|
|
88
|
+
const dir = resolve(process.cwd(), 'test/e2e', '__current__')
|
|
89
|
+
mkdirSync(dir, { recursive: true })
|
|
90
|
+
|
|
91
|
+
const NOW = new Date()
|
|
92
|
+
const reportPath = resolve(dir, `report-${reportTimestamp(NOW)}.html`)
|
|
93
|
+
provide('screenshotReportPath', reportPath)
|
|
94
|
+
provide('screenshotReportTitle', reportTimestamp(NOW, true))
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
if (!existsSync(reportPath)) return
|
|
98
|
+
|
|
99
|
+
let footer = readFileSync(resolve(templatesDir, 'report-tail.html'), 'utf-8')
|
|
100
|
+
footer = footer.replace('{{TIMESTAMP}}', new Date().toISOString())
|
|
101
|
+
appendFileSync(reportPath, footer)
|
|
102
|
+
console.log(`\n(nuxt-spec) Visual regression report available at:\nfile://${reportPath}`)
|
|
103
|
+
|
|
104
|
+
if (!process.env.CI) {
|
|
105
|
+
console.log('(nuxt-spec) Opening report in default browser...')
|
|
106
|
+
const openCmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open'
|
|
107
|
+
exec(`${openCmd} "${reportPath}"`, (err) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
console.log('(nuxt-spec) Failed to automatically open report')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log('\n')
|
|
115
|
+
}
|
|
116
|
+
}
|