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.
Files changed (72) hide show
  1. package/esm/client/align_images.d.ts +2 -0
  2. package/esm/client/align_images.d.ts.map +1 -0
  3. package/esm/client/align_images.js +10 -0
  4. package/esm/client/compare_image.d.ts +7 -0
  5. package/esm/client/compare_image.d.ts.map +1 -0
  6. package/esm/client/compare_image.js +13 -0
  7. package/esm/client/expect/to_match_image_snapshot.d.ts.map +1 -1
  8. package/esm/client/expect/to_match_image_snapshot.js +2 -15
  9. package/esm/client/image_data.d.ts +3 -0
  10. package/esm/client/image_data.d.ts.map +1 -0
  11. package/esm/client/image_data.js +33 -0
  12. package/esm/client/image_resizer.d.ts +9 -0
  13. package/esm/client/image_resizer.d.ts.map +1 -0
  14. package/esm/client/image_resizer.js +49 -0
  15. package/esm/client/match_image_snapshot.d.ts +8 -0
  16. package/esm/client/match_image_snapshot.d.ts.map +1 -0
  17. package/esm/client/match_image_snapshot.js +63 -0
  18. package/esm/client/match_image_snapshot.logic.d.ts +3 -0
  19. package/esm/client/match_image_snapshot.logic.d.ts.map +1 -0
  20. package/esm/client/match_image_snapshot.logic.js +11 -0
  21. package/esm/client/task_id.d.ts +3 -0
  22. package/esm/client/task_id.d.ts.map +1 -1
  23. package/esm/client/task_id.js +3 -0
  24. package/esm/client/vitest_browser_context_proxy.d.ts +32 -0
  25. package/esm/client/vitest_browser_context_proxy.d.ts.map +1 -0
  26. package/esm/client/vitest_browser_context_proxy.js +11 -0
  27. package/esm/client.d.ts +1 -1
  28. package/esm/client.d.ts.map +1 -1
  29. package/esm/client.js +1 -1
  30. package/esm/config/vis.d.ts +2 -0
  31. package/esm/config/vis.d.ts.map +1 -1
  32. package/esm/server/browser_provider/playwright.js +1 -1
  33. package/esm/server/browser_provider/webdriverio.js +1 -1
  34. package/esm/server/commands/extend.d.ts +5 -1
  35. package/esm/server/commands/extend.d.ts.map +1 -1
  36. package/esm/server/commands/extend.js +4 -0
  37. package/esm/server/commands/match_image_snapshot.d.ts.map +1 -1
  38. package/esm/server/commands/match_image_snapshot.js +7 -26
  39. package/esm/server/commands/prepare_image_snapshot_comparison.d.ts +20 -0
  40. package/esm/server/commands/prepare_image_snapshot_comparison.d.ts.map +1 -0
  41. package/esm/server/commands/prepare_image_snapshot_comparison.js +32 -0
  42. package/esm/server/commands/write_image_snapshot.d.ts +6 -0
  43. package/esm/server/commands/write_image_snapshot.d.ts.map +1 -0
  44. package/esm/server/commands/write_image_snapshot.js +10 -0
  45. package/esm/server/snapshot.d.ts +7 -0
  46. package/esm/server/snapshot.d.ts.map +1 -0
  47. package/esm/server/snapshot.js +27 -0
  48. package/esm/shared/convert_threshold_unit.js +1 -1
  49. package/package.json +2 -2
  50. package/readme.md +2 -2
  51. package/src/client/align_images.ts +11 -0
  52. package/src/client/compare_image.ts +21 -0
  53. package/src/client/expect/to_match_image_snapshot.ts +2 -17
  54. package/src/client/image_data.ts +36 -0
  55. package/src/client/image_resizer.ts +59 -0
  56. package/src/client/match_image_snapshot.logic.ts +13 -0
  57. package/src/client/match_image_snapshot.ts +78 -0
  58. package/src/client/task_id.ts +3 -0
  59. package/src/client/vitest_browser_context_proxy.ts +44 -0
  60. package/src/client.ts +1 -1
  61. package/src/server/browser_provider/playwright.ts +1 -1
  62. package/src/server/browser_provider/webdriverio.ts +1 -1
  63. package/src/server/commands/extend.ts +9 -0
  64. package/src/server/commands/match_image_snapshot.ts +9 -40
  65. package/src/server/commands/prepare_image_snapshot_comparison.ts +55 -0
  66. package/src/server/commands/write_image_snapshot.ts +20 -0
  67. package/src/server/snapshot.ts +42 -0
  68. package/src/shared/convert_threshold_unit.ts +1 -1
  69. package/esm/client/expect/to_match_image_snapshot.logic.d.ts +0 -2
  70. package/esm/client/expect/to_match_image_snapshot.logic.d.ts.map +0 -1
  71. package/esm/client/expect/to_match_image_snapshot.logic.js +0 -11
  72. 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(`Invalid failureThresholdType: ${failureThresholdType}`);
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.4",
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": "^1.1.2",
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 { toTaskId } from '../task_id.ts'
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
- const s = parseImageSnapshotSubject(subject)
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
+ }
@@ -3,6 +3,9 @@ type Task = {
3
3
  suite?: Task | undefined
4
4
  }
5
5
 
6
+ /**
7
+ * @deprecated not needed anymore. Use `matchImageSnapshot` directly instead.
8
+ */
6
9
  export function toTaskId(task: Task) {
7
10
  const l: any[] = []
8
11
  let t = task
@@ -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 ?? 'body')
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 ?? 'body'}`)
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 { mkdirp } from 'mkdirp'
4
- import { dirname, resolve } from 'pathe'
3
+ import { resolve } from 'pathe'
5
4
  import { PNG } from 'pngjs'
6
- import type { BrowserCommand, BrowserCommandContext } from 'vitest/node'
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, subject, info.resultPath, options)
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(`Invalid failureThresholdType: ${failureThresholdType}`)
11
+ throw new Error(`Unsupported failureThresholdType: ${failureThresholdType}`)
12
12
  }
13
13
  }