nuxt-spec 0.2.4-alpha → 0.2.4-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.
- package/README.md +24 -0
- package/app/components/index.d.ts +7 -0
- package/config/index.mjs +1 -1
- package/package.json +4 -2
- package/utils/index.ts +2 -2
- package/utils/screenshot/report-utils.ts +211 -0
- package/utils/{screenshot-compare.ts → screenshot.ts} +15 -62
- package/utils/screenshot-report.ts +0 -116
- /package/utils/{html → screenshot}/report-entry.html +0 -0
- /package/utils/{html → screenshot}/report-head.html +0 -0
- /package/utils/{html → screenshot}/report-tail.html +0 -0
package/README.md
CHANGED
|
@@ -384,6 +384,30 @@ For detailed description, see [utils.d.ts](https://github.com/AloisSeckar/nuxt-s
|
|
|
384
384
|
|
|
385
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).
|
|
386
386
|
|
|
387
|
+
### Notice on non-default setups
|
|
388
|
+
|
|
389
|
+
The creation of the report file and it's proper wrap-up at the end is ensured via `globalSetup` function passed into default Vitest E2E suite defined by Nuxt Spec. If you need to override the default `e2e` project, you also need to make sure to call the setup function manually.
|
|
390
|
+
|
|
391
|
+
Add following into your `vitest.config.ts`:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// vitest.config.ts
|
|
395
|
+
import { loadVitestConfig } from 'nuxt-spec/config'
|
|
396
|
+
|
|
397
|
+
// resolve path to Nuxt Spec's setup function
|
|
398
|
+
const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot.ts', import.meta.resolve('nuxt-spec/config')))
|
|
399
|
+
|
|
400
|
+
export default loadVitestConfig({
|
|
401
|
+
// whatever e2e test you are defining
|
|
402
|
+
test: {
|
|
403
|
+
// provide it to your `compareScreenshot` using test suite
|
|
404
|
+
globalSetup: [screenshotReportSetup],
|
|
405
|
+
// other config
|
|
406
|
+
},
|
|
407
|
+
// other config
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
387
411
|
## Contact
|
|
388
412
|
|
|
389
413
|
Use GitHub issues to report bugs or suggest improvements. I will be more than happy to address them.
|
package/config/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { playwright } from '@vitest/browser-playwright'
|
|
|
11
11
|
import vue from '@vitejs/plugin-vue'
|
|
12
12
|
|
|
13
13
|
// absolute path so it works from nuxt-ignis package
|
|
14
|
-
const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot
|
|
14
|
+
const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot.ts', import.meta.url))
|
|
15
15
|
|
|
16
16
|
export async function loadVitestConfig(userVitestConfig, projects = true) {
|
|
17
17
|
const baseConfig = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-spec",
|
|
3
|
-
"version": "0.2.4-alpha",
|
|
3
|
+
"version": "0.2.4-alpha.3",
|
|
4
4
|
"description": "Test-pack layer for Nuxt Applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"analyze": "nuxt analyze",
|
|
14
14
|
"eslint": "eslint .",
|
|
15
|
+
"typecheck": "nuxi typecheck",
|
|
15
16
|
"build": "pnpm test && nuxt build",
|
|
16
17
|
"dev": "nuxt dev",
|
|
17
18
|
"generate": "nuxt generate",
|
|
@@ -68,7 +69,8 @@
|
|
|
68
69
|
"vitest": "4.1.9",
|
|
69
70
|
"vitest-browser-vue": "2.1.0",
|
|
70
71
|
"vue": "~3.5.39",
|
|
71
|
-
"vue-router": "~5.1.0"
|
|
72
|
+
"vue-router": "~5.1.0",
|
|
73
|
+
"vue-tsc": "~3.3.6"
|
|
72
74
|
},
|
|
73
75
|
"packageManager": "pnpm@11.9.0"
|
|
74
76
|
}
|
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'
|
|
3
|
+
import type { CompareScreenshotOptions } from './screenshot'
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
compareScreenshot,
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { platform } from 'node:os'
|
|
4
|
+
import { resolve, sep } from 'node:path'
|
|
5
|
+
import { fileURLToPath } 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(REPORT_PATH)) {
|
|
20
|
+
// Keep exactly one shared report per test run; first writer wins.
|
|
21
|
+
reportPath = readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
22
|
+
} else {
|
|
23
|
+
const root = process.cwd()
|
|
24
|
+
const dir = resolveWithin(root, targetDir)
|
|
25
|
+
const fileTimestamp = process.env.SCREENSHOT_REPORT_TIMESTAMP ?? reportTimestamp(new Date())
|
|
26
|
+
reportPath = resolve(dir, '__current__', `report-${fileTimestamp}.html`)
|
|
27
|
+
writeFileSync(REPORT_PATH, reportPath)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.env.SCREENSHOT_REPORT_PATH = reportPath
|
|
31
|
+
if (!reportPath || existsSync(reportPath)) return
|
|
32
|
+
|
|
33
|
+
const titleTimestamp = process.env.SCREENSHOT_REPORT_TITLE ?? reportTimestamp(new Date(), true)
|
|
34
|
+
mkdirSync(resolve(reportPath, '..'), { recursive: true })
|
|
35
|
+
|
|
36
|
+
const template = readFileSync(resolve(templatesDir, 'report-head.html'), 'utf-8')
|
|
37
|
+
const report = template.replace('{{TIMESTAMP}}', titleTimestamp)
|
|
38
|
+
writeFileSync(reportPath, report)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// append a side-by-side baseline/actual comparison
|
|
43
|
+
// to the HTML report if the screenshots don't match
|
|
44
|
+
// protected from parallel execution issues
|
|
45
|
+
export function appendToReport(fileName: string, message: string, baseline: Uint8Array, actual: Uint8Array): void {
|
|
46
|
+
const baselineUri = `data:image/png;base64,${Buffer.from(baseline).toString('base64')}`
|
|
47
|
+
const actualUri = `data:image/png;base64,${Buffer.from(actual).toString('base64')}`
|
|
48
|
+
|
|
49
|
+
const entry = REPORT_ENTRY
|
|
50
|
+
.replace('{{FILE_NAME}}', escapeHtml(fileName))
|
|
51
|
+
.replace('{{MESSAGE}}', escapeHtml(message))
|
|
52
|
+
.replace('{{BASELINE_URI}}', baselineUri)
|
|
53
|
+
.replace('{{ACTUAL_URI}}', actualUri)
|
|
54
|
+
|
|
55
|
+
withConcurrentLock(() => {
|
|
56
|
+
const reportPath = process.env.SCREENSHOT_REPORT_PATH || (existsSync(REPORT_PATH)
|
|
57
|
+
? readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
58
|
+
: '')
|
|
59
|
+
if (!reportPath || !existsSync(reportPath)) return
|
|
60
|
+
|
|
61
|
+
appendFileSync(reportPath, entry)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Vitest globalSetup entry point
|
|
66
|
+
// - computes stable timestamp values and exposes them via env variables
|
|
67
|
+
// - the report file itself is created lazily on first compareScreenshot call
|
|
68
|
+
// - provides a callback to close the HTML report once tests are finished (if it was created)
|
|
69
|
+
export function screenshotSetup() {
|
|
70
|
+
const NOW = new Date()
|
|
71
|
+
const fileTimestamp = reportTimestamp(NOW)
|
|
72
|
+
const titleTimestamp = reportTimestamp(NOW, true)
|
|
73
|
+
|
|
74
|
+
process.env.SCREENSHOT_REPORT_TIMESTAMP = fileTimestamp
|
|
75
|
+
process.env.SCREENSHOT_REPORT_TITLE = titleTimestamp
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
if (!existsSync(REPORT_PATH)) return
|
|
79
|
+
|
|
80
|
+
const reportPath = readFileSync(REPORT_PATH, 'utf-8').trim()
|
|
81
|
+
unlinkSync(REPORT_PATH)
|
|
82
|
+
|
|
83
|
+
if (!reportPath) return
|
|
84
|
+
if (!existsSync(reportPath)) return
|
|
85
|
+
|
|
86
|
+
let footer = readFileSync(resolve(templatesDir, 'report-tail.html'), 'utf-8')
|
|
87
|
+
footer = footer.replace('{{TIMESTAMP}}', new Date().toISOString())
|
|
88
|
+
appendFileSync(reportPath, footer)
|
|
89
|
+
console.log(`\n(nuxt-spec) Visual regression report available at:\nfile://${reportPath}`)
|
|
90
|
+
|
|
91
|
+
if (!process.env.CI) {
|
|
92
|
+
console.log('(nuxt-spec) Opening report in default browser...')
|
|
93
|
+
const openCmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open'
|
|
94
|
+
exec(`${openCmd} "${reportPath}"`, (err) => {
|
|
95
|
+
if (err) {
|
|
96
|
+
console.log('(nuxt-spec) Failed to automatically open report')
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('\n')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// helper to keep user-provided targetDir inside the current project root
|
|
106
|
+
export function resolveWithin(base: string, segment: string): string {
|
|
107
|
+
const target = resolve(base, segment)
|
|
108
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
109
|
+
throw new Error(`Invalid path: "${segment}" resolves outside of "${base}"`)
|
|
110
|
+
}
|
|
111
|
+
return target
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
115
|
+
export function toRGBA(img: DecodedPng): Uint8Array {
|
|
116
|
+
const { width, height, data, channels = 4 } = img
|
|
117
|
+
if (channels === 4) return data as Uint8Array
|
|
118
|
+
const pixels = width * height
|
|
119
|
+
const rgba = new Uint8Array(pixels * 4)
|
|
120
|
+
for (let i = 0; i < pixels; i++) {
|
|
121
|
+
const src = i * 3
|
|
122
|
+
rgba[i * 4 + 0] = data[src + 0] ?? 0
|
|
123
|
+
rgba[i * 4 + 1] = data[src + 1] ?? 0
|
|
124
|
+
rgba[i * 4 + 2] = data[src + 2] ?? 0
|
|
125
|
+
rgba[i * 4 + 3] = 255
|
|
126
|
+
}
|
|
127
|
+
return rgba
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// protection from parallel execution issues
|
|
131
|
+
function withConcurrentLock<T>(fn: () => T): T {
|
|
132
|
+
const start = Date.now()
|
|
133
|
+
|
|
134
|
+
while (true) {
|
|
135
|
+
try {
|
|
136
|
+
// try acquiring the lock
|
|
137
|
+
writeFileSync(REPORT_LOCK, String(process.pid), { flag: 'wx' })
|
|
138
|
+
break
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// we only expect "file already exists" error
|
|
141
|
+
// otherwise it is an issue
|
|
142
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
143
|
+
if (code !== 'EEXIST') throw error
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const lockAge = Date.now() - statSync(REPORT_LOCK).mtimeMs
|
|
147
|
+
if (lockAge > 30_000) {
|
|
148
|
+
// stale lock
|
|
149
|
+
unlinkSync(REPORT_LOCK)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// lock disappeared between checks, retry acquisition
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (Date.now() - start > 10_000) {
|
|
157
|
+
// not successful in 10 minutes, give up
|
|
158
|
+
throw new Error('Timed out while waiting for screenshot report lock', {
|
|
159
|
+
cause: error,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// sleep before next attempt
|
|
164
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
return fn()
|
|
170
|
+
} finally {
|
|
171
|
+
try {
|
|
172
|
+
unlinkSync(REPORT_LOCK)
|
|
173
|
+
} catch {
|
|
174
|
+
// lock file may already be removed
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// timestamp string
|
|
180
|
+
// separators=false produces YYYYMMDDHHMMSS for report file name
|
|
181
|
+
// separators=true produces YYYY-MM-DD HH:MM:SS for report title
|
|
182
|
+
function reportTimestamp(date: Date, separators = false): string {
|
|
183
|
+
const pad2 = (n: number) => String(n).padStart(2, '0')
|
|
184
|
+
const dateSeparator = separators ? '-' : ''
|
|
185
|
+
const midSeparator = separators ? ' ' : ''
|
|
186
|
+
const timeSeparator = separators ? ':' : ''
|
|
187
|
+
return [
|
|
188
|
+
date.getFullYear(),
|
|
189
|
+
dateSeparator,
|
|
190
|
+
pad2(date.getMonth() + 1),
|
|
191
|
+
dateSeparator,
|
|
192
|
+
pad2(date.getDate()),
|
|
193
|
+
midSeparator,
|
|
194
|
+
pad2(date.getHours()),
|
|
195
|
+
timeSeparator,
|
|
196
|
+
pad2(date.getMinutes()),
|
|
197
|
+
timeSeparator,
|
|
198
|
+
pad2(date.getSeconds()),
|
|
199
|
+
].join('')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// helper to escape a string for safe interpolation into the HTML report
|
|
203
|
+
function escapeHtml(value: string): string {
|
|
204
|
+
return value.replace(/[&<>"']/g, char => ({
|
|
205
|
+
'&': '&',
|
|
206
|
+
'<': '<',
|
|
207
|
+
'>': '>',
|
|
208
|
+
'"': '"',
|
|
209
|
+
'\'': ''',
|
|
210
|
+
}[char] ?? char))
|
|
211
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve
|
|
3
|
-
import {
|
|
4
|
-
import { decode, type DecodedPng } from 'fast-png'
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { decode } from 'fast-png'
|
|
5
4
|
import { expect } from 'vitest'
|
|
6
|
-
import { ensureReportCreated,
|
|
5
|
+
import { appendToReport, ensureReportCreated, resolveWithin, screenshotSetup, toRGBA } from './screenshot/report-utils'
|
|
7
6
|
import pixelmatch from 'pixelmatch'
|
|
8
7
|
import type { NuxtPage } from '@nuxt/test-utils'
|
|
9
8
|
|
|
@@ -24,14 +23,14 @@ export interface CompareScreenshotOptions {
|
|
|
24
23
|
|
|
25
24
|
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
26
25
|
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
27
|
-
// create report file on first call
|
|
28
|
-
ensureReportCreated()
|
|
29
|
-
|
|
30
26
|
const root = process.cwd()
|
|
31
27
|
|
|
32
28
|
// ensure the target directory stays within the project root
|
|
33
29
|
const dir = resolveWithin(root, options?.targetDir ?? 'test/e2e')
|
|
34
30
|
|
|
31
|
+
// create report file on first call
|
|
32
|
+
ensureReportCreated(dir)
|
|
33
|
+
|
|
35
34
|
const baselineDir = resolve(dir, '__baseline__')
|
|
36
35
|
const currentDir = resolve(dir, '__current__')
|
|
37
36
|
|
|
@@ -94,58 +93,12 @@ export async function compareScreenshot(page: NuxtPage, options?: CompareScreens
|
|
|
94
93
|
return true
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
138
|
-
// helper for bridging difference between Vitest PNG saving and fast-png encoding
|
|
139
|
-
function toRGBA(img: DecodedPng): Uint8Array {
|
|
140
|
-
const { width, height, data, channels = 4 } = img
|
|
141
|
-
if (channels === 4) return data as Uint8Array
|
|
142
|
-
const pixels = width * height
|
|
143
|
-
const rgba = new Uint8Array(pixels * 4)
|
|
144
|
-
for (let i = 0; i < pixels; i++) {
|
|
145
|
-
rgba[i * 4] = data[i * 3]
|
|
146
|
-
rgba[i * 4 + 1] = data[i * 3 + 1]
|
|
147
|
-
rgba[i * 4 + 2] = data[i * 3 + 2]
|
|
148
|
-
rgba[i * 4 + 3] = 255
|
|
149
|
-
}
|
|
150
|
-
return rgba
|
|
96
|
+
// Vitest globalSetup entry point
|
|
97
|
+
// - computes stable timestamp values and exposes them via env variables
|
|
98
|
+
// - the report file itself is created lazily on first compareScreenshot call
|
|
99
|
+
// - provides a callback to close the HTML report once tests are finished (if it was created)
|
|
100
|
+
export default function setup() {
|
|
101
|
+
// the function itself is defined in the helper file
|
|
102
|
+
// to avoid imports here and there
|
|
103
|
+
return screenshotSetup()
|
|
151
104
|
}
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|