nuxt-spec 0.2.3 → 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/LICENSE +21 -21
- package/README.md +389 -387
- package/app/app.vue +10 -10
- package/app/components/NuxtSpecApiTestComponent.vue +24 -24
- package/app/components/NuxtSpecTestComponent.vue +9 -9
- package/app/components/index.ts +2 -2
- package/app/utils/vitest-utils.ts +5 -5
- package/bin/cli.js +53 -53
- package/bin/prepublish-check.js +16 -0
- package/bin/setup.js +251 -268
- package/config/index.d.ts +17 -17
- package/config/index.mjs +85 -79
- package/config/templates/pnpm-workspace.yaml.template +4 -4
- package/config/templates/vitest.config.ts.template +5 -5
- package/config/utils/merge.mjs +43 -43
- package/config/utils/warnings.mjs +33 -33
- package/nuxt.config.ts +19 -19
- package/package.json +31 -29
- package/utils/e2e.ts +30 -30
- 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.d.ts +70 -70
- package/utils/index.ts +12 -12
- package/utils/{screenshot.ts → screenshot-compare.ts} +151 -89
- package/utils/screenshot-report.ts +116 -0
package/utils/index.d.ts
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import type { NuxtPage } from '@nuxt/test-utils'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Visit a specified URL and return the page instance for further interaction.
|
|
5
|
-
*
|
|
6
|
-
* @param pageName - Path segment appended to the base URL (e.g. `'about'` → `/<about>`)
|
|
7
|
-
* @returns Playwright page instance after navigation and hydration
|
|
8
|
-
*/
|
|
9
|
-
export declare function gotoPage(pageName: string): Promise<NuxtPage>
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Extract inner HTML content from a specified element on a given page.
|
|
13
|
-
*
|
|
14
|
-
* @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
|
|
15
|
-
* @param element - CSS selector identifying the target element
|
|
16
|
-
* @returns The inner HTML of the matched element
|
|
17
|
-
*/
|
|
18
|
-
export declare function getDataHtml(page: NuxtPage | string, element: string): Promise<string>
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Execute an API call by clicking a trigger element, wait for a successful
|
|
22
|
-
* response matching the target URL, then extract the inner HTML from the
|
|
23
|
-
* response element.
|
|
24
|
-
*
|
|
25
|
-
* @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
|
|
26
|
-
* @param triggerElement - CSS selector for the clickable element that triggers the API request
|
|
27
|
-
* @param targetUrl - Substring matched against the response URL to identify the expected API call
|
|
28
|
-
* @param responseElement - CSS selector for the element displaying the API response
|
|
29
|
-
* @returns The inner HTML of the response element
|
|
30
|
-
*/
|
|
31
|
-
export declare function getAPIResultHtml(
|
|
32
|
-
page: NuxtPage | string,
|
|
33
|
-
triggerElement: string,
|
|
34
|
-
targetUrl: string,
|
|
35
|
-
responseElement: string,
|
|
36
|
-
): Promise<string>
|
|
37
|
-
|
|
38
|
-
export interface CompareScreenshotOptions {
|
|
39
|
-
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
40
|
-
fileName?: string
|
|
41
|
-
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
42
|
-
targetDir?: string
|
|
43
|
-
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
44
|
-
selector?: string
|
|
45
|
-
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
46
|
-
maxDiffPixelRatio?: number
|
|
47
|
-
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
48
|
-
maxDiffPixels?: number
|
|
49
|
-
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
50
|
-
threshold?: number
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Capture a browser screenshot and compare it against a stored baseline PNG.
|
|
55
|
-
* When run with `-u` / `--update`, or when no baseline exists yet, the current
|
|
56
|
-
* screenshot is saved as the new baseline.
|
|
57
|
-
*
|
|
58
|
-
* Comparison uses pixelmatch for perceptual pixel diffing. By default,
|
|
59
|
-
* zero differing pixels are allowed (exact match). Set `maxDiffPixelRatio`
|
|
60
|
-
* or `maxDiffPixels` to tolerate cross-platform rendering differences.
|
|
61
|
-
*
|
|
62
|
-
* @param page - Playwright page instance obtained from `createPage()`
|
|
63
|
-
* @param options - extra options (see `CompareScreenshotOptions`)
|
|
64
|
-
* @returns `true` when the screenshot matches the baseline (or a new baseline was saved)
|
|
65
|
-
* @throws Fails the current Vitest test when a mismatch is detected
|
|
66
|
-
*/
|
|
67
|
-
export declare function compareScreenshot(
|
|
68
|
-
page: NuxtPage,
|
|
69
|
-
options?: CompareScreenshotOptions,
|
|
70
|
-
): Promise<boolean>
|
|
1
|
+
import type { NuxtPage } from '@nuxt/test-utils'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visit a specified URL and return the page instance for further interaction.
|
|
5
|
+
*
|
|
6
|
+
* @param pageName - Path segment appended to the base URL (e.g. `'about'` → `/<about>`)
|
|
7
|
+
* @returns Playwright page instance after navigation and hydration
|
|
8
|
+
*/
|
|
9
|
+
export declare function gotoPage(pageName: string): Promise<NuxtPage>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract inner HTML content from a specified element on a given page.
|
|
13
|
+
*
|
|
14
|
+
* @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
|
|
15
|
+
* @param element - CSS selector identifying the target element
|
|
16
|
+
* @returns The inner HTML of the matched element
|
|
17
|
+
*/
|
|
18
|
+
export declare function getDataHtml(page: NuxtPage | string, element: string): Promise<string>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute an API call by clicking a trigger element, wait for a successful
|
|
22
|
+
* response matching the target URL, then extract the inner HTML from the
|
|
23
|
+
* response element.
|
|
24
|
+
*
|
|
25
|
+
* @param page - Playwright page instance, or a page name string (will call `gotoPage` internally)
|
|
26
|
+
* @param triggerElement - CSS selector for the clickable element that triggers the API request
|
|
27
|
+
* @param targetUrl - Substring matched against the response URL to identify the expected API call
|
|
28
|
+
* @param responseElement - CSS selector for the element displaying the API response
|
|
29
|
+
* @returns The inner HTML of the response element
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAPIResultHtml(
|
|
32
|
+
page: NuxtPage | string,
|
|
33
|
+
triggerElement: string,
|
|
34
|
+
targetUrl: string,
|
|
35
|
+
responseElement: string,
|
|
36
|
+
): Promise<string>
|
|
37
|
+
|
|
38
|
+
export interface CompareScreenshotOptions {
|
|
39
|
+
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
40
|
+
fileName?: string
|
|
41
|
+
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
42
|
+
targetDir?: string
|
|
43
|
+
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
44
|
+
selector?: string
|
|
45
|
+
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
46
|
+
maxDiffPixelRatio?: number
|
|
47
|
+
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
48
|
+
maxDiffPixels?: number
|
|
49
|
+
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
50
|
+
threshold?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Capture a browser screenshot and compare it against a stored baseline PNG.
|
|
55
|
+
* When run with `-u` / `--update`, or when no baseline exists yet, the current
|
|
56
|
+
* screenshot is saved as the new baseline.
|
|
57
|
+
*
|
|
58
|
+
* Comparison uses pixelmatch for perceptual pixel diffing. By default,
|
|
59
|
+
* zero differing pixels are allowed (exact match). Set `maxDiffPixelRatio`
|
|
60
|
+
* or `maxDiffPixels` to tolerate cross-platform rendering differences.
|
|
61
|
+
*
|
|
62
|
+
* @param page - Playwright page instance obtained from `createPage()`
|
|
63
|
+
* @param options - extra options (see `CompareScreenshotOptions`)
|
|
64
|
+
* @returns `true` when the screenshot matches the baseline (or a new baseline was saved)
|
|
65
|
+
* @throws Fails the current Vitest test when a mismatch is detected
|
|
66
|
+
*/
|
|
67
|
+
export declare function compareScreenshot(
|
|
68
|
+
page: NuxtPage,
|
|
69
|
+
options?: CompareScreenshotOptions,
|
|
70
|
+
): Promise<boolean>
|
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-compare'
|
|
3
|
+
import type { CompareScreenshotOptions } from './screenshot-compare'
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
compareScreenshot,
|
|
7
|
+
gotoPage,
|
|
8
|
+
getDataHtml,
|
|
9
|
+
getAPIResultHtml,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type { CompareScreenshotOptions }
|
|
@@ -1,89 +1,151 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
import { decode, type DecodedPng } from 'fast-png'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/** Max
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve, sep } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { decode, type DecodedPng } from 'fast-png'
|
|
5
|
+
import { expect } from 'vitest'
|
|
6
|
+
import { ensureReportCreated, getInjection } from './screenshot-report'
|
|
7
|
+
import pixelmatch from 'pixelmatch'
|
|
8
|
+
import type { NuxtPage } from '@nuxt/test-utils'
|
|
9
|
+
|
|
10
|
+
export interface CompareScreenshotOptions {
|
|
11
|
+
/** Name of the PNG file used for baseline storage and comparison (defaults to route and `index.png` for `/`) */
|
|
12
|
+
fileName?: string
|
|
13
|
+
/** Directory for baseline/current screenshots, relative to project root (defaults to `test/e2e`) */
|
|
14
|
+
targetDir?: string
|
|
15
|
+
/** CSS selector for a specific element to capture (defaults to full page) */
|
|
16
|
+
selector?: string
|
|
17
|
+
/** Max ratio of different pixels (0–1). Default: 0 (exact match) */
|
|
18
|
+
maxDiffPixelRatio?: number
|
|
19
|
+
/** Max absolute number of different pixels. Takes precedence over `maxDiffPixelRatio` when set. Default: 0 (exact match) */
|
|
20
|
+
maxDiffPixels?: number
|
|
21
|
+
/** Per-pixel color distance threshold (0–1). Lower = stricter. Default: 0.1 */
|
|
22
|
+
threshold?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// capture a browser screenshot and compare it against a stored baseline PNG
|
|
26
|
+
export async function compareScreenshot(page: NuxtPage, options?: CompareScreenshotOptions): Promise<boolean> {
|
|
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
|
+
|
|
35
|
+
const baselineDir = resolve(dir, '__baseline__')
|
|
36
|
+
const currentDir = resolve(dir, '__current__')
|
|
37
|
+
|
|
38
|
+
const route = page.url().substring(page.url().lastIndexOf('/') + 1) || 'index'
|
|
39
|
+
const fileName = options?.fileName ?? `${route}.png`
|
|
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
|
+
|
|
50
|
+
// capture element specified by locator or a full-page screenshot as PNG
|
|
51
|
+
const screenshot = options?.selector
|
|
52
|
+
? await page.locator(options.selector).screenshot()
|
|
53
|
+
: await page.screenshot({ fullPage: true })
|
|
54
|
+
|
|
55
|
+
// always save the current screenshot for inspection
|
|
56
|
+
mkdirSync(currentDir, { recursive: true })
|
|
57
|
+
writeFileSync(currentPath, screenshot)
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error - this is reliable way of reading Vitest "update" flag
|
|
60
|
+
const updating = expect.getState().snapshotState?._updateSnapshot === 'all'
|
|
61
|
+
if (updating || !existsSync(baselinePath)) {
|
|
62
|
+
// save new baseline screenshot
|
|
63
|
+
mkdirSync(baselineDir, { recursive: true })
|
|
64
|
+
writeFileSync(baselinePath, screenshot)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// compare against stored baseline PNG using pixelmatch
|
|
69
|
+
const baseline = readFileSync(baselinePath)
|
|
70
|
+
const baselineImg = decode(baseline)
|
|
71
|
+
const actualImg = decode(screenshot)
|
|
72
|
+
const { width, height } = baselineImg
|
|
73
|
+
|
|
74
|
+
if (actualImg.width !== width || actualImg.height !== height) {
|
|
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)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const diffCount = pixelmatch(toRGBA(baselineImg), toRGBA(actualImg), undefined, width, height, {
|
|
81
|
+
threshold: options?.threshold ?? 0.1,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const totalPixels = width * height
|
|
85
|
+
const maxAllowed = options?.maxDiffPixels ?? Math.ceil(totalPixels * (options?.maxDiffPixelRatio ?? 0))
|
|
86
|
+
|
|
87
|
+
if (diffCount > maxAllowed) {
|
|
88
|
+
const ratio = (diffCount / totalPixels * 100).toFixed(2)
|
|
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)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
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
|
+
|
|
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
|
|
151
|
+
}
|
|
@@ -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
|
+
}
|