vitest-plugin-vis 1.1.0 → 1.3.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 (104) hide show
  1. package/esm/client/expect/augment.d.ts +8 -0
  2. package/esm/client/expect/augment.d.ts.map +1 -0
  3. package/esm/client/expect/augment.js +1 -0
  4. package/esm/client/expect/extend.d.ts +1 -7
  5. package/esm/client/expect/extend.d.ts.map +1 -1
  6. package/esm/client/expect/extend.js +1 -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 +3 -2
  9. package/esm/client/expect/to_match_image_snapshot.types.d.ts +5 -0
  10. package/esm/client/expect/to_match_image_snapshot.types.d.ts.map +1 -1
  11. package/esm/client/match_image_snapshot.d.ts +4 -1
  12. package/esm/client/match_image_snapshot.d.ts.map +1 -1
  13. package/esm/client/match_image_snapshot.js +43 -30
  14. package/esm/client/match_image_snapshot.logic.js +1 -1
  15. package/esm/client/page/augment.d.ts +6 -0
  16. package/esm/client/page/augment.d.ts.map +1 -0
  17. package/esm/client/page/augment.js +1 -0
  18. package/esm/client/page/extend.d.ts +1 -5
  19. package/esm/client/page/extend.d.ts.map +1 -1
  20. package/esm/client/page/extend.js +1 -0
  21. package/esm/client/selector.d.ts +0 -2
  22. package/esm/client/selector.d.ts.map +1 -1
  23. package/esm/client/selector.js +5 -12
  24. package/esm/commands.d.ts +2 -0
  25. package/esm/commands.d.ts.map +1 -1
  26. package/esm/commands.js +2 -0
  27. package/esm/config/vis.d.ts +1 -1
  28. package/esm/index.d.ts +2 -0
  29. package/esm/index.d.ts.map +1 -1
  30. package/esm/index.js +2 -0
  31. package/esm/presets/auto.d.ts +2 -1
  32. package/esm/presets/auto.d.ts.map +1 -1
  33. package/esm/presets/auto.js +2 -1
  34. package/esm/presets/manual.d.ts +2 -1
  35. package/esm/presets/manual.d.ts.map +1 -1
  36. package/esm/presets/manual.js +2 -1
  37. package/esm/server/commands/_assertions.d.ts +5 -0
  38. package/esm/server/commands/_assertions.d.ts.map +1 -0
  39. package/esm/server/commands/_assertions.js +11 -0
  40. package/esm/server/commands/extend.d.ts +1 -1
  41. package/esm/server/commands/has_image_snapshot.d.ts.map +1 -1
  42. package/esm/server/commands/has_image_snapshot.js +2 -3
  43. package/esm/server/commands/image_snapshot_next_index.d.ts.map +1 -1
  44. package/esm/server/commands/image_snapshot_next_index.js +2 -3
  45. package/esm/server/commands/match_image_snapshot.d.ts.map +1 -1
  46. package/esm/server/commands/match_image_snapshot.js +6 -5
  47. package/esm/server/commands/prepare_image_snapshot_comparison.d.ts.map +1 -1
  48. package/esm/server/commands/prepare_image_snapshot_comparison.js +2 -3
  49. package/esm/server/commands/setup_vis_suite.d.ts.map +1 -1
  50. package/esm/server/commands/setup_vis_suite.js +2 -0
  51. package/esm/server/commands/write_image_snapshot.d.ts +8 -2
  52. package/esm/server/commands/write_image_snapshot.d.ts.map +1 -1
  53. package/esm/server/commands/write_image_snapshot.js +6 -7
  54. package/esm/server/vis_context.d.ts.map +1 -1
  55. package/esm/server/vis_context.logic.d.ts +2 -2
  56. package/esm/server/vis_context.logic.d.ts.map +1 -1
  57. package/esm/server/vis_context.logic.js +3 -3
  58. package/esm/shared/compare_image.d.ts +6 -0
  59. package/esm/shared/compare_image.d.ts.map +1 -0
  60. package/esm/shared/compare_image.js +10 -0
  61. package/esm/testing/stubBrowserCommandContext.d.ts +3 -0
  62. package/esm/testing/stubBrowserCommandContext.d.ts.map +1 -0
  63. package/esm/testing/stubBrowserCommandContext.js +2 -0
  64. package/esm/testing.d.ts +1 -0
  65. package/esm/testing.d.ts.map +1 -1
  66. package/esm/testing.js +1 -0
  67. package/package.json +2 -1
  68. package/readme.md +38 -2
  69. package/src/client/expect/augment.ts +8 -0
  70. package/src/client/expect/extend.ts +1 -8
  71. package/src/client/expect/to_match_image_snapshot.ts +3 -2
  72. package/src/client/expect/to_match_image_snapshot.types.ts +7 -1
  73. package/src/client/match_image_snapshot.logic.ts +1 -1
  74. package/src/client/match_image_snapshot.ts +68 -36
  75. package/src/client/page/augment.ts +5 -0
  76. package/src/client/page/extend.ts +2 -5
  77. package/src/client/selector.ts +0 -13
  78. package/src/commands.ts +2 -0
  79. package/src/index.ts +3 -0
  80. package/src/presets/auto.ts +2 -1
  81. package/src/presets/manual.ts +2 -1
  82. package/src/server/commands/_assertions.ts +18 -0
  83. package/src/server/commands/has_image_snapshot.ts +2 -3
  84. package/src/server/commands/image_snapshot_next_index.ts +2 -3
  85. package/src/server/commands/match_image_snapshot.ts +13 -6
  86. package/src/server/commands/prepare_image_snapshot_comparison.ts +2 -3
  87. package/src/server/commands/setup_vis_suite.ts +3 -0
  88. package/src/server/commands/write_image_snapshot.ts +17 -11
  89. package/src/server/vis_context.logic.ts +6 -6
  90. package/src/shared/compare_image.ts +20 -0
  91. package/src/testing/stubBrowserCommandContext.ts +4 -0
  92. package/src/testing.ts +1 -0
  93. package/esm/client/compare_image.d.ts +0 -7
  94. package/esm/client/compare_image.d.ts.map +0 -1
  95. package/esm/client/compare_image.js +0 -13
  96. package/esm/server/commands/is_ci.d.ts +0 -2
  97. package/esm/server/commands/is_ci.d.ts.map +0 -1
  98. package/esm/server/commands/is_ci.js +0 -6
  99. package/esm/server/compare_image.d.ts +0 -8
  100. package/esm/server/compare_image.d.ts.map +0 -1
  101. package/esm/server/compare_image.js +0 -16
  102. package/src/client/compare_image.ts +0 -21
  103. package/src/server/commands/is_ci.ts +0 -7
  104. package/src/server/compare_image.ts +0 -34
