vitest-plugin-vis 1.0.4 → 1.1.0
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/esm/client/align_images.d.ts +2 -0
- package/esm/client/align_images.d.ts.map +1 -0
- package/esm/client/align_images.js +10 -0
- package/esm/client/compare_image.d.ts +7 -0
- package/esm/client/compare_image.d.ts.map +1 -0
- package/esm/client/compare_image.js +13 -0
- package/esm/client/expect/to_match_image_snapshot.d.ts.map +1 -1
- package/esm/client/expect/to_match_image_snapshot.js +2 -15
- package/esm/client/image_data.d.ts +3 -0
- package/esm/client/image_data.d.ts.map +1 -0
- package/esm/client/image_data.js +33 -0
- package/esm/client/image_resizer.d.ts +9 -0
- package/esm/client/image_resizer.d.ts.map +1 -0
- package/esm/client/image_resizer.js +49 -0
- package/esm/client/match_image_snapshot.d.ts +8 -0
- package/esm/client/match_image_snapshot.d.ts.map +1 -0
- package/esm/client/match_image_snapshot.js +63 -0
- package/esm/client/match_image_snapshot.logic.d.ts +3 -0
- package/esm/client/match_image_snapshot.logic.d.ts.map +1 -0
- package/esm/client/match_image_snapshot.logic.js +11 -0
- package/esm/client/task_id.d.ts +3 -0
- package/esm/client/task_id.d.ts.map +1 -1
- package/esm/client/task_id.js +3 -0
- package/esm/client/vitest_browser_context_proxy.d.ts +32 -0
- package/esm/client/vitest_browser_context_proxy.d.ts.map +1 -0
- package/esm/client/vitest_browser_context_proxy.js +11 -0
- package/esm/client.d.ts +1 -1
- package/esm/client.d.ts.map +1 -1
- package/esm/client.js +1 -1
- package/esm/config/vis.d.ts +2 -0
- package/esm/config/vis.d.ts.map +1 -1
- package/esm/server/browser_provider/playwright.js +1 -1
- package/esm/server/browser_provider/webdriverio.js +1 -1
- package/esm/server/commands/extend.d.ts +5 -1
- package/esm/server/commands/extend.d.ts.map +1 -1
- package/esm/server/commands/extend.js +4 -0
- package/esm/server/commands/match_image_snapshot.d.ts.map +1 -1
- package/esm/server/commands/match_image_snapshot.js +7 -26
- package/esm/server/commands/prepare_image_snapshot_comparison.d.ts +20 -0
- package/esm/server/commands/prepare_image_snapshot_comparison.d.ts.map +1 -0
- package/esm/server/commands/prepare_image_snapshot_comparison.js +32 -0
- package/esm/server/commands/write_image_snapshot.d.ts +6 -0
- package/esm/server/commands/write_image_snapshot.d.ts.map +1 -0
- package/esm/server/commands/write_image_snapshot.js +10 -0
- package/esm/server/snapshot.d.ts +7 -0
- package/esm/server/snapshot.d.ts.map +1 -0
- package/esm/server/snapshot.js +27 -0
- package/esm/shared/convert_threshold_unit.js +1 -1
- package/package.json +2 -2
- package/readme.md +2 -2
- package/src/client/align_images.ts +11 -0
- package/src/client/compare_image.ts +21 -0
- package/src/client/expect/to_match_image_snapshot.ts +2 -17
- package/src/client/image_data.ts +36 -0
- package/src/client/image_resizer.ts +59 -0
- package/src/client/match_image_snapshot.logic.ts +13 -0
- package/src/client/match_image_snapshot.ts +78 -0
- package/src/client/task_id.ts +3 -0
- package/src/client/vitest_browser_context_proxy.ts +44 -0
- package/src/client.ts +1 -1
- package/src/server/browser_provider/playwright.ts +1 -1
- package/src/server/browser_provider/webdriverio.ts +1 -1
- package/src/server/commands/extend.ts +9 -0
- package/src/server/commands/match_image_snapshot.ts +9 -40
- package/src/server/commands/prepare_image_snapshot_comparison.ts +55 -0
- package/src/server/commands/write_image_snapshot.ts +20 -0
- package/src/server/snapshot.ts +42 -0
- package/src/shared/convert_threshold_unit.ts +1 -1
- package/esm/client/expect/to_match_image_snapshot.logic.d.ts +0 -2
- package/esm/client/expect/to_match_image_snapshot.logic.d.ts.map +0 -1
- package/esm/client/expect/to_match_image_snapshot.logic.js +0 -11
- package/src/client/expect/to_match_image_snapshot.logic.ts +0 -11
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BrowserCommandContext } from 'vitest/node';
|
|
2
|
+
import type { ImageSnapshotTimeoutOptions } from '../client.ts';
|
|
3
|
+
export declare function takeSnapshot(context: BrowserCommandContext, filePath: string, subject: string, options: ImageSnapshotTimeoutOptions | undefined): Promise<Buffer<ArrayBufferLike>>;
|
|
4
|
+
export declare function takeSnapshotByBrowser(context: BrowserCommandContext, filePath: string, subject: string, options: ImageSnapshotTimeoutOptions | undefined): Promise<Buffer<ArrayBufferLike>>;
|
|
5
|
+
export declare function writeSnapshot(filePath: string, subject: string): Promise<void>;
|
|
6
|
+
export declare function writeSnapshotBuffer(filePath: string, subject: Buffer): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/server/snapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAK/D,wBAAsB,YAAY,CACjC,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,2BAA2B,GAAG,SAAS,oCAOhD;AAED,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,2BAA2B,GAAG,SAAS,oCAOhD;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAGpE;AACD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAG1E"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mkdirp } from 'mkdirp';
|
|
2
|
+
import { dirname } from 'pathe';
|
|
3
|
+
import { isBase64String } from "../shared/base64.js";
|
|
4
|
+
import { browserApi } from "./browser_provider/browser_api.js";
|
|
5
|
+
import { file } from './file.js';
|
|
6
|
+
export async function takeSnapshot(context, filePath, subject, options) {
|
|
7
|
+
if (isBase64String(subject)) {
|
|
8
|
+
await writeSnapshot(filePath, subject);
|
|
9
|
+
return Buffer.from(subject, 'base64');
|
|
10
|
+
}
|
|
11
|
+
return takeSnapshotByBrowser(context, filePath, subject, options);
|
|
12
|
+
}
|
|
13
|
+
export async function takeSnapshotByBrowser(context, filePath, subject, options) {
|
|
14
|
+
await mkdirp(dirname(filePath));
|
|
15
|
+
const browser = browserApi(context);
|
|
16
|
+
return browser.takeScreenshot(filePath, subject ?? 'body', {
|
|
17
|
+
timeout: options?.timeout,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function writeSnapshot(filePath, subject) {
|
|
21
|
+
await mkdirp(dirname(filePath));
|
|
22
|
+
await file.writeFile(filePath, subject, { encoding: 'base64' });
|
|
23
|
+
}
|
|
24
|
+
export async function writeSnapshotBuffer(filePath, subject) {
|
|
25
|
+
await mkdirp(dirname(filePath));
|
|
26
|
+
await file.writeFile(filePath, subject);
|
|
27
|
+
}
|
|
@@ -5,6 +5,6 @@ export function convertThresholdUnit({ failureThresholdType, width, height }, pi
|
|
|
5
5
|
case 'percent':
|
|
6
6
|
return (pixelDiff / (width * height)) * 100;
|
|
7
7
|
default:
|
|
8
|
-
throw new Error(`
|
|
8
|
+
throw new Error(`Unsupported failureThresholdType: ${failureThresholdType}`);
|
|
9
9
|
}
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-plugin-vis",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Vitest visual testing plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"image-snapshot",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dedent": "^1.5.1",
|
|
65
65
|
"is-ci": "^4.0.0",
|
|
66
66
|
"mkdirp": "^3.0.1",
|
|
67
|
-
"pathe": "^
|
|
67
|
+
"pathe": "^2.0.0",
|
|
68
68
|
"pixelmatch": "^6.0.0",
|
|
69
69
|
"pngjs": "^7.0.0",
|
|
70
70
|
"rimraf": "^6.0.1",
|
package/readme.md
CHANGED
|
@@ -53,14 +53,14 @@ You can customize the configuration:
|
|
|
53
53
|
```ts
|
|
54
54
|
// vitest.config.ts
|
|
55
55
|
import { defineConfig } from 'vitest/config'
|
|
56
|
-
import { vis } from 'vitest-plugin-vis/config'
|
|
56
|
+
import { vis, trimCommonFolder } from 'vitest-plugin-vis/config'
|
|
57
57
|
|
|
58
58
|
export default defineConfig({
|
|
59
59
|
plugins: [
|
|
60
60
|
vis({
|
|
61
61
|
preset: 'auto',
|
|
62
62
|
snapshotRootDir: '__vis__',
|
|
63
|
-
customizeSnapshotSubpath: (subpath) => subpath,
|
|
63
|
+
customizeSnapshotSubpath: (subpath) => trimCommonFolder(subpath),
|
|
64
64
|
customizeSnapshotId: (id, index) => `${id}-${index}`,
|
|
65
65
|
diffOptions: undefined, // pixelmatch options
|
|
66
66
|
failureThresholdType: 'pixel',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getMaxSize } from '../shared/get_max_size.ts'
|
|
2
|
+
import { isSameSize } from '../shared/is_same_size.ts'
|
|
3
|
+
import { createImageResizer } from './image_resizer.ts'
|
|
4
|
+
|
|
5
|
+
export function alignImagesToSameSize(image1: ImageData, image2: ImageData): [image1: ImageData, image2: ImageData] {
|
|
6
|
+
if (isSameSize(image1, image2)) return [image1, image2] as const
|
|
7
|
+
|
|
8
|
+
const size = getMaxSize(image1, image2)
|
|
9
|
+
const resize = createImageResizer(size)
|
|
10
|
+
return [resize(image1), resize(image2)] as const
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pixelmatch from 'pixelmatch'
|
|
2
|
+
import { convertThresholdUnit } from '../shared/convert_threshold_unit.ts'
|
|
3
|
+
import type { ImageSnapshotCompareOptions } from '../shared/types.ts'
|
|
4
|
+
|
|
5
|
+
export function compareImage(
|
|
6
|
+
baselineImage: ImageData,
|
|
7
|
+
resultImage: ImageData,
|
|
8
|
+
{ failureThreshold = 0, failureThresholdType = 'pixel', diffOptions }: ImageSnapshotCompareOptions = {},
|
|
9
|
+
) {
|
|
10
|
+
const { width, height } = resultImage
|
|
11
|
+
const diffImage = new ImageData(width, height)
|
|
12
|
+
|
|
13
|
+
const pixelDiff = pixelmatch(resultImage.data, baselineImage.data, diffImage.data, width, height, diffOptions)
|
|
14
|
+
const diffAmount = convertThresholdUnit({ failureThresholdType, width, height }, pixelDiff)
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
pass: diffAmount <= failureThreshold,
|
|
18
|
+
diffAmount,
|
|
19
|
+
diffImage,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { commands } from '@vitest/browser/context'
|
|
2
1
|
import type { AsyncExpectationResult } from '@vitest/expect'
|
|
3
2
|
import { ctx } from '../ctx.ts'
|
|
4
|
-
import {
|
|
3
|
+
import { matchImageSnapshot } from '../match_image_snapshot.ts'
|
|
5
4
|
import { success } from './expectation_result.ts'
|
|
6
|
-
import { parseImageSnapshotSubject } from './to_match_image_snapshot.logic.ts'
|
|
7
5
|
import type { ToMatchImageSnapshotOptions } from './to_match_image_snapshot.types.ts'
|
|
8
6
|
|
|
9
7
|
export function toMatchImageSnapshot(
|
|
@@ -26,18 +24,5 @@ export function toMatchImageSnapshot(
|
|
|
26
24
|
)
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
const taskId = toTaskId(test)
|
|
31
|
-
return toMatchImageSnapshotAsync(taskId, s, options).then(() => success)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function toMatchImageSnapshotAsync(taskId: string, subject: string, options?: ToMatchImageSnapshotOptions) {
|
|
35
|
-
if (options?.customizeSnapshotId) {
|
|
36
|
-
const index = await commands.imageSnapshotNextIndex(taskId)
|
|
37
|
-
const { customizeSnapshotId, ...rest } = options
|
|
38
|
-
const snapshotFileId = customizeSnapshotId(taskId, index)
|
|
39
|
-
return commands.matchImageSnapshot(taskId, subject, { ...rest, snapshotFileId })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return commands.matchImageSnapshot(taskId, subject, options)
|
|
27
|
+
return matchImageSnapshot(test, subject, options).then(() => success)
|
|
43
28
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function toImageData(base64: string) {
|
|
2
|
+
return new Promise<ImageData>((resolve, reject) => {
|
|
3
|
+
const img = new Image()
|
|
4
|
+
img.src = `data:image/png;base64,${base64}`
|
|
5
|
+
|
|
6
|
+
img.onload = () => {
|
|
7
|
+
const canvas = document.createElement('canvas')
|
|
8
|
+
canvas.width = img.width
|
|
9
|
+
canvas.height = img.height
|
|
10
|
+
const ctx = canvas.getContext('2d')!
|
|
11
|
+
ctx.drawImage(img, 0, 0)
|
|
12
|
+
resolve(ctx.getImageData(0, 0, canvas.width, canvas.height))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
img.onerror = () => {
|
|
16
|
+
reject(new Error('Failed to load image'))
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function toDataURL(imageData: ImageData) {
|
|
22
|
+
const canvas = document.createElement('canvas')
|
|
23
|
+
canvas.width = imageData.width
|
|
24
|
+
canvas.height = imageData.height
|
|
25
|
+
const ctx = canvas.getContext('2d')!
|
|
26
|
+
ctx.putImageData(imageData, 0, 0)
|
|
27
|
+
return new Promise<string>((resolve) => {
|
|
28
|
+
canvas.toBlob((blob) => {
|
|
29
|
+
const reader = new FileReader()
|
|
30
|
+
reader.onload = () => {
|
|
31
|
+
resolve(reader.result as string)
|
|
32
|
+
}
|
|
33
|
+
reader.readAsDataURL(blob!)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isSameSize } from '../shared/is_same_size.ts'
|
|
2
|
+
|
|
3
|
+
export const createImageResizer =
|
|
4
|
+
({ width, height }: { width: number; height: number }) =>
|
|
5
|
+
(image: ImageData) => {
|
|
6
|
+
if (isSameSize(image, { width, height })) return image
|
|
7
|
+
|
|
8
|
+
const inArea = (x: number, y: number) => y <= image.height && x <= image.width
|
|
9
|
+
|
|
10
|
+
const data = new Uint8ClampedArray(width * height * 4)
|
|
11
|
+
const result = new ImageData(data, width, height, { colorSpace: image.colorSpace })
|
|
12
|
+
|
|
13
|
+
for (let y = 0; y < height; y++) {
|
|
14
|
+
for (let x = 0; x < width; x++) {
|
|
15
|
+
const idx = (width * y + x) << 2
|
|
16
|
+
if (inArea(x, y)) {
|
|
17
|
+
const old = (image.width * y + x) << 2
|
|
18
|
+
result.data[idx] = image.data[old]!
|
|
19
|
+
result.data[idx + 1] = image.data[old + 1]!
|
|
20
|
+
result.data[idx + 2] = image.data[old + 2]!
|
|
21
|
+
result.data[idx + 3] = image.data[old + 3]!
|
|
22
|
+
} else {
|
|
23
|
+
result.data[idx] = 0
|
|
24
|
+
result.data[idx + 1] = 0
|
|
25
|
+
result.data[idx + 2] = 0
|
|
26
|
+
result.data[idx + 3] = 64
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// slower
|
|
35
|
+
export const createImageResizer2 =
|
|
36
|
+
({ width, height }: { width: number; height: number }) =>
|
|
37
|
+
(image: ImageData) => {
|
|
38
|
+
if (isSameSize(image, { width, height })) return image
|
|
39
|
+
|
|
40
|
+
const inArea = (x: number, y: number) => y <= image.height && x <= image.width
|
|
41
|
+
|
|
42
|
+
const data = new Uint8ClampedArray(width * height * 4)
|
|
43
|
+
const result = new ImageData(data, width, height, { colorSpace: image.colorSpace })
|
|
44
|
+
|
|
45
|
+
const copyBytes = image.width * 4
|
|
46
|
+
for (let y = 0; y < height; y++) {
|
|
47
|
+
result.data.set(image.data.slice(y * image.width, copyBytes), y * width * 4)
|
|
48
|
+
for (let x = image.width; x < width; x++) {
|
|
49
|
+
const idx = (width * y + x) << 2
|
|
50
|
+
if (!inArea(x, y)) {
|
|
51
|
+
result.data[idx] = 0
|
|
52
|
+
result.data[idx + 1] = 0
|
|
53
|
+
result.data[idx + 2] = 0
|
|
54
|
+
result.data[idx + 3] = 64
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
|
|
2
|
+
|
|
3
|
+
export function prettifyOptions(options: ToMatchImageSnapshotOptions | undefined) {
|
|
4
|
+
if (!options) return 'none'
|
|
5
|
+
|
|
6
|
+
return [
|
|
7
|
+
`failureThreshold: ${options.failureThreshold ?? 0} ${options.failureThresholdType ?? 'pixel'}`,
|
|
8
|
+
options.diffOptions ? `diffOptions: ${JSON.stringify(options.diffOptions)}` : '',
|
|
9
|
+
options.timeout ? `timeout: ${options.timeout} ms` : '',
|
|
10
|
+
]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join(', ')
|
|
13
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { commands } from '@vitest/browser/context'
|
|
2
|
+
import dedent from 'dedent'
|
|
3
|
+
import { resolve } from 'pathe'
|
|
4
|
+
import type { Task } from 'vitest'
|
|
5
|
+
import { isBase64String } from '../shared/base64.ts'
|
|
6
|
+
import { alignImagesToSameSize } from './align_images.ts'
|
|
7
|
+
import { compareImage } from './compare_image.ts'
|
|
8
|
+
import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
|
|
9
|
+
import { toDataURL, toImageData } from './image_data.ts'
|
|
10
|
+
import { prettifyOptions } from './match_image_snapshot.logic.ts'
|
|
11
|
+
import { convertElementToCssSelector } from './selector.ts'
|
|
12
|
+
import { toTaskId } from './task_id.ts'
|
|
13
|
+
import { server } from './vitest_browser_context_proxy.ts'
|
|
14
|
+
|
|
15
|
+
export async function matchImageSnapshot(test: Task, subject: any, options?: ToMatchImageSnapshotOptions) {
|
|
16
|
+
const taskId = toTaskId(test)
|
|
17
|
+
const info = await commands.prepareImageSnapshotComparison(
|
|
18
|
+
taskId,
|
|
19
|
+
parseImageSnapshotSubject(subject),
|
|
20
|
+
options?.customizeSnapshotId ? await parseImageSnapshotOptions(taskId, options) : options,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if (!info) return
|
|
24
|
+
|
|
25
|
+
const baselineImage = await toImageData(info.baseline)
|
|
26
|
+
const resultImage = await toImageData(info.result)
|
|
27
|
+
const [baselineAlignedImage, resultAlignedImage] = alignImagesToSameSize(baselineImage, resultImage)
|
|
28
|
+
const { pass, diffAmount, diffImage } = compareImage(baselineAlignedImage, resultAlignedImage, options)
|
|
29
|
+
if (pass) return
|
|
30
|
+
if (server.config.snapshotOptions.updateSnapshot === 'all') {
|
|
31
|
+
await writeSnapshot(info.baselinePath, resultImage)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await writeSnapshot(info.diffPath, diffImage)
|
|
36
|
+
|
|
37
|
+
throw new Error(
|
|
38
|
+
dedent`Snapshot \`${taskId}\` mismatched
|
|
39
|
+
|
|
40
|
+
${
|
|
41
|
+
options?.failureThreshold
|
|
42
|
+
? options?.failureThresholdType === 'percent'
|
|
43
|
+
? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`
|
|
44
|
+
: `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`
|
|
45
|
+
: `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Options: ${prettifyOptions(options)}
|
|
49
|
+
|
|
50
|
+
Expected: ${resolve(info.projectRoot, info.baselinePath)}
|
|
51
|
+
Actual: ${resolve(info.projectRoot, info.resultPath)}
|
|
52
|
+
Difference: ${resolve(info.projectRoot, info.diffPath)}`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated internalized. Use `matchImageSnapshot` directly instead.
|
|
58
|
+
*/
|
|
59
|
+
export function parseImageSnapshotSubject(subject: any) {
|
|
60
|
+
if (subject instanceof Element) return convertElementToCssSelector(subject)
|
|
61
|
+
if (subject?.['selector']) return subject['selector']
|
|
62
|
+
if (isBase64String(subject)) return subject
|
|
63
|
+
throw new Error(
|
|
64
|
+
`'toMatchImageSnapshot()' expects the subject to be an element, locator, or image encoded in base64 string, but got: ${subject}`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function writeSnapshot(path: string, image: ImageData) {
|
|
69
|
+
const content = (await toDataURL(image)).split(',')[1]!
|
|
70
|
+
return commands.writeImageSnapshot(path, content)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function parseImageSnapshotOptions(taskId: string, options: ToMatchImageSnapshotOptions) {
|
|
74
|
+
const index = await commands.imageSnapshotNextIndex(taskId)
|
|
75
|
+
const { customizeSnapshotId, ...rest } = options
|
|
76
|
+
const snapshotFileId = customizeSnapshotId!(taskId, index)
|
|
77
|
+
return { ...rest, snapshotFileId }
|
|
78
|
+
}
|
package/src/client/task_id.ts
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BrowserCommands, Platform } from '@vitest/browser/context'
|
|
2
|
+
import type { SerializedConfig } from 'vitest'
|
|
3
|
+
|
|
4
|
+
let ctx: Awaited<typeof import('@vitest/browser/context')>
|
|
5
|
+
|
|
6
|
+
if ((globalThis as any).__vitest_browser__) {
|
|
7
|
+
import('@vitest/browser/context').then((m) => {
|
|
8
|
+
ctx = m
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const server = new Proxy<{
|
|
13
|
+
/**
|
|
14
|
+
* Platform the Vitest server is running on.
|
|
15
|
+
* The same as calling `process.platform` on the server.
|
|
16
|
+
*/
|
|
17
|
+
platform: Platform
|
|
18
|
+
/**
|
|
19
|
+
* Runtime version of the Vitest server.
|
|
20
|
+
* The same as calling `process.version` on the server.
|
|
21
|
+
*/
|
|
22
|
+
version: string
|
|
23
|
+
/**
|
|
24
|
+
* Name of the browser provider.
|
|
25
|
+
*/
|
|
26
|
+
provider: string
|
|
27
|
+
/**
|
|
28
|
+
* Name of the current browser.
|
|
29
|
+
*/
|
|
30
|
+
browser: string
|
|
31
|
+
/**
|
|
32
|
+
* Available commands for the browser.
|
|
33
|
+
* @see {@link https://vitest.dev/guide/browser/commands}
|
|
34
|
+
*/
|
|
35
|
+
commands: BrowserCommands
|
|
36
|
+
/**
|
|
37
|
+
* Serialized test config.
|
|
38
|
+
*/
|
|
39
|
+
config: SerializedConfig
|
|
40
|
+
}>({} as any, {
|
|
41
|
+
get(target, prop) {
|
|
42
|
+
return (target as any)[prop] ?? (ctx?.server as any)[prop]
|
|
43
|
+
},
|
|
44
|
+
})
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './client/expect/expectation_result.ts'
|
|
2
|
-
export * from './client/expect/to_match_image_snapshot.logic.ts'
|
|
3
2
|
export type * from './client/expect/to_match_image_snapshot.types.ts'
|
|
3
|
+
export * from './client/match_image_snapshot.ts'
|
|
4
4
|
export * from './client/should_take_snapshot.ts'
|
|
5
5
|
export * from './client/snapshot_options.ts'
|
|
6
6
|
export * from './client/task_id.ts'
|
|
@@ -5,7 +5,7 @@ export function playwright(context: BrowserCommandContext): BrowserApi {
|
|
|
5
5
|
return {
|
|
6
6
|
async takeScreenshot(filePath, selector, options) {
|
|
7
7
|
// The `Locator` type from `vitest` has less props than the `Locator` in `playwright`
|
|
8
|
-
const subject = context.iframe.locator(selector
|
|
8
|
+
const subject = context.iframe.locator(selector)
|
|
9
9
|
return subject.screenshot({
|
|
10
10
|
timeout: options?.timeout,
|
|
11
11
|
path: filePath,
|
|
@@ -5,7 +5,7 @@ export function webdriverio(context: BrowserCommandContext): BrowserApi {
|
|
|
5
5
|
const page = (context.provider as any).browser!
|
|
6
6
|
return {
|
|
7
7
|
async takeScreenshot(filePath, selector) {
|
|
8
|
-
const element = await page.$(`${selector
|
|
8
|
+
const element = await page.$(`${selector}`)
|
|
9
9
|
return element.saveScreenshot(filePath)
|
|
10
10
|
},
|
|
11
11
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { type HasImageSnapshotCommand, hasImageSnapshot } from './has_image_snapshot.ts'
|
|
2
2
|
import { type ImageSnapshotNextIndexCommand, imageSnapshotNextIndex } from './image_snapshot_next_index.ts'
|
|
3
3
|
import { type MatchImageSnapshotCommand, matchImageSnapshot } from './match_image_snapshot.ts'
|
|
4
|
+
import {
|
|
5
|
+
type PrepareImageSnapshotComparisonCommand,
|
|
6
|
+
prepareImageSnapshotComparison,
|
|
7
|
+
} from './prepare_image_snapshot_comparison.ts'
|
|
4
8
|
import { type SetupVisSuiteCommand, setupVisSuite } from './setup_vis_suite.ts'
|
|
9
|
+
import { type WriteImageSnapshotCommand, writeImageSnapshot } from './write_image_snapshot.ts'
|
|
5
10
|
|
|
6
11
|
declare module '@vitest/browser/context' {
|
|
7
12
|
interface BrowserCommands
|
|
8
13
|
extends SetupVisSuiteCommand,
|
|
9
14
|
ImageSnapshotNextIndexCommand,
|
|
10
15
|
HasImageSnapshotCommand,
|
|
16
|
+
PrepareImageSnapshotComparisonCommand,
|
|
17
|
+
WriteImageSnapshotCommand,
|
|
11
18
|
MatchImageSnapshotCommand {}
|
|
12
19
|
}
|
|
13
20
|
|
|
@@ -16,4 +23,6 @@ export const commands = {
|
|
|
16
23
|
imageSnapshotNextIndex,
|
|
17
24
|
hasImageSnapshot,
|
|
18
25
|
matchImageSnapshot,
|
|
26
|
+
prepareImageSnapshotComparison,
|
|
27
|
+
writeImageSnapshot,
|
|
19
28
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import dedent from 'dedent'
|
|
2
2
|
import ci from 'is-ci'
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, resolve } from 'pathe'
|
|
3
|
+
import { resolve } from 'pathe'
|
|
5
4
|
import { PNG } from 'pngjs'
|
|
6
|
-
import type { BrowserCommand
|
|
5
|
+
import type { BrowserCommand } from 'vitest/node'
|
|
7
6
|
import { isBase64String } from '../../shared/base64.ts'
|
|
8
7
|
import { getMaxSize } from '../../shared/get_max_size.ts'
|
|
9
8
|
import { isSameSize } from '../../shared/is_same_size.ts'
|
|
@@ -12,9 +11,9 @@ import type {
|
|
|
12
11
|
ImageSnapshotIdOptions,
|
|
13
12
|
ImageSnapshotTimeoutOptions,
|
|
14
13
|
} from '../../shared/types.ts'
|
|
15
|
-
import { browserApi } from '../browser_provider/browser_api.ts'
|
|
16
14
|
import { compareImage } from '../compare_image.ts'
|
|
17
15
|
import { file } from '../file.ts'
|
|
16
|
+
import { takeSnapshot, takeSnapshotByBrowser, writeSnapshot, writeSnapshotBuffer } from '../snapshot.ts'
|
|
18
17
|
import { visContext } from '../vis_context.ts'
|
|
19
18
|
|
|
20
19
|
export interface MatchImageSnapshotCommand {
|
|
@@ -57,7 +56,7 @@ export const matchImageSnapshot: BrowserCommand<
|
|
|
57
56
|
return
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
const resultBuffer = await takeSnapshot(context,
|
|
59
|
+
const resultBuffer = await takeSnapshot(context, info.resultPath, subject, options)
|
|
61
60
|
const baselineImage = PNG.sync.read(baselineBuffer, { skipRescale: true, checkCRC: false })
|
|
62
61
|
const resultImage = PNG.sync.read(resultBuffer, { skipRescale: true, checkCRC: false })
|
|
63
62
|
const [baselineAlignedImage, resultAlignedImage] = alignImageSizes(baselineImage, resultImage)
|
|
@@ -77,6 +76,11 @@ export const matchImageSnapshot: BrowserCommand<
|
|
|
77
76
|
)
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
if (context.project.config.snapshotOptions.updateSnapshot === 'all') {
|
|
80
|
+
await writeSnapshotBuffer(info.baselinePath, resultImage.data)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
await writeSnapshotBuffer(info.diffPath, diffImage.data)
|
|
81
85
|
|
|
82
86
|
throw new Error(
|
|
@@ -98,41 +102,6 @@ export const matchImageSnapshot: BrowserCommand<
|
|
|
98
102
|
)
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
async function takeSnapshot(
|
|
102
|
-
context: BrowserCommandContext,
|
|
103
|
-
subject: string,
|
|
104
|
-
filePath: string,
|
|
105
|
-
options: ImageSnapshotTimeoutOptions | undefined,
|
|
106
|
-
) {
|
|
107
|
-
if (isBase64String(subject)) {
|
|
108
|
-
await writeSnapshot(filePath, subject)
|
|
109
|
-
return Buffer.from(subject, 'base64')
|
|
110
|
-
}
|
|
111
|
-
return takeSnapshotByBrowser(context, filePath, subject, options)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function takeSnapshotByBrowser(
|
|
115
|
-
context: BrowserCommandContext,
|
|
116
|
-
filePath: string,
|
|
117
|
-
subject: string,
|
|
118
|
-
options: ImageSnapshotTimeoutOptions | undefined,
|
|
119
|
-
) {
|
|
120
|
-
await mkdirp(dirname(filePath))
|
|
121
|
-
const browser = browserApi(context)
|
|
122
|
-
return browser.takeScreenshot(filePath, subject, {
|
|
123
|
-
timeout: options?.timeout,
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function writeSnapshot(filePath: string, subject: string) {
|
|
128
|
-
await mkdirp(dirname(filePath))
|
|
129
|
-
await file.writeFile(filePath, subject, { encoding: 'base64' })
|
|
130
|
-
}
|
|
131
|
-
async function writeSnapshotBuffer(filePath: string, subject: Buffer) {
|
|
132
|
-
await mkdirp(dirname(filePath))
|
|
133
|
-
await file.writeFile(filePath, subject)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
105
|
function alignImageSizes(baseline: PNG, result: PNG) {
|
|
137
106
|
if (isSameSize(baseline, result)) return [baseline, result] as const
|
|
138
107
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import ci from 'is-ci'
|
|
2
|
+
import type { BrowserCommand } from 'vitest/node'
|
|
3
|
+
import { isBase64String } from '../../shared/base64.ts'
|
|
4
|
+
import { file } from '../file.ts'
|
|
5
|
+
import { takeSnapshot, takeSnapshotByBrowser, writeSnapshot } from '../snapshot.ts'
|
|
6
|
+
import { visContext } from '../vis_context.ts'
|
|
7
|
+
import type { MatchImageSnapshotOptions } from './match_image_snapshot.ts'
|
|
8
|
+
|
|
9
|
+
type ImageSnapshotComparisonInfo = {
|
|
10
|
+
projectRoot: string
|
|
11
|
+
baselinePath: string
|
|
12
|
+
resultPath: string
|
|
13
|
+
diffPath: string
|
|
14
|
+
baseline: string
|
|
15
|
+
result: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PrepareImageSnapshotComparisonCommand {
|
|
19
|
+
prepareImageSnapshotComparison: (
|
|
20
|
+
taskId: string | undefined,
|
|
21
|
+
subject: string,
|
|
22
|
+
options?: MatchImageSnapshotOptions | undefined,
|
|
23
|
+
) => Promise<ImageSnapshotComparisonInfo | undefined>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const prepareImageSnapshotComparison: BrowserCommand<
|
|
27
|
+
[taskId: string, snapshotId: string, options?: MatchImageSnapshotOptions | undefined]
|
|
28
|
+
> = async (context, taskId, subject, options) => {
|
|
29
|
+
if (!context.testPath) {
|
|
30
|
+
throw new Error('Cannot match snapshot without testPath')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// vitest:browser passes in `null` when not defined
|
|
34
|
+
if (!options) options = {}
|
|
35
|
+
options.timeout = options.timeout ?? (ci ? 30000 : 5000)
|
|
36
|
+
|
|
37
|
+
const info = visContext.getSnapshotInfo(context.testPath, taskId, options)
|
|
38
|
+
const baselineBuffer = await file.tryReadFile(info.baselinePath)
|
|
39
|
+
if (!baselineBuffer) {
|
|
40
|
+
if (isBase64String(subject)) {
|
|
41
|
+
await writeSnapshot(info.baselinePath, subject)
|
|
42
|
+
} else {
|
|
43
|
+
await takeSnapshotByBrowser(context, info.baselinePath, subject, options)
|
|
44
|
+
}
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const resultBuffer = await takeSnapshot(context, info.resultPath, subject, options)
|
|
49
|
+
return {
|
|
50
|
+
...info,
|
|
51
|
+
projectRoot: context.project.runner.root,
|
|
52
|
+
baseline: baselineBuffer.toString('base64'),
|
|
53
|
+
result: resultBuffer.toString('base64'),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { mkdirp } from 'mkdirp'
|
|
2
|
+
import { dirname } from 'pathe'
|
|
3
|
+
import type { BrowserCommand } from 'vitest/node'
|
|
4
|
+
import { file } from '../file.ts'
|
|
5
|
+
|
|
6
|
+
export interface WriteImageSnapshotCommand {
|
|
7
|
+
writeImageSnapshot(taskId: string, snapshotId?: string | undefined): Promise<boolean>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const writeImageSnapshot: BrowserCommand<[filePath: string, base64: string]> = async (
|
|
11
|
+
context,
|
|
12
|
+
filePath,
|
|
13
|
+
base64,
|
|
14
|
+
) => {
|
|
15
|
+
if (!context.testPath) {
|
|
16
|
+
throw new Error('Cannot take snapshot without testPath')
|
|
17
|
+
}
|
|
18
|
+
await mkdirp(dirname(filePath))
|
|
19
|
+
await file.writeFile(filePath, base64, { encoding: 'base64' })
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdirp } from 'mkdirp'
|
|
2
|
+
import { dirname } from 'pathe'
|
|
3
|
+
import type { BrowserCommandContext } from 'vitest/node'
|
|
4
|
+
import type { ImageSnapshotTimeoutOptions } from '../client.ts'
|
|
5
|
+
import { isBase64String } from '../shared/base64.ts'
|
|
6
|
+
import { browserApi } from './browser_provider/browser_api.ts'
|
|
7
|
+
import { file } from './file.js'
|
|
8
|
+
|
|
9
|
+
export async function takeSnapshot(
|
|
10
|
+
context: BrowserCommandContext,
|
|
11
|
+
filePath: string,
|
|
12
|
+
subject: string,
|
|
13
|
+
options: ImageSnapshotTimeoutOptions | undefined,
|
|
14
|
+
) {
|
|
15
|
+
if (isBase64String(subject)) {
|
|
16
|
+
await writeSnapshot(filePath, subject)
|
|
17
|
+
return Buffer.from(subject, 'base64')
|
|
18
|
+
}
|
|
19
|
+
return takeSnapshotByBrowser(context, filePath, subject, options)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function takeSnapshotByBrowser(
|
|
23
|
+
context: BrowserCommandContext,
|
|
24
|
+
filePath: string,
|
|
25
|
+
subject: string,
|
|
26
|
+
options: ImageSnapshotTimeoutOptions | undefined,
|
|
27
|
+
) {
|
|
28
|
+
await mkdirp(dirname(filePath))
|
|
29
|
+
const browser = browserApi(context)
|
|
30
|
+
return browser.takeScreenshot(filePath, subject ?? 'body', {
|
|
31
|
+
timeout: options?.timeout,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function writeSnapshot(filePath: string, subject: string) {
|
|
36
|
+
await mkdirp(dirname(filePath))
|
|
37
|
+
await file.writeFile(filePath, subject, { encoding: 'base64' })
|
|
38
|
+
}
|
|
39
|
+
export async function writeSnapshotBuffer(filePath: string, subject: Buffer) {
|
|
40
|
+
await mkdirp(dirname(filePath))
|
|
41
|
+
await file.writeFile(filePath, subject)
|
|
42
|
+
}
|
|
@@ -8,6 +8,6 @@ export function convertThresholdUnit(
|
|
|
8
8
|
case 'percent':
|
|
9
9
|
return (pixelDiff / (width * height)) * 100
|
|
10
10
|
default:
|
|
11
|
-
throw new Error(`
|
|
11
|
+
throw new Error(`Unsupported failureThresholdType: ${failureThresholdType}`)
|
|
12
12
|
}
|
|
13
13
|
}
|