nuxt-spec 0.3.4 → 0.4.0-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.
- package/LICENSE +21 -21
- package/README.md +13 -13
- 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/_internal/prepublish-check.js +16 -16
- package/bin/cli.js +53 -44
- package/bin/commands/ci.js +124 -0
- package/bin/commands/setup.js +367 -0
- package/bin/commands/update.js +126 -0
- package/bin/helpers/commands.js +82 -82
- package/bin/helpers/constants.js +8 -0
- package/bin/helpers/git.js +73 -0
- package/config/index.d.ts +36 -33
- package/config/index.mjs +147 -132
- package/config/report/report-entry.html +6 -0
- package/config/report/report-head.html +34 -0
- package/{utils/screenshot/report-entry.html → config/report/report-screenshot.html} +8 -8
- package/config/report/report-tail.html +4 -0
- package/config/templates/.github/workflows/ci-test-update.yml.template +105 -0
- package/config/templates/.github/workflows/ci-test-verify.yml.template +68 -0
- package/config/templates/.gitlab/ci/ci-test-update.yml.template +69 -0
- package/config/templates/.gitlab/ci/ci-test-verify.yml.template +41 -0
- package/config/templates/.gitlab-ci.yml.template +6 -0
- package/config/templates/pnpm-workspace.yaml.template +19 -19
- package/config/templates/vitest.config.ts.template +5 -5
- package/config/utils/merge.mjs +43 -43
- package/config/utils/options.d.mts +59 -0
- package/config/utils/options.mjs +116 -0
- package/config/utils/reporter.mjs +409 -0
- package/config/utils/warnings.mjs +38 -33
- package/modules/spec-options.ts +23 -0
- package/nuxt.config.ts +20 -24
- package/package.json +37 -31
- package/utils/e2e.ts +131 -131
- package/utils/helpers/check-params.ts +44 -41
- package/utils/helpers/location-hint.ts +30 -30
- package/utils/helpers/screenshot-utils.ts +27 -0
- package/utils/index.ts +12 -12
- package/utils/playwright.ts +28 -20
- package/utils/screenshot.ts +201 -172
- package/bin/setup.js +0 -287
- package/bin/update.js +0 -94
- package/utils/screenshot/report-head.html +0 -24
- package/utils/screenshot/report-tail.html +0 -4
- package/utils/screenshot/report-utils.ts +0 -236
package/bin/setup.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
|
-
import {
|
|
5
|
-
createFileFromWebTemplate, deletePath, hasJsonKey,
|
|
6
|
-
pathExists, promptUser, removeFromJsonFile, showMessage,
|
|
7
|
-
updateConfigFile, updateJsonFile, updateTextFile,
|
|
8
|
-
} from 'elrh-cosca'
|
|
9
|
-
import {
|
|
10
|
-
getPlaywrightInstallCmd, getPrepareCmd,
|
|
11
|
-
} from './helpers/commands.js'
|
|
12
|
-
|
|
13
|
-
const TARGET_VERSION = '0.3.4'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* CLI tool to scaffold necessary adjustments in project folder.
|
|
17
|
-
*
|
|
18
|
-
* It first asks whether to run in "auto" mode (no prompts, force = true) or "manual" mode (with prompts, force = false).
|
|
19
|
-
*
|
|
20
|
-
* Then it:
|
|
21
|
-
* 1) adds `nuxt-spec` into `package.json` dependencies and removes `nuxt`, `vue` and `vue-router` if present
|
|
22
|
-
* 2) adds `extends: ['nuxt-spec']` to `nuxt.config.ts`
|
|
23
|
-
* 3) creates/updates `pnpm-workspace.yaml` file (only if pnpm is used)
|
|
24
|
-
* 4) creates default `vitest.config.ts` file
|
|
25
|
-
* 5) creates default `.nuxtrc` file
|
|
26
|
-
* 6) adds test-related scripts in `package.json`
|
|
27
|
-
* 7) creates sample test files
|
|
28
|
-
* 8) clear node_modules and lock file(s)
|
|
29
|
-
* 9) run install command
|
|
30
|
-
* 10) run Nuxt prepare command to generate types and auto-imports
|
|
31
|
-
* 11) run Playwright setup command
|
|
32
|
-
*
|
|
33
|
-
* @param {boolean} autoRun - Whether to run the setup automatically without any prompts (defaults to false).
|
|
34
|
-
* @param {string} [packageManager] - Package manager to be used (`npm`, `pnpm`, `yarn`, `bun` or `deno`).
|
|
35
|
-
*/
|
|
36
|
-
export async function specSetup(autoRun = false, packageManager) {
|
|
37
|
-
showMessage('NUXT SPEC SETUP')
|
|
38
|
-
showMessage('This CLI tool will help you include Nuxt Spec in your project.')
|
|
39
|
-
showMessage('Refer to the documentation for more information.', 2)
|
|
40
|
-
|
|
41
|
-
const isAutoRun = autoRun || await promptUser('Do you want to set everything up automatically (no more prompts)?')
|
|
42
|
-
showMessage('')
|
|
43
|
-
|
|
44
|
-
// 1) manage dependencies in package.json
|
|
45
|
-
|
|
46
|
-
// add nuxt-spec
|
|
47
|
-
try {
|
|
48
|
-
await updateJsonFile('package.json', 'dependencies', {
|
|
49
|
-
['nuxt-spec']: TARGET_VERSION,
|
|
50
|
-
}, isAutoRun, `This will add 'nuxt-spec' dependency to your 'package.json'. Continue?`)
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`Error adding 'nuxt-spec' dependency:\n`, error.message)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// remove now obsolete nuxt, vue and vue-router
|
|
56
|
-
const removeDeps = isAutoRun || await promptUser(`As 'nuxt-spec' provides 'nuxt', 'vue' and 'vue-router' dependencies out of the box, do you want to remove them from your 'package.json' to avoid duplications and possible version clashes?`)
|
|
57
|
-
if (removeDeps) {
|
|
58
|
-
if (hasJsonKey('package.json', 'dependencies.nuxt')) {
|
|
59
|
-
try {
|
|
60
|
-
await removeFromJsonFile('package.json', 'dependencies.nuxt', true)
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Error removing \'nuxt\' dependency:\n', error.message)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (hasJsonKey('package.json', 'dependencies.vue')) {
|
|
66
|
-
try {
|
|
67
|
-
await removeFromJsonFile('package.json', 'dependencies.vue', true)
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.error('Error removing \'vue\' dependency:\n', error.message)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (hasJsonKey('package.json', 'dependencies.vue-router')) {
|
|
73
|
-
try {
|
|
74
|
-
await removeFromJsonFile('package.json', 'dependencies.vue-router', true)
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error('Error removing \'vue-router\' dependency:\n', error.message)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (hasJsonKey('package.json', 'devDependencies.nuxt')) {
|
|
80
|
-
try {
|
|
81
|
-
await removeFromJsonFile('package.json', 'devDependencies.nuxt', true)
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error('Error removing \'nuxt\' devDependency:\n', error.message)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (hasJsonKey('package.json', 'devDependencies.vue')) {
|
|
87
|
-
try {
|
|
88
|
-
await removeFromJsonFile('package.json', 'devDependencies.vue', true)
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.error('Error removing \'vue\' devDependency:\n', error.message)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (hasJsonKey('package.json', 'devDependencies.vue-router')) {
|
|
94
|
-
try {
|
|
95
|
-
await removeFromJsonFile('package.json', 'devDependencies.vue-router', true)
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error('Error removing \'vue-router\' devDependency:\n', error.message)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 2) modify nuxt.config.ts
|
|
103
|
-
try {
|
|
104
|
-
await updateConfigFile('nuxt.config.ts', {
|
|
105
|
-
extends: [
|
|
106
|
-
'nuxt-spec',
|
|
107
|
-
],
|
|
108
|
-
}, isAutoRun, `This will add 'nuxt-spec' module to your 'nuxt.config.ts'. Continue?`)
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error('Error updating \'nuxt.config.ts\':\n', error.message)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 3) `pnpm-workspace.yaml` file (only if pnpm is used)
|
|
114
|
-
if (packageManager === 'pnpm') {
|
|
115
|
-
try {
|
|
116
|
-
if (pathExists('pnpm-workspace.yaml')) {
|
|
117
|
-
await updateTextFile('pnpm-workspace.yaml', ['shamefully-hoist: true'], isAutoRun, 'This will adjust \'pnpm-workspace.yaml\' file in your project. Continue?')
|
|
118
|
-
} else {
|
|
119
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/config/templates/pnpm-workspace.yaml.template`,
|
|
120
|
-
'pnpm-workspace.yaml', isAutoRun, 'This will add \'pnpm-workspace.yaml\' file for your project. Continue?')
|
|
121
|
-
}
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.error('Error setting up \'pnpm-workspace.yaml\':\n', error.message)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 4) create vitest.config.ts
|
|
128
|
-
try {
|
|
129
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/config/templates/vitest.config.ts.template`,
|
|
130
|
-
'vitest.config.ts', isAutoRun, 'This will create a new \'vitest.config.ts\' file for your project. Continue?')
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error('Error setting up \'vitest.config.ts\':\n', error.message)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 5) create .nuxtrc to prevent @nuxt/test-utils setup from running automatically on first start
|
|
136
|
-
if (!pathExists('.nuxtrc')) {
|
|
137
|
-
try {
|
|
138
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/.nuxtrc`, '.nuxtrc', isAutoRun, 'This will create a \'.nuxtrc\' file to prevent @nuxt/test-utils setup from running automatically when dev server starts. Continue?')
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error('Error creating \'.nuxtrc\':\n', error.message)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 6) modify package.json with test scripts
|
|
145
|
-
try {
|
|
146
|
-
await updateJsonFile('package.json', 'scripts', {
|
|
147
|
-
'test': 'vitest run',
|
|
148
|
-
'test-u': 'vitest run -u',
|
|
149
|
-
'test-i': 'vitest',
|
|
150
|
-
}, isAutoRun, 'This will adjust the test-related commands in your \'package.json\'. Continue?')
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error('Error adjusting scripts in \'package.json\':\n', error.message)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 7) create sample test files
|
|
156
|
-
const createSampleTests = isAutoRun || await promptUser('Do you want to create sample tests in \'/test\' folder?')
|
|
157
|
-
if (createSampleTests) {
|
|
158
|
-
try {
|
|
159
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/browser/vitest-browser.test.ts`,
|
|
160
|
-
'test/browser/vitest-browser.test.ts', true)
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.error('Error setting up \'vitest-browser.test.ts\':\n', error.message)
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/e2e/nuxt-e2e.test.ts`,
|
|
166
|
-
'test/e2e/nuxt-e2e.test.ts', true)
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error('Error setting up \'nuxt-e2e.test.ts\':\n', error.message)
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/e2e/nuxt-visual.test.ts`,
|
|
172
|
-
'test/e2e/nuxt-visual.test.ts', true)
|
|
173
|
-
} catch (error) {
|
|
174
|
-
console.error('Error setting up \'nuxt-visual.test.ts\':\n', error.message)
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/nuxt/nuxt-unit.test.ts`,
|
|
178
|
-
'test/nuxt/nuxt-unit.test.ts', true)
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.error('Error setting up \'nuxt-unit.test.ts\':\n', error.message)
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
await createFileFromWebTemplate(`https://raw.githubusercontent.com/AloisSeckar/nuxt-spec/refs/tags/v${TARGET_VERSION}/test/unit/vitest-unit.test.ts`,
|
|
184
|
-
'test/unit/vitest-unit.test.ts', true)
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.error('Error setting up \'vitest-unit.test.ts\':\n', error.message)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 8) clear node_modules and lock file(s)
|
|
191
|
-
const prepareForReinstall = isAutoRun || await promptUser('Dependencies should be re-installed now. Do you want to remove node_modules and the lock file?')
|
|
192
|
-
if (prepareForReinstall) {
|
|
193
|
-
if (pathExists('node_modules')) {
|
|
194
|
-
try {
|
|
195
|
-
await deletePath('node_modules', true)
|
|
196
|
-
} catch (error) {
|
|
197
|
-
console.error('Error deleting \'node_modules\':\n', error.message)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (pathExists('package-lock.json')) {
|
|
201
|
-
try {
|
|
202
|
-
await deletePath('package-lock.json', true)
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error('Error deleting \'package-lock.json\':\n', error.message)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (pathExists('pnpm-lock.yaml')) {
|
|
208
|
-
try {
|
|
209
|
-
await deletePath('pnpm-lock.yaml', true)
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.error('Error deleting \'pnpm-lock.yaml\':\n', error.message)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (pathExists('yarn.lock')) {
|
|
215
|
-
try {
|
|
216
|
-
await deletePath('yarn.lock', true)
|
|
217
|
-
} catch (error) {
|
|
218
|
-
console.error('Error deleting \'yarn.lock\':\n', error.message)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (pathExists('bun.lockb')) {
|
|
222
|
-
try {
|
|
223
|
-
await deletePath('bun.lockb', true)
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error('Error deleting \'bun.lockb\':\n', error.message)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (pathExists('deno.lock')) {
|
|
229
|
-
try {
|
|
230
|
-
await deletePath('deno.lock', true)
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error('Error deleting \'deno.lock\':\n', error.message)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 9) run install command
|
|
238
|
-
const runInstall = isAutoRun || await promptUser(`Fresh \`${packageManager} install\` is required. Do you want to run it now?`)
|
|
239
|
-
if (runInstall) {
|
|
240
|
-
try {
|
|
241
|
-
showMessage(`Running \`${packageManager} install\`...`)
|
|
242
|
-
execSync(`${packageManager} install`, { stdio: 'inherit' })
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.error(`Error running \`${packageManager} install\`:\n`, error.message)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 10) run Nuxt prepare command to generate types and auto-imports
|
|
249
|
-
const prepareCmd = getPrepareCmd(packageManager)
|
|
250
|
-
const runPrepare = isAutoRun || await promptUser(`Nuxt needs to generate types and auto-imports before the project is fully usable. Do you want to run \`${prepareCmd}\` now?`)
|
|
251
|
-
if (runPrepare) {
|
|
252
|
-
try {
|
|
253
|
-
showMessage(`Running \`${prepareCmd}\`...`)
|
|
254
|
-
execSync(prepareCmd, { stdio: 'inherit' })
|
|
255
|
-
} catch (error) {
|
|
256
|
-
console.error(`Error running \`${prepareCmd}\`:\n`, error.message)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 11) run Playwright browser install command
|
|
261
|
-
const playwrightInstallCmd = getPlaywrightInstallCmd(packageManager)
|
|
262
|
-
const runPlaywrightInstall = isAutoRun || await promptUser(`Playwright browser runtimes might need to be installed locally for e2e tests. Do you want to run \`${playwrightInstallCmd}\` now?`)
|
|
263
|
-
if (runPlaywrightInstall) {
|
|
264
|
-
try {
|
|
265
|
-
showMessage(`Running \`${playwrightInstallCmd}\`...`)
|
|
266
|
-
execSync(playwrightInstallCmd, { stdio: 'inherit' })
|
|
267
|
-
} catch (error) {
|
|
268
|
-
console.error(`Error running \`${playwrightInstallCmd}\`:\n`, error.message)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 12) inform user
|
|
273
|
-
showMessage('')
|
|
274
|
-
showMessage('NUXT SPEC SETUP COMPLETE', 2)
|
|
275
|
-
if (!runInstall) {
|
|
276
|
-
showMessage(`Proceed with \`${packageManager} install\` to install dependencies.`)
|
|
277
|
-
}
|
|
278
|
-
if (!runPrepare) {
|
|
279
|
-
showMessage(`Run \`${prepareCmd}\` to generate the required Nuxt types and auto-imports.`)
|
|
280
|
-
}
|
|
281
|
-
if (!runPlaywrightInstall) {
|
|
282
|
-
showMessage(`Run \`${playwrightInstallCmd}\` to setup the Playwright browser runtimes for e2e tests.`)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// force exit to prevent #20
|
|
286
|
-
process.exit(0)
|
|
287
|
-
}
|
package/bin/update.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
|
-
import {
|
|
5
|
-
hasJsonKey, promptUser, showMessage,
|
|
6
|
-
} from 'elrh-cosca'
|
|
7
|
-
import {
|
|
8
|
-
getPlaywrightInstallCmd, getPrepareCmd, getUpdateCmd,
|
|
9
|
-
} from './helpers/commands.js'
|
|
10
|
-
|
|
11
|
-
const TARGET_VERSION = '0.3.4'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* CLI tool to update existing `nuxt-spec` installation.
|
|
15
|
-
*
|
|
16
|
-
* It first asks whether to run in "auto" mode (no prompts, force = true) or "manual" mode (with prompts, force = false).
|
|
17
|
-
*
|
|
18
|
-
* Then it:
|
|
19
|
-
* 1) verifies `nuxt-spec` is present in `package.json` (fails with a hint to run `setup` otherwise)
|
|
20
|
-
* 2) runs the package manager's update command to bump `nuxt-spec` to the latest version
|
|
21
|
-
* 3) run the `nuxt prepare` command to regenerate types and auto-imports
|
|
22
|
-
* 4) run the `playwright install` command to ensure the Playwright browser runtimes are up to date
|
|
23
|
-
*
|
|
24
|
-
* @param {boolean} autoRun - Whether to run the update automatically without any prompts (defaults to false).
|
|
25
|
-
* @param {string} [packageManager] - Package manager to be used (`npm`, `pnpm`, `yarn`, `bun` or `deno`).
|
|
26
|
-
*/
|
|
27
|
-
export async function specUpdate(autoRun = false, packageManager) {
|
|
28
|
-
showMessage('NUXT SPEC UPDATE')
|
|
29
|
-
showMessage('This CLI tool will help you update Nuxt Spec in your project.')
|
|
30
|
-
showMessage('Refer to the documentation for more information.', 2)
|
|
31
|
-
|
|
32
|
-
// fail fast if there is nothing to update
|
|
33
|
-
const isInstalled = hasJsonKey('package.json', `dependencies.nuxt-spec`)
|
|
34
|
-
|| hasJsonKey('package.json', `devDependencies.nuxt-spec`)
|
|
35
|
-
if (!isInstalled) {
|
|
36
|
-
console.error(`'nuxt-spec' was not found in your 'package.json'. Run the 'setup' command first.`)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const isAutoRun = autoRun || await promptUser('Do you want to update everything automatically (no more prompts)?')
|
|
41
|
-
showMessage('')
|
|
42
|
-
|
|
43
|
-
// 1) run 'update nuxt-spec'
|
|
44
|
-
const updateCmd = getUpdateCmd(packageManager, `nuxt-spec@${TARGET_VERSION}`)
|
|
45
|
-
const runUpdate = isAutoRun || await promptUser(`This will bump 'nuxt-spec' to version '${TARGET_VERSION}' by running \`${updateCmd}\`. Continue?`)
|
|
46
|
-
if (runUpdate) {
|
|
47
|
-
try {
|
|
48
|
-
showMessage(`Running \`${updateCmd}\`...`)
|
|
49
|
-
execSync(updateCmd, { stdio: 'inherit' })
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error(`Error running \`${updateCmd}\`:\n`, error.message)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 2) run 'nuxt prepare'
|
|
56
|
-
const prepareCmd = getPrepareCmd(packageManager)
|
|
57
|
-
const runPrepare = isAutoRun || await promptUser(`Nuxt types and auto-imports should be regenerated after the update. Do you want to run \`${prepareCmd}\` now?`)
|
|
58
|
-
if (runPrepare) {
|
|
59
|
-
try {
|
|
60
|
-
showMessage(`Running \`${prepareCmd}\`...`)
|
|
61
|
-
execSync(prepareCmd, { stdio: 'inherit' })
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(`Error running \`${prepareCmd}\`:\n`, error.message)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 3) run 'playwright install'
|
|
68
|
-
const playwrightUpdateCmd = getPlaywrightInstallCmd(packageManager)
|
|
69
|
-
const runPlaywrightUpdate = isAutoRun || await promptUser(`Playwright browser runtimes might need to be updated for e2e tests. Do you want to run \`${playwrightUpdateCmd}\` now?`)
|
|
70
|
-
if (runPlaywrightUpdate) {
|
|
71
|
-
try {
|
|
72
|
-
showMessage(`Running \`${playwrightUpdateCmd}\`...`)
|
|
73
|
-
execSync(playwrightUpdateCmd, { stdio: 'inherit' })
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.error(`Error running \`${playwrightUpdateCmd}\`:\n`, error.message)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 4) inform user
|
|
80
|
-
showMessage('')
|
|
81
|
-
showMessage('NUXT SPEC UPDATE COMPLETE', 2)
|
|
82
|
-
if (!runUpdate) {
|
|
83
|
-
showMessage(`Run \`${updateCmd}\` to update 'nuxt-spec'.`)
|
|
84
|
-
}
|
|
85
|
-
if (!runPrepare) {
|
|
86
|
-
showMessage(`Run \`${prepareCmd}\` to regenerate the Nuxt types and auto-imports.`)
|
|
87
|
-
}
|
|
88
|
-
if (!runPlaywrightUpdate) {
|
|
89
|
-
showMessage(`Run \`${playwrightUpdateCmd}\` to update the Playwright browser runtimes for e2e tests.`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// force exit to prevent #20
|
|
93
|
-
process.exit(0)
|
|
94
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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 -->
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process'
|
|
2
|
-
import { appendFileSync, existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { platform } from 'node:os'
|
|
4
|
-
import { resolve, sep } from 'node:path'
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
-
import type { DecodedPng } from 'fast-png'
|
|
7
|
-
|
|
8
|
-
const templatesDir = resolve(fileURLToPath(import.meta.url), '..')
|
|
9
|
-
const REPORT_ENTRY = readFileSync(resolve(templatesDir, 'report-entry.html'), 'utf-8')
|
|
10
|
-
const REPORT_PATH = resolve(process.cwd(), '.nuxt/.nuxt-spec-report-path')
|
|
11
|
-
const REPORT_LOCK = resolve(process.cwd(), '.nuxt/.nuxt-spec-report-lock')
|
|
12
|
-
|
|
13
|
-
// create report file on first call
|
|
14
|
-
// protected from parallel execution issues
|
|
15
|
-
export function ensureReportCreated(targetDir = 'test/e2e'): void {
|
|
16
|
-
withConcurrentLock(() => {
|
|
17
|
-
let reportPath: string
|
|
18
|
-
|
|
19
|
-
if (!existsSync(targetDir)) {
|
|
20
|
-
throw new Error(`Target directory "${targetDir}" does not exist.`)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (existsSync(REPORT_PATH)) {
|
|
24
|
-
// report file was already created
|
|
25
|
-
reportPath = readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
26
|
-
// this shouldn't happen...
|
|
27
|
-
if (!existsSync(reportPath)) {
|
|
28
|
-
throw new Error(`Invalid data in "${REPORT_PATH}": Report file "${reportPath}" does not exist.`)
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
// for the first time, prepare and store new time-stamped file path
|
|
32
|
-
const fileTimestamp = process.env.SCREENSHOT_REPORT_TIMESTAMP ?? reportTimestamp(new Date())
|
|
33
|
-
reportPath = resolve(targetDir, '__current__', `report-${fileTimestamp}.html`)
|
|
34
|
-
writeFileSync(REPORT_PATH, reportPath)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// store the path for `appendToReport`
|
|
38
|
-
process.env.SCREENSHOT_REPORT_PATH = reportPath
|
|
39
|
-
|
|
40
|
-
// report file was already created
|
|
41
|
-
if (existsSync(reportPath)) return
|
|
42
|
-
|
|
43
|
-
// create new report file from template
|
|
44
|
-
const titleTimestamp = process.env.SCREENSHOT_REPORT_TITLE ?? reportTimestamp(new Date(), true)
|
|
45
|
-
const template = readFileSync(resolve(templatesDir, 'report-head.html'), 'utf-8')
|
|
46
|
-
const report = template.replace('{{TIMESTAMP}}', titleTimestamp)
|
|
47
|
-
writeFileSync(reportPath, report)
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// append a side-by-side baseline/actual comparison
|
|
52
|
-
// to the HTML report if the screenshots don't match
|
|
53
|
-
// protected from parallel execution issues
|
|
54
|
-
export function appendToReport(fileName: string, message: string, baseline: Uint8Array, actual: Uint8Array): void {
|
|
55
|
-
const baselineUri = `data:image/png;base64,${Buffer.from(baseline).toString('base64')}`
|
|
56
|
-
const actualUri = `data:image/png;base64,${Buffer.from(actual).toString('base64')}`
|
|
57
|
-
|
|
58
|
-
const entry = REPORT_ENTRY
|
|
59
|
-
.replace('{{FILE_NAME}}', escapeHtml(fileName))
|
|
60
|
-
.replace('{{MESSAGE}}', escapeHtml(message))
|
|
61
|
-
.replace('{{BASELINE_URI}}', baselineUri)
|
|
62
|
-
.replace('{{ACTUAL_URI}}', actualUri)
|
|
63
|
-
|
|
64
|
-
withConcurrentLock(() => {
|
|
65
|
-
const reportPath = process.env.SCREENSHOT_REPORT_PATH || (existsSync(REPORT_PATH)
|
|
66
|
-
? readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
67
|
-
: '')
|
|
68
|
-
if (!reportPath || !existsSync(reportPath)) return
|
|
69
|
-
|
|
70
|
-
appendFileSync(reportPath, entry)
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Vitest globalSetup entry point
|
|
75
|
-
// - computes stable timestamp values and exposes them via env variables
|
|
76
|
-
// - the report file itself is created lazily on first compareScreenshot call
|
|
77
|
-
// - provides a callback to close the HTML report once tests are finished (if it was created)
|
|
78
|
-
export function screenshotSetup() {
|
|
79
|
-
const NOW = new Date()
|
|
80
|
-
const fileTimestamp = reportTimestamp(NOW)
|
|
81
|
-
const titleTimestamp = reportTimestamp(NOW, true)
|
|
82
|
-
|
|
83
|
-
process.env.SCREENSHOT_REPORT_TIMESTAMP = fileTimestamp
|
|
84
|
-
process.env.SCREENSHOT_REPORT_TITLE = titleTimestamp
|
|
85
|
-
|
|
86
|
-
// callback that is executed upon Vitest teardown phase
|
|
87
|
-
return () => {
|
|
88
|
-
if (!existsSync(REPORT_PATH)) return
|
|
89
|
-
|
|
90
|
-
const reportPath = readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
91
|
-
unlinkSync(REPORT_PATH)
|
|
92
|
-
|
|
93
|
-
if (!reportPath) return
|
|
94
|
-
if (!existsSync(reportPath)) return
|
|
95
|
-
|
|
96
|
-
// add "success" / "error" message as a conclusion
|
|
97
|
-
const reportBody = readFileSync(reportPath, 'utf-8')
|
|
98
|
-
const hasFailure = reportBody.includes('<div class="failure"')
|
|
99
|
-
if (hasFailure) {
|
|
100
|
-
appendFileSync(reportPath, '<p style="color: #b00; font-weight: 700; font-size: 18px;">Tests finished with errors</p>\n')
|
|
101
|
-
} else {
|
|
102
|
-
appendFileSync(reportPath, '<p style="color: #0b0; font-weight: 700; font-size: 18px;">All tests have passed</p>\n')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// wrap the report up with a footer
|
|
106
|
-
let footer = readFileSync(resolve(templatesDir, 'report-tail.html'), 'utf-8')
|
|
107
|
-
footer = footer.replace('{{TIMESTAMP}}', new Date().toISOString())
|
|
108
|
-
appendFileSync(reportPath, footer)
|
|
109
|
-
console.log(`\n(nuxt-spec) Visual regression report available at:\n${pathToFileURL(reportPath).href}`)
|
|
110
|
-
|
|
111
|
-
if (!process.env.CI) {
|
|
112
|
-
console.log('(nuxt-spec) Opening report in default browser...')
|
|
113
|
-
const openCmd
|
|
114
|
-
= platform() === 'darwin'
|
|
115
|
-
? `open "${reportPath}"`
|
|
116
|
-
: platform() === 'win32'
|
|
117
|
-
? `start "" "${reportPath}"`
|
|
118
|
-
: `xdg-open "${reportPath}"`
|
|
119
|
-
exec(openCmd, (err) => {
|
|
120
|
-
if (err) {
|
|
121
|
-
console.log('(nuxt-spec) Failed to automatically open report')
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.log('\n')
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// helper to keep user-provided targetDir inside the current project root
|
|
131
|
-
export function resolveWithin(base: string, segment: string): string {
|
|
132
|
-
const target = resolve(base, segment)
|
|
133
|
-
if (target !== base && !target.startsWith(base + sep)) {
|
|
134
|
-
throw new Error(`Invalid path: "${segment}" resolves outside of "${base}"`)
|
|
135
|
-
}
|
|
136
|
-
return target
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
140
|
-
export function toRGBA(img: DecodedPng): Uint8Array {
|
|
141
|
-
const { width, height, data, channels = 4 } = img
|
|
142
|
-
if (channels === 4) return data as Uint8Array
|
|
143
|
-
const pixels = width * height
|
|
144
|
-
const rgba = new Uint8Array(pixels * 4)
|
|
145
|
-
for (let i = 0; i < pixels; i++) {
|
|
146
|
-
const src = i * 3
|
|
147
|
-
rgba[i * 4 + 0] = data[src + 0] ?? 0
|
|
148
|
-
rgba[i * 4 + 1] = data[src + 1] ?? 0
|
|
149
|
-
rgba[i * 4 + 2] = data[src + 2] ?? 0
|
|
150
|
-
rgba[i * 4 + 3] = 255
|
|
151
|
-
}
|
|
152
|
-
return rgba
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// protection from parallel execution issues
|
|
156
|
-
function withConcurrentLock<T>(fn: () => T): T {
|
|
157
|
-
const start = Date.now()
|
|
158
|
-
|
|
159
|
-
while (true) {
|
|
160
|
-
try {
|
|
161
|
-
// try acquiring the lock
|
|
162
|
-
writeFileSync(REPORT_LOCK, String(process.pid), { flag: 'wx' })
|
|
163
|
-
break
|
|
164
|
-
} catch (error) {
|
|
165
|
-
// we only expect "file already exists" error
|
|
166
|
-
// otherwise it is an issue
|
|
167
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
168
|
-
if (code !== 'EEXIST') throw error
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const lockAge = Date.now() - statSync(REPORT_LOCK).mtimeMs
|
|
172
|
-
if (lockAge > 30_000) {
|
|
173
|
-
// stale lock
|
|
174
|
-
unlinkSync(REPORT_LOCK)
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// lock disappeared between checks, retry acquisition
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (Date.now() - start > 10_000) {
|
|
182
|
-
// not successful in 10 minutes, give up
|
|
183
|
-
throw new Error('Timed out while waiting for screenshot report lock', {
|
|
184
|
-
cause: error,
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// sleep before next attempt
|
|
189
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
return fn()
|
|
195
|
-
} finally {
|
|
196
|
-
try {
|
|
197
|
-
unlinkSync(REPORT_LOCK)
|
|
198
|
-
} catch {
|
|
199
|
-
// lock file may already be removed
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// timestamp string
|
|
205
|
-
// separators=false produces YYYYMMDDHHMMSS for report file name
|
|
206
|
-
// separators=true produces YYYY-MM-DD HH:MM:SS for report title
|
|
207
|
-
function reportTimestamp(date: Date, separators = false): string {
|
|
208
|
-
const pad2 = (n: number) => String(n).padStart(2, '0')
|
|
209
|
-
const dateSeparator = separators ? '-' : ''
|
|
210
|
-
const midSeparator = separators ? ' ' : ''
|
|
211
|
-
const timeSeparator = separators ? ':' : ''
|
|
212
|
-
return [
|
|
213
|
-
date.getFullYear(),
|
|
214
|
-
dateSeparator,
|
|
215
|
-
pad2(date.getMonth() + 1),
|
|
216
|
-
dateSeparator,
|
|
217
|
-
pad2(date.getDate()),
|
|
218
|
-
midSeparator,
|
|
219
|
-
pad2(date.getHours()),
|
|
220
|
-
timeSeparator,
|
|
221
|
-
pad2(date.getMinutes()),
|
|
222
|
-
timeSeparator,
|
|
223
|
-
pad2(date.getSeconds()),
|
|
224
|
-
].join('')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// helper to escape a string for safe interpolation into the HTML report
|
|
228
|
-
function escapeHtml(value: string): string {
|
|
229
|
-
return value.replace(/[&<>"']/g, char => ({
|
|
230
|
-
'&': '&',
|
|
231
|
-
'<': '<',
|
|
232
|
-
'>': '>',
|
|
233
|
-
'"': '"',
|
|
234
|
-
'\'': ''',
|
|
235
|
-
}[char] ?? char))
|
|
236
|
-
}
|