@@ -99,9 +99,6 @@ async function setupState(suite, visOptions) {
99
99
  await Promise.allSettled([ctx.rimraf(join(state.snapshotDiffDir)), ctx.rimraf(join(state.snapshotResultDir))]);
100
100
  return state;
101
101
  }
102
- export function getSuiteId(state, testPath, options) {
103
- return getSnapshotSubpath(relative(state.projectPath, testPath), options);
104
- }
105
102
  export function createSuite(state, testPath, options) {
106
103
  const suiteId = getSuiteId(state, testPath, options);
107
104
  return {
@@ -114,3 +111,6 @@ export function createSuite(state, testPath, options) {
114
111
  },
115
112
  };
116
113
  }
114
+ export function getSuiteId(state, testPath, options) {
115
+ return getSnapshotSubpath(relative(state.projectPath, testPath), options);
116
+ }
@@ -0,0 +1,6 @@
1
+ import type { ImageSnapshotCompareOptions } from './types.ts';
2
+ export declare function compareImage(img1: Uint8ClampedArray | Buffer, img2: Uint8ClampedArray | Buffer, output: Uint8ClampedArray | Buffer, width: number, height: number, { failureThreshold, failureThresholdType, diffOptions }?: ImageSnapshotCompareOptions): {
3
+ pass: boolean;
4
+ diffAmount: number;
5
+ };
6
+ //# sourceMappingURL=compare_image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare_image.d.ts","sourceRoot":"","sources":["../../src/shared/compare_image.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAE7D,wBAAgB,YAAY,CAC3B,IAAI,EAAE,iBAAiB,GAAG,MAAM,EAChC,IAAI,EAAE,iBAAiB,GAAG,MAAM,EAChC,MAAM,EAAE,iBAAiB,GAAG,MAAM,EAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,EAAE,gBAAoB,EAAE,oBAA8B,EAAE,WAAW,EAAE,GAAE,2BAAgC;;;EASvG"}
@@ -0,0 +1,10 @@
1
+ import pixelmatch from 'pixelmatch';
2
+ import { convertThresholdUnit } from "./convert_threshold_unit.js";
3
+ export function compareImage(img1, img2, output, width, height, { failureThreshold = 0, failureThresholdType = 'pixel', diffOptions } = {}) {
4
+ const pixelDiff = pixelmatch(img1, img2, output, width, height, diffOptions);
5
+ const diffAmount = convertThresholdUnit({ failureThresholdType, width, height }, pixelDiff);
6
+ return {
7
+ pass: diffAmount <= failureThreshold,
8
+ diffAmount,
9
+ };
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { BrowserCommandContext } from 'vitest/node';
2
+ export declare const stubBrowserCommandContext: (stub?: import("type-plus").RecursivePartial<BrowserCommandContext> | undefined) => BrowserCommandContext;
3
+ //# sourceMappingURL=stubBrowserCommandContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stubBrowserCommandContext.d.ts","sourceRoot":"","sources":["../../src/testing/stubBrowserCommandContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD,eAAO,MAAM,yBAAyB,2GAAwC,CAAA"}
@@ -0,0 +1,2 @@
1
+ import { stub } from 'type-plus';
2
+ export const stubBrowserCommandContext = stub.build({});
package/esm/testing.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './testing/constants.ts';
2
+ export * from './testing/stubBrowserCommandContext.ts';
2
3
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA;AACtC,cAAc,wCAAwC,CAAA"}
package/esm/testing.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./testing/constants.js";
2
+ export * from "./testing/stubBrowserCommandContext.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-plugin-vis",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Vitest visual testing plugin",
5
5
  "keywords": [
6
6
  "image-snapshot",
@@ -80,6 +80,7 @@
80
80
  "@vitest/coverage-v8": "^2.1.8",
81
81
  "@vitest/expect": "^2.1.8",
82
82
  "cross-env": "^7.0.3",
83
+ "never-fail": "^1.1.4",
83
84
  "npm-run-all2": "^7.0.2",
84
85
  "playwright": "^1.49.0",
85
86
  "react": "^19.0.0",
package/readme.md CHANGED
@@ -103,7 +103,7 @@ vis.presets.auto()
103
103
  vis.presets.manual()
104
104
 
105
105
 
106
- // or capture image snapshot for all stories with `snapshot` tag,
106
+ // or capture image snapshot for all rendering tests
107
107
  // for multiple themes (light and dark in this example)
108
108
  vis.presets.theme({
109
109
  light() { document.body.classList.remove('dark') },
@@ -111,11 +111,30 @@ vis.presets.theme({
111
111
  })
112
112
  ```
113
113
 
114
+ ### TypeScript Configuration
115
+
116
+ The main usage of this addon is to use the `toMatchImageSnapshot` matcher.
117
+
118
+ Since it is exposed under the `expect` object of `vitest`,
119
+ you typically do not need to import `storybook-addon-vis` directly.
120
+
121
+ Because of this, TypeScript may not recognize the matcher.
122
+ To address this, you can add the following to your `tsconfig.json`:
123
+
124
+ ```json
125
+ {
126
+ "compilerOptions": {
127
+ "types": ["vitest-plugin-vis"]
128
+ }
129
+ }
130
+ ```
131
+
114
132
  ## Usage
115
133
 
116
134
  ### Auto Snapshot
117
135
 
118
- By default, the plugin will use the `auto` preset, which will take a snapshot at the end of each test.
136
+ By default, the plugin will use the `auto` preset,
137
+ which will take a snapshot at the end of each test.
119
138
 
120
139
  You can control how the auto snapshot is taken using the `setAutoSnapshotOptions` function:
121
140
 
@@ -153,6 +172,23 @@ describe('nested scope', () => {
153
172
  })
154
173
  ```
155
174
 
175
+ The options are similar to `expect(...).toMatchImageSnapshot(options)`:
176
+
177
+ ```ts
178
+ // enable/disable auto snapshot
179
+ setAutoSnapshotOptions(true /* or false */)
180
+
181
+ // detailed options
182
+ setAutoSnapshotOptions({
183
+ enable: true,
184
+ customizeSnapshotId: (id, index) => `${id}-custom-${index}`,
185
+ diffOptions: { threshold: 0.01 } // pixelmatch options,
186
+ failureThreshold: 0.01,
187
+ failureThresholdType: 'percent',
188
+ timeout: 15000
189
+ })
190
+ ```
191
+
156
192
  ### Manual Snapshot
157
193
 
158
194
  You can also set the preset to `manual` and compare snapshots manually:
@@ -0,0 +1,8 @@
1
+ import type { ImageSnapshotMatcher } from './to_match_image_snapshot.types.ts'
2
+
3
+ declare global {
4
+ namespace jest {
5
+ // biome-ignore lint/correctness/noUnusedVariables: augmentation must have matching type params.
6
+ interface Matchers<R, T> extends ImageSnapshotMatcher {}
7
+ }
8
+ }
@@ -1,12 +1,5 @@
1
1
  import { expect } from 'vitest'
2
2
  import { toMatchImageSnapshot } from './to_match_image_snapshot.ts'
3
- import type { ImageSnapshotMatcher } from './to_match_image_snapshot.types.ts'
4
-
5
- declare global {
6
- namespace jest {
7
- // biome-ignore lint/correctness/noUnusedVariables: augmentation must have matching type params.
8
- interface Matchers<R, T> extends ImageSnapshotMatcher {}
9
- }
10
- }
3
+ import './augment.ts'
11
4
 
12
5
  expect.extend({ toMatchImageSnapshot })
@@ -1,6 +1,7 @@
1
+ import { commands } from '@vitest/browser/context'
1
2
  import type { AsyncExpectationResult } from '@vitest/expect'
2
3
  import { ctx } from '../ctx.ts'
3
- import { matchImageSnapshot } from '../match_image_snapshot.ts'
4
+ import { imageSnapshotMatcher } from '../match_image_snapshot.ts'
4
5
  import { success } from './expectation_result.ts'
5
6
  import type { ToMatchImageSnapshotOptions } from './to_match_image_snapshot.types.ts'
6
7
 
@@ -24,5 +25,5 @@ export function toMatchImageSnapshot(
24
25
  )
25
26
  }
26
27
 
27
- return matchImageSnapshot(test, subject, options).then(() => success)
28
+ return imageSnapshotMatcher(commands)(test, subject, options).then(() => success)
28
29
  }
@@ -11,4 +11,10 @@ export interface ImageSnapshotMatcher {
11
11
  export interface ToMatchImageSnapshotOptions
12
12
  extends ImageSnapshotTimeoutOptions,
13
13
  ImageSnapshotIdOptions,
14
- ImageSnapshotCompareOptions {}
14
+ ImageSnapshotCompareOptions {
15
+ /**
16
+ * Expect the matcher to fail.
17
+ * If it passes, it will throw an error with details.
18
+ */
19
+ expectToFail?: boolean | undefined
20
+ }
@@ -4,7 +4,7 @@ export function prettifyOptions(options: ToMatchImageSnapshotOptions | undefined
4
4
  if (!options) return 'none'
5
5
 
6
6
  return [
7
- `failureThreshold: ${options.failureThreshold ?? 0} ${options.failureThresholdType ?? 'pixel'}`,
7
+ `failureThreshold: ${options.failureThreshold ?? 0} ${options.failureThresholdType ?? 'pixels'}`,
8
8
  options.diffOptions ? `diffOptions: ${JSON.stringify(options.diffOptions)}` : '',
9
9
  options.timeout ? `timeout: ${options.timeout} ms` : '',
10
10
  ]
@@ -1,10 +1,12 @@
1
- import { commands } from '@vitest/browser/context'
2
1
  import dedent from 'dedent'
3
2
  import { resolve } from 'pathe'
4
3
  import type { Task } from 'vitest'
4
+ import type { ImageSnapshotNextIndexCommand } from '../commands.ts'
5
+ import type { PrepareImageSnapshotComparisonCommand } from '../server/commands/prepare_image_snapshot_comparison.ts'
6
+ import type { WriteImageSnapshotCommand } from '../server/commands/write_image_snapshot.ts'
5
7
  import { isBase64String } from '../shared/base64.ts'
8
+ import { compareImage } from '../shared/compare_image.ts'
6
9
  import { alignImagesToSameSize } from './align_images.ts'
7
- import { compareImage } from './compare_image.ts'
8
10
  import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
9
11
  import { toDataURL, toImageData } from './image_data.ts'
10
12
  import { prettifyOptions } from './match_image_snapshot.logic.ts'
@@ -12,45 +14,71 @@ import { convertElementToCssSelector } from './selector.ts'
12
14
  import { toTaskId } from './task_id.ts'
13
15
  import { server } from './vitest_browser_context_proxy.ts'
14
16
 
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
- )
17
+ export function imageSnapshotMatcher(
18
+ commands: PrepareImageSnapshotComparisonCommand & WriteImageSnapshotCommand & ImageSnapshotNextIndexCommand,
19
+ ) {
20
+ return async function matchImageSnapshot(test: Task, subject: any, options?: ToMatchImageSnapshotOptions) {
21
+ const taskId = toTaskId(test)
22
+ const info = await commands.prepareImageSnapshotComparison(
23
+ taskId,
24
+ parseImageSnapshotSubject(subject),
25
+ options?.customizeSnapshotId ? await parseImageSnapshotOptions(commands, taskId, options) : options,
26
+ )
22
27
 
23
- if (!info) return
28
+ if (!info) return
24
29
 
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
- }
30
+ const baselineImage = await toImageData(info.baseline)
31
+ const resultImage = await toImageData(info.result)
32
+ const [baselineAlignedImage, resultAlignedImage] = alignImagesToSameSize(baselineImage, resultImage)
33
+ const { width, height } = baselineAlignedImage
34
+ const diffImage = new ImageData(width, height)
35
+ const { pass, diffAmount } = compareImage(
36
+ baselineAlignedImage.data,
37
+ resultAlignedImage.data,
38
+ diffImage.data,
39
+ width,
40
+ height,
41
+ options,
42
+ )
43
+ if (pass) {
44
+ if (options?.expectToFail) {
45
+ throw new Error(
46
+ dedent`Snapshot \`${taskId}\` matched but expected to fail.
34
47
 
35
- await writeSnapshot(info.diffPath, diffImage)
48
+ Options: ${prettifyOptions(options)}
49
+ DiffAmount: ${options.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}
36
50
 
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`}.`
51
+ Expected: ${resolve(info.projectRoot, info.baselinePath)}
52
+ Actual: ${resolve(info.projectRoot, info.resultPath)}`,
53
+ )
46
54
  }
55
+ return
56
+ }
57
+ if (server.config.snapshotOptions.updateSnapshot === 'all') {
58
+ await writeSnapshot(commands, info.baselinePath, resultImage)
59
+ return
60
+ }
47
61
 
48
- Options: ${prettifyOptions(options)}
62
+ await writeSnapshot(commands, info.diffPath, diffImage)
49
63
 
50
- Expected: ${resolve(info.projectRoot, info.baselinePath)}
51
- Actual: ${resolve(info.projectRoot, info.resultPath)}
52
- Difference: ${resolve(info.projectRoot, info.diffPath)}`,
53
- )
64
+ throw new Error(
65
+ dedent`Snapshot \`${taskId}\` mismatched
66
+
67
+ ${
68
+ options?.failureThreshold
69
+ ? options?.failureThresholdType === 'percent'
70
+ ? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`
71
+ : `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`
72
+ : `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`
73
+ }
74
+
75
+ Options: ${prettifyOptions(options)}
76
+
77
+ Expected: ${resolve(info.projectRoot, info.baselinePath)}
78
+ Actual: ${resolve(info.projectRoot, info.resultPath)}
79
+ Difference: ${resolve(info.projectRoot, info.diffPath)}`,
80
+ )
81
+ }
54
82
  }
55
83
 
56
84
  /**
@@ -65,12 +93,16 @@ export function parseImageSnapshotSubject(subject: any) {
65
93
  )
66
94
  }
67
95
 
68
- async function writeSnapshot(path: string, image: ImageData) {
96
+ async function writeSnapshot(commands: WriteImageSnapshotCommand, path: string, image: ImageData) {
69
97
  const content = (await toDataURL(image)).split(',')[1]!
70
98
  return commands.writeImageSnapshot(path, content)
71
99
  }
72
100
 
73
- async function parseImageSnapshotOptions(taskId: string, options: ToMatchImageSnapshotOptions) {
101
+ async function parseImageSnapshotOptions(
102
+ commands: ImageSnapshotNextIndexCommand,
103
+ taskId: string,
104
+ options: ToMatchImageSnapshotOptions,
105
+ ) {
74
106
  const index = await commands.imageSnapshotNextIndex(taskId)
75
107
  const { customizeSnapshotId, ...rest } = options
76
108
  const snapshotFileId = customizeSnapshotId!(taskId, index)
@@ -0,0 +1,5 @@
1
+ import type { HasImageSnapshotAction } from './has_image_snapshot.ts'
2
+
3
+ declare module '@vitest/browser/context' {
4
+ interface BrowserPage extends HasImageSnapshotAction {}
5
+ }
@@ -1,8 +1,5 @@
1
1
  import { page } from '@vitest/browser/context'
2
- import { type HasImageSnapshotAction, hasImageSnapshot } from './has_image_snapshot.ts'
3
-
4
- declare module '@vitest/browser/context' {
5
- interface BrowserPage extends HasImageSnapshotAction {}
6
- }
2
+ import './augment.ts'
3
+ import { hasImageSnapshot } from './has_image_snapshot.ts'
7
4
 
8
5
  page.extend({ hasImageSnapshot })
@@ -3,20 +3,7 @@
3
3
  *
4
4
  * @see https://github.com/vitest-dev/vitest/blob/main/packages/browser/src/client/utils.ts
5
5
  */
6
- import type { Locator } from '@vitest/browser/context'
7
6
 
8
- export function convertToSelector(elementOrLocator: Element | Locator): string {
9
- if (!elementOrLocator) {
10
- throw new Error('Expected element or locator to be defined.')
11
- }
12
- if (elementOrLocator instanceof Element) {
13
- return convertElementToCssSelector(elementOrLocator)
14
- }
15
- if ('selector' in elementOrLocator) {
16
- return (elementOrLocator as any).selector
17
- }
18
- throw new Error('Expected element or locator to be an instance of Element or Locator.')
19
- }
20
7
  export function convertElementToCssSelector(element: Element) {
21
8
  if (!element || !(element instanceof Element)) {
22
9
  throw new Error(`Expected DOM element to be an instance of Element, received ${typeof element}`)
package/src/commands.ts CHANGED
@@ -2,3 +2,5 @@ export * from './server/commands/has_image_snapshot.ts'
2
2
  export * from './server/commands/image_snapshot_next_index.ts'
3
3
  export * from './server/commands/match_image_snapshot.ts'
4
4
  export * from './server/commands/setup_vis_suite.ts'
5
+ export * from './server/commands/prepare_image_snapshot_comparison.ts'
6
+ export * from './server/commands/write_image_snapshot.ts'
package/src/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  // `vitest-plugin-vis` provides code that can be used in test files.
2
+ import './client/expect/augment.ts'
3
+ import './client/page/augment.ts'
4
+
2
5
  export * from './client/snapshot_options.ts'
@@ -1,4 +1,5 @@
1
- import 'vitest-browser-react'
2
1
  import { vis } from '../setup/vis.ts'
2
+ import '../client/expect/extend.ts'
3
+ import '../client/page/extend.ts'
3
4
 
4
5
  vis.presets.auto()
@@ -1,4 +1,5 @@
1
- import 'vitest-browser-react'
2
1
  import { vis } from '../setup/vis.ts'
2
+ import '../client/expect/extend.ts'
3
+ import '../client/page/extend.ts'
3
4
 
4
5
  vis.presets.manual()
@@ -0,0 +1,18 @@
1
+ import { isAbsolute } from 'pathe'
2
+ import type { RequiredPick } from 'type-plus'
3
+ import type { BrowserCommandContext } from 'vitest/node'
4
+
5
+ export function assertTestPathDefined(
6
+ context: BrowserCommandContext,
7
+ commandName: string,
8
+ ): asserts context is RequiredPick<BrowserCommandContext, 'testPath'> {
9
+ if (!context.testPath) {
10
+ throw new Error(`'commands.${commandName}' requires testPath to be defined`)
11
+ }
12
+ }
13
+
14
+ export function assertIsRelativePath(relativeFilePath: string, propName: string) {
15
+ if (isAbsolute(relativeFilePath)) {
16
+ throw new Error(`'${propName}' must be a relative path`)
17
+ }
18
+ }
@@ -1,5 +1,6 @@
1
1
  import type { BrowserCommand } from 'vitest/node'
2
2
  import { visContext } from '../vis_context.ts'
3
+ import { assertTestPathDefined } from './_assertions.ts'
3
4
 
4
5
  export interface HasImageSnapshotCommand {
5
6
  hasImageSnapshot(taskId: string, snapshotId?: string | undefined): Promise<boolean>
@@ -10,9 +11,7 @@ export const hasImageSnapshot: BrowserCommand<[taskId: string, snapshotId?: stri
10
11
  taskId,
11
12
  snapshotId,
12
13
  ) => {
13
- if (!context.testPath) {
14
- throw new Error('Cannot take snapshot without testPath')
15
- }
14
+ assertTestPathDefined(context, 'hasImageSnapshot')
16
15
 
17
16
  return visContext.hasImageSnapshot(context.testPath, taskId, snapshotId)
18
17
  }
@@ -1,5 +1,6 @@
1
1
  import type { BrowserCommand } from 'vitest/node'
2
2
  import { visContext } from '../vis_context.ts'
3
+ import { assertTestPathDefined } from './_assertions.ts'
3
4
 
4
5
  export interface ImageSnapshotNextIndexCommand {
5
6
  /**
@@ -9,9 +10,7 @@ export interface ImageSnapshotNextIndexCommand {
9
10
  }
10
11
 
11
12
  export const imageSnapshotNextIndex: BrowserCommand<[taskId: string]> = async (context, taskId) => {
12
- if (!context.testPath) {
13
- throw new Error('Cannot take snapshot without testPath')
14
- }
13
+ assertTestPathDefined(context, 'imageSnapshotNextIndex')
15
14
 
16
15
  return visContext.getTaskCount(context.testPath, taskId)
17
16
  }
@@ -4,6 +4,7 @@ import { resolve } from 'pathe'
4
4
  import { PNG } from 'pngjs'
5
5
  import type { BrowserCommand } from 'vitest/node'
6
6
  import { isBase64String } from '../../shared/base64.ts'
7
+ import { compareImage } from '../../shared/compare_image.ts'
7
8
  import { getMaxSize } from '../../shared/get_max_size.ts'
8
9
  import { isSameSize } from '../../shared/is_same_size.ts'
9
10
  import type {
@@ -11,10 +12,10 @@ import type {
11
12
  ImageSnapshotIdOptions,
12
13
  ImageSnapshotTimeoutOptions,
13
14
  } from '../../shared/types.ts'
14
- import { compareImage } from '../compare_image.ts'
15
15
  import { file } from '../file.ts'
16
16
  import { takeSnapshot, takeSnapshotByBrowser, writeSnapshot, writeSnapshotBuffer } from '../snapshot.ts'
17
17
  import { visContext } from '../vis_context.ts'
18
+ import { assertTestPathDefined } from './_assertions.ts'
18
19
 
19
20
  export interface MatchImageSnapshotCommand {
20
21
  matchImageSnapshot: (
@@ -37,9 +38,7 @@ export interface MatchImageSnapshotOptions
37
38
  export const matchImageSnapshot: BrowserCommand<
38
39
  [taskId: string, subject: string, options?: MatchImageSnapshotOptions | undefined]
39
40
  > = async (context, taskId, subject, options) => {
40
- if (!context.testPath) {
41
- throw new Error('Cannot match snapshot without testPath')
42
- }
41
+ assertTestPathDefined(context, 'matchImageSnapshot')
43
42
 
44
43
  // vitest:browser passes in `null` when not defined
45
44
  if (!options) options = {}
@@ -60,8 +59,16 @@ export const matchImageSnapshot: BrowserCommand<
60
59
  const baselineImage = PNG.sync.read(baselineBuffer, { skipRescale: true, checkCRC: false })
61
60
  const resultImage = PNG.sync.read(resultBuffer, { skipRescale: true, checkCRC: false })
62
61
  const [baselineAlignedImage, resultAlignedImage] = alignImageSizes(baselineImage, resultImage)
63
-
64
- const { pass, diffAmount, diffImage } = compareImage(baselineAlignedImage, resultAlignedImage, options)
62
+ const { width, height } = baselineAlignedImage
63
+ const diffImage = new PNG({ width, height })
64
+ const { pass, diffAmount } = compareImage(
65
+ baselineAlignedImage.data,
66
+ resultAlignedImage.data,
67
+ diffImage.data,
68
+ width,
69
+ height,
70
+ options,
71
+ )
65
72
  if (pass) {
66
73
  if (sizeNotChanged(baselineImage, baselineAlignedImage) && sizeNotChanged(resultImage, resultAlignedImage)) {
67
74
  return
@@ -4,6 +4,7 @@ import { isBase64String } from '../../shared/base64.ts'
4
4
  import { file } from '../file.ts'
5
5
  import { takeSnapshot, takeSnapshotByBrowser, writeSnapshot } from '../snapshot.ts'
6
6
  import { visContext } from '../vis_context.ts'
7
+ import { assertTestPathDefined } from './_assertions.ts'
7
8
  import type { MatchImageSnapshotOptions } from './match_image_snapshot.ts'
8
9
 
9
10
  type ImageSnapshotComparisonInfo = {
@@ -26,9 +27,7 @@ export interface PrepareImageSnapshotComparisonCommand {
26
27
  export const prepareImageSnapshotComparison: BrowserCommand<
27
28
  [taskId: string, snapshotId: string, options?: MatchImageSnapshotOptions | undefined]
28
29
  > = async (context, taskId, subject, options) => {
29
- if (!context.testPath) {
30
- throw new Error('Cannot match snapshot without testPath')
31
- }
30
+ assertTestPathDefined(context, 'prepareImageSnapshotComparison')
32
31
 
33
32
  // vitest:browser passes in `null` when not defined
34
33
  if (!options) options = {}
@@ -1,5 +1,6 @@
1
1
  import type { BrowserCommand } from 'vitest/node'
2
2
  import { visContext } from '../vis_context.ts'
3
+ import { assertTestPathDefined } from './_assertions.ts'
3
4
 
4
5
  export interface SetupVisSuiteCommand {
5
6
  /**
@@ -13,5 +14,7 @@ export interface SetupVisSuiteCommand {
13
14
  }
14
15
 
15
16
  export const setupVisSuite: BrowserCommand<[]> = async (context) => {
17
+ assertTestPathDefined(context, 'setupVisSuite')
18
+
16
19
  await visContext.setupSuite(context)
17
20
  }
@@ -1,20 +1,26 @@
1
1
  import { mkdirp } from 'mkdirp'
2
- import { dirname } from 'pathe'
2
+ import { dirname, isAbsolute } from 'pathe'
3
3
  import type { BrowserCommand } from 'vitest/node'
4
4
  import { file } from '../file.ts'
5
+ import { assertIsRelativePath } from './_assertions.ts'
5
6
 
6
7
  export interface WriteImageSnapshotCommand {
7
- writeImageSnapshot(taskId: string, snapshotId?: string | undefined): Promise<boolean>
8
+ /**
9
+ * Writes the image snapshot to the file system.
10
+ * @param relativeFilePath The file path to write the image snapshot to.
11
+ * It is relative to the project root.
12
+ * @param imageBase64 The base64 encoded image.
13
+ */
14
+ writeImageSnapshot(relativeFilePath: string, imageBase64: string): Promise<void>
8
15
  }
9
16
 
10
- export const writeImageSnapshot: BrowserCommand<[filePath: string, base64: string]> = async (
11
- context,
12
- filePath,
13
- base64,
17
+ export const writeImageSnapshot: BrowserCommand<[relativeFilePath: string, imageBase64: string]> = async (
18
+ _,
19
+ relativeFilePath,
20
+ imageBase64,
14
21
  ) => {
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' })
22
+ assertIsRelativePath(relativeFilePath, 'relativeFilePath')
23
+
24
+ await mkdirp(dirname(relativeFilePath))
25
+ await file.writeFile(relativeFilePath, imageBase64, { encoding: 'base64' })
20
26
  }
@@ -17,7 +17,7 @@ export type PartialBrowserCommandContext = {
17
17
  hookTimeout: number
18
18
  }
19
19
  }
20
- testPath: string | undefined
20
+ testPath: string
21
21
  }
22
22
 
23
23
  export function createVisContext() {
@@ -51,7 +51,7 @@ export function createVisContext() {
51
51
  await globalStateReady
52
52
  }
53
53
 
54
- const { suiteId, suite } = createSuite(state, context.testPath!, visOptions)
54
+ const { suiteId, suite } = createSuite(state, context.testPath, visOptions)
55
55
  state.suites[suiteId] = suite
56
56
  await Promise.allSettled([ctx.rimraf(suite.diffDir), ctx.rimraf(suite.resultDir)])
57
57
  },
@@ -123,10 +123,6 @@ async function setupState(suite: PartialBrowserCommandContext, visOptions: VisOp
123
123
  return state
124
124
  }
125
125
 
126
- export function getSuiteId(state: VisState, testPath: string, options: VisOptions) {
127
- return getSnapshotSubpath(relative(state.projectPath, testPath), options)
128
- }
129
-
130
126
  export function createSuite(state: VisState, testPath: string, options: VisOptions) {
131
127
  const suiteId = getSuiteId(state, testPath, options)
132
128
  return {
@@ -139,3 +135,7 @@ export function createSuite(state: VisState, testPath: string, options: VisOptio
139
135
  },
140
136
  }
141
137
  }
138
+
139
+ export function getSuiteId(state: VisState, testPath: string, options: VisOptions) {
140
+ return getSnapshotSubpath(relative(state.projectPath, testPath), options)
141
+ }