vitest-plugin-vis 1.0.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 (204) hide show
  1. package/esm/client/ctx.d.ts +6 -0
  2. package/esm/client/ctx.d.ts.map +1 -0
  3. package/esm/client/ctx.js +7 -0
  4. package/esm/client/expect/expectation_result.d.ts +3 -0
  5. package/esm/client/expect/expectation_result.d.ts.map +1 -0
  6. package/esm/client/expect/expectation_result.js +5 -0
  7. package/esm/client/expect/extend.d.ts +8 -0
  8. package/esm/client/expect/extend.d.ts.map +1 -0
  9. package/esm/client/expect/extend.js +3 -0
  10. package/esm/client/expect/to_match_image_snapshot.d.ts +9 -0
  11. package/esm/client/expect/to_match_image_snapshot.d.ts.map +1 -0
  12. package/esm/client/expect/to_match_image_snapshot.js +32 -0
  13. package/esm/client/expect/to_match_image_snapshot.logic.d.ts +2 -0
  14. package/esm/client/expect/to_match_image_snapshot.logic.d.ts.map +1 -0
  15. package/esm/client/expect/to_match_image_snapshot.logic.js +11 -0
  16. package/esm/client/expect/to_match_image_snapshot.types.d.ts +7 -0
  17. package/esm/client/expect/to_match_image_snapshot.types.d.ts.map +1 -0
  18. package/esm/client/expect/to_match_image_snapshot.types.js +1 -0
  19. package/esm/client/page/extend.d.ts +6 -0
  20. package/esm/client/page/extend.d.ts.map +1 -0
  21. package/esm/client/page/extend.js +3 -0
  22. package/esm/client/page/has_image_snapshot.d.ts +10 -0
  23. package/esm/client/page/has_image_snapshot.d.ts.map +1 -0
  24. package/esm/client/page/has_image_snapshot.js +23 -0
  25. package/esm/client/selector.d.ts +9 -0
  26. package/esm/client/selector.d.ts.map +1 -0
  27. package/esm/client/selector.js +104 -0
  28. package/esm/client/should_take_snapshot.d.ts +10 -0
  29. package/esm/client/should_take_snapshot.d.ts.map +1 -0
  30. package/esm/client/should_take_snapshot.js +13 -0
  31. package/esm/client/snapshot_options.d.ts +34 -0
  32. package/esm/client/snapshot_options.d.ts.map +1 -0
  33. package/esm/client/snapshot_options.internal.d.ts +3 -0
  34. package/esm/client/snapshot_options.internal.d.ts.map +1 -0
  35. package/esm/client/snapshot_options.internal.js +20 -0
  36. package/esm/client/snapshot_options.js +16 -0
  37. package/esm/client/task_id.d.ts +7 -0
  38. package/esm/client/task_id.d.ts.map +1 -0
  39. package/esm/client/task_id.js +10 -0
  40. package/esm/client.d.ts +8 -0
  41. package/esm/client.d.ts.map +1 -0
  42. package/esm/client.js +5 -0
  43. package/esm/commands.d.ts +5 -0
  44. package/esm/commands.d.ts.map +1 -0
  45. package/esm/commands.js +4 -0
  46. package/esm/config/types.d.ts +35 -0
  47. package/esm/config/types.d.ts.map +1 -0
  48. package/esm/config/types.js +1 -0
  49. package/esm/config/vis.d.ts +25 -0
  50. package/esm/config/vis.d.ts.map +1 -0
  51. package/esm/config/vis.js +27 -0
  52. package/esm/config.d.ts +4 -0
  53. package/esm/config.d.ts.map +1 -0
  54. package/esm/config.js +2 -0
  55. package/esm/index.d.ts +2 -0
  56. package/esm/index.d.ts.map +1 -0
  57. package/esm/index.js +2 -0
  58. package/esm/presets/auto.d.ts +2 -0
  59. package/esm/presets/auto.d.ts.map +1 -0
  60. package/esm/presets/auto.js +3 -0
  61. package/esm/presets/manual.d.ts +2 -0
  62. package/esm/presets/manual.d.ts.map +1 -0
  63. package/esm/presets/manual.js +3 -0
  64. package/esm/server/browser_provider/browser_api.d.ts +3 -0
  65. package/esm/server/browser_provider/browser_api.d.ts.map +1 -0
  66. package/esm/server/browser_provider/browser_api.js +11 -0
  67. package/esm/server/browser_provider/playwright.d.ts +4 -0
  68. package/esm/server/browser_provider/playwright.d.ts.map +1 -0
  69. package/esm/server/browser_provider/playwright.js +13 -0
  70. package/esm/server/browser_provider/types.d.ts +6 -0
  71. package/esm/server/browser_provider/types.d.ts.map +1 -0
  72. package/esm/server/browser_provider/types.js +1 -0
  73. package/esm/server/browser_provider/webdriverio.d.ts +4 -0
  74. package/esm/server/browser_provider/webdriverio.d.ts.map +1 -0
  75. package/esm/server/browser_provider/webdriverio.js +10 -0
  76. package/esm/server/commands/extend.d.ts +15 -0
  77. package/esm/server/commands/extend.d.ts.map +1 -0
  78. package/esm/server/commands/extend.js +10 -0
  79. package/esm/server/commands/has_image_snapshot.d.ts +6 -0
  80. package/esm/server/commands/has_image_snapshot.d.ts.map +1 -0
  81. package/esm/server/commands/has_image_snapshot.js +7 -0
  82. package/esm/server/commands/image_snapshot_next_index.d.ts +9 -0
  83. package/esm/server/commands/image_snapshot_next_index.d.ts.map +1 -0
  84. package/esm/server/commands/image_snapshot_next_index.js +7 -0
  85. package/esm/server/commands/is_ci.d.ts +2 -0
  86. package/esm/server/commands/is_ci.d.ts.map +1 -0
  87. package/esm/server/commands/is_ci.js +6 -0
  88. package/esm/server/commands/match_image_snapshot.d.ts +17 -0
  89. package/esm/server/commands/match_image_snapshot.d.ts.map +1 -0
  90. package/esm/server/commands/match_image_snapshot.js +88 -0
  91. package/esm/server/commands/setup_vis_suite.d.ts +13 -0
  92. package/esm/server/commands/setup_vis_suite.d.ts.map +1 -0
  93. package/esm/server/commands/setup_vis_suite.js +4 -0
  94. package/esm/server/compare_image.d.ts +8 -0
  95. package/esm/server/compare_image.d.ts.map +1 -0
  96. package/esm/server/compare_image.js +16 -0
  97. package/esm/server/file.d.ts +6 -0
  98. package/esm/server/file.d.ts.map +1 -0
  99. package/esm/server/file.js +14 -0
  100. package/esm/server/snapshot_path.d.ts +4 -0
  101. package/esm/server/snapshot_path.d.ts.map +1 -0
  102. package/esm/server/snapshot_path.js +9 -0
  103. package/esm/server/vis_context.ctx.d.ts +29 -0
  104. package/esm/server/vis_context.ctx.d.ts.map +1 -0
  105. package/esm/server/vis_context.ctx.js +11 -0
  106. package/esm/server/vis_context.d.ts +33 -0
  107. package/esm/server/vis_context.d.ts.map +1 -0
  108. package/esm/server/vis_context.js +2 -0
  109. package/esm/server/vis_context.logic.d.ts +63 -0
  110. package/esm/server/vis_context.logic.d.ts.map +1 -0
  111. package/esm/server/vis_context.logic.js +116 -0
  112. package/esm/server/vis_context.types.d.ts +20 -0
  113. package/esm/server/vis_context.types.d.ts.map +1 -0
  114. package/esm/server/vis_context.types.js +1 -0
  115. package/esm/setup/create_vis.d.ts +39 -0
  116. package/esm/setup/create_vis.d.ts.map +1 -0
  117. package/esm/setup/create_vis.js +96 -0
  118. package/esm/setup/vis.d.ts +18 -0
  119. package/esm/setup/vis.d.ts.map +1 -0
  120. package/esm/setup/vis.js +6 -0
  121. package/esm/setup.d.ts +5 -0
  122. package/esm/setup.d.ts.map +1 -0
  123. package/esm/setup.js +7 -0
  124. package/esm/shared/base64.d.ts +2 -0
  125. package/esm/shared/base64.d.ts.map +1 -0
  126. package/esm/shared/base64.js +3 -0
  127. package/esm/shared/constants.d.ts +5 -0
  128. package/esm/shared/constants.d.ts.map +1 -0
  129. package/esm/shared/constants.js +4 -0
  130. package/esm/shared/convert_threshold_unit.d.ts +6 -0
  131. package/esm/shared/convert_threshold_unit.d.ts.map +1 -0
  132. package/esm/shared/convert_threshold_unit.js +10 -0
  133. package/esm/shared/get_max_size.d.ts +11 -0
  134. package/esm/shared/get_max_size.d.ts.map +1 -0
  135. package/esm/shared/get_max_size.js +5 -0
  136. package/esm/shared/is_same_size.d.ts +8 -0
  137. package/esm/shared/is_same_size.d.ts.map +1 -0
  138. package/esm/shared/is_same_size.js +3 -0
  139. package/esm/shared/trim_common_folder.d.ts +6 -0
  140. package/esm/shared/trim_common_folder.d.ts.map +1 -0
  141. package/esm/shared/trim_common_folder.js +10 -0
  142. package/esm/shared/types.d.ts +37 -0
  143. package/esm/shared/types.d.ts.map +1 -0
  144. package/esm/shared/types.js +1 -0
  145. package/esm/testing/constants.d.ts +9 -0
  146. package/esm/testing/constants.d.ts.map +1 -0
  147. package/esm/testing/constants.js +1933 -0
  148. package/esm/testing.d.ts +2 -0
  149. package/esm/testing.d.ts.map +1 -0
  150. package/esm/testing.js +1 -0
  151. package/package.json +115 -0
  152. package/readme.md +264 -0
  153. package/src/client/ctx.ts +8 -0
  154. package/src/client/expect/expectation_result.ts +7 -0
  155. package/src/client/expect/extend.ts +12 -0
  156. package/src/client/expect/to_match_image_snapshot.logic.ts +11 -0
  157. package/src/client/expect/to_match_image_snapshot.ts +43 -0
  158. package/src/client/expect/to_match_image_snapshot.types.ts +14 -0
  159. package/src/client/page/extend.ts +8 -0
  160. package/src/client/page/has_image_snapshot.ts +33 -0
  161. package/src/client/selector.ts +118 -0
  162. package/src/client/should_take_snapshot.ts +15 -0
  163. package/src/client/snapshot_options.internal.ts +22 -0
  164. package/src/client/snapshot_options.ts +53 -0
  165. package/src/client/task_id.ts +16 -0
  166. package/src/client.ts +7 -0
  167. package/src/commands.ts +4 -0
  168. package/src/config/readme.md +4 -0
  169. package/src/config/types.ts +35 -0
  170. package/src/config/vis.ts +30 -0
  171. package/src/config.ts +3 -0
  172. package/src/index.ts +2 -0
  173. package/src/presets/auto.ts +4 -0
  174. package/src/presets/manual.ts +4 -0
  175. package/src/server/browser_provider/browser_api.ts +13 -0
  176. package/src/server/browser_provider/playwright.ts +16 -0
  177. package/src/server/browser_provider/types.ts +11 -0
  178. package/src/server/browser_provider/webdriverio.ts +13 -0
  179. package/src/server/commands/extend.ts +19 -0
  180. package/src/server/commands/has_image_snapshot.ts +18 -0
  181. package/src/server/commands/image_snapshot_next_index.ts +17 -0
  182. package/src/server/commands/is_ci.ts +7 -0
  183. package/src/server/commands/match_image_snapshot.ts +139 -0
  184. package/src/server/commands/setup_vis_suite.ts +17 -0
  185. package/src/server/compare_image.ts +34 -0
  186. package/src/server/file.ts +15 -0
  187. package/src/server/readme.md +5 -0
  188. package/src/server/snapshot_path.ts +12 -0
  189. package/src/server/vis_context.ctx.ts +12 -0
  190. package/src/server/vis_context.logic.ts +141 -0
  191. package/src/server/vis_context.ts +3 -0
  192. package/src/server/vis_context.types.ts +25 -0
  193. package/src/setup/create_vis.ts +98 -0
  194. package/src/setup/vis.ts +7 -0
  195. package/src/setup.ts +9 -0
  196. package/src/shared/base64.ts +3 -0
  197. package/src/shared/constants.ts +4 -0
  198. package/src/shared/convert_threshold_unit.ts +13 -0
  199. package/src/shared/get_max_size.ts +5 -0
  200. package/src/shared/is_same_size.ts +3 -0
  201. package/src/shared/trim_common_folder.ts +10 -0
  202. package/src/shared/types.ts +39 -0
  203. package/src/testing/constants.ts +1936 -0
  204. package/src/testing.ts +1 -0
@@ -0,0 +1,2 @@
1
+ export * from './testing/constants.ts';
2
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA"}
package/esm/testing.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./testing/constants.js";
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "vitest-plugin-vis",
3
+ "version": "1.0.0",
4
+ "description": "Vitest visual testing plugin",
5
+ "keywords": [
6
+ "image-snapshot",
7
+ "vitest",
8
+ "visual-test"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/repobuddy/storybook-image-snapshot",
13
+ "directory": "packages/vitest-image-snapshot"
14
+ },
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "Homa Wong",
18
+ "email": "homawong@gmail.com",
19
+ "url": "https://github.com/unional"
20
+ },
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./esm/index.d.ts",
25
+ "default": "./esm/index.js"
26
+ },
27
+ "./config": {
28
+ "types": "./esm/config.d.ts",
29
+ "default": "./esm/config.js"
30
+ },
31
+ "./client": {
32
+ "types": "./esm/client.d.ts",
33
+ "default": "./esm/client.js"
34
+ },
35
+ "./commands": {
36
+ "types": "./esm/commands.d.ts",
37
+ "default": "./esm/commands.js"
38
+ },
39
+ "./presets/auto": {
40
+ "types": "./esm/presets/auto.d.ts",
41
+ "default": "./esm/presets/auto.js"
42
+ },
43
+ "./presets/manual": {
44
+ "types": "./esm/presets/manual.d.ts",
45
+ "default": "./esm/presets/manual.js"
46
+ },
47
+ "./setup": {
48
+ "types": "./esm/setup.d.ts",
49
+ "default": "./esm/setup.js"
50
+ },
51
+ "./testing": {
52
+ "types": "./esm/testing.d.ts",
53
+ "default": "./esm/testing.js"
54
+ },
55
+ "./package.json": "./package.json"
56
+ },
57
+ "files": [
58
+ "esm",
59
+ "src",
60
+ "!**/*.{spec,test,unit,accept,integrate,system,perf,stress}.*"
61
+ ],
62
+ "dependencies": {
63
+ "@types/pixelmatch": "^5.2.6",
64
+ "dedent": "^1.5.1",
65
+ "is-ci": "^4.0.0",
66
+ "mkdirp": "^3.0.1",
67
+ "pathe": "^1.1.2",
68
+ "pixelmatch": "^6.0.0",
69
+ "pngjs": "^7.0.0",
70
+ "rimraf": "^6.0.1",
71
+ "type-plus": "^8.0.0-beta.6"
72
+ },
73
+ "devDependencies": {
74
+ "@repobuddy/typescript": "^2.1.0",
75
+ "@types/is-ci": "^3.0.4",
76
+ "@types/pngjs": "^6.0.5",
77
+ "@types/react": "^19.0.2",
78
+ "@vitest/browser": "^2.1.8",
79
+ "@vitest/coverage-istanbul": "^2.1.8",
80
+ "@vitest/coverage-v8": "^2.1.8",
81
+ "@vitest/expect": "^2.1.8",
82
+ "cross-env": "^7.0.3",
83
+ "npm-run-all2": "^7.0.2",
84
+ "playwright": "^1.49.0",
85
+ "react": "^19.0.0",
86
+ "react-dom": "^19.0.0",
87
+ "typescript": "^5.7.2",
88
+ "vite": "^5.3.5",
89
+ "vitest": "^2.1.3",
90
+ "vitest-browser-react": "^0.0.4",
91
+ "webdriverio": "^9.4.1"
92
+ },
93
+ "peerDependencies": {
94
+ "@vitest/browser": "^2.1.3"
95
+ },
96
+ "publishConfig": {
97
+ "access": "public"
98
+ },
99
+ "scripts": {
100
+ "build": "tsc",
101
+ "build:watch": "tsc -w",
102
+ "clean": "rimraf coverage esm *.tsbuildinfo vitest.*.timestamp*",
103
+ "cov": "cross-env COVERAGE=true vitest run --coverage",
104
+ "coverage": "cross-env COVERAGE=true vitest run --coverage",
105
+ "dev": "run-p build:watch w",
106
+ "nuke": "rimraf node_modules",
107
+ "test": "vitest run",
108
+ "v": "run-p build cov",
109
+ "w": "vitest",
110
+ "w:cov": "cross-env COVERAGE=true vitest --coverage",
111
+ "w:node": "vitest --project=vpv:node",
112
+ "w:pw": "vitest --project=vpv:pw",
113
+ "w:wd": "vitest --project=vpv:wd"
114
+ }
115
+ }
package/readme.md ADDED
@@ -0,0 +1,264 @@
1
+ # Vitest Visual Testing Plugin
2
+
3
+ Vitest visual testing plugin allowing you to capture and compare image snapshots automatically and manually.
4
+
5
+ It requires [Vitest Browser Mode][vitest-browser-mode] to work.
6
+
7
+ This plugin is inspired by [`jest-image-snapshot`][jest-image-snapshot],
8
+ and extracted from [`storybook-addon-vis`][storybook-addon-vis] for better modularity.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install --save-dev vitest-plugin-vis
14
+
15
+ pnpm add --save-dev vitest-plugin-vis
16
+
17
+ yarn add --save-dev vitest-plugin-vis
18
+ ```
19
+
20
+ ## Config
21
+
22
+ Vitest visual testing plugin supports zero-config setup.
23
+
24
+ ```ts
25
+ // vitest.config.ts
26
+ import { defineConfig } from 'vitest/config'
27
+ import { vis } from 'vitest-plugin-vis/config'
28
+
29
+ export default defineConfig({
30
+ plugins: [vis()],
31
+ test: {
32
+ browser: {
33
+ // typical browser config
34
+ enabled: true,
35
+ provider: 'playwright',
36
+ name: 'chromium'
37
+ }
38
+ }
39
+ })
40
+ ```
41
+
42
+ This default configuration will:
43
+
44
+ - Use the `auto` preset, taking image snapshot at the end on each rendering test.
45
+ - Set config to compare image snapshot with a failure threshold of `0 pixels`.
46
+ - Local (non-CI) image snapshots are saved in the `<root>/__vis__/local` directory.
47
+ - CI image snapshots are saved in the `<root>/__vis__/<process.platform>` directory.
48
+ - Image snapshots of the current test run are saved in the `<root>/__vis__/__results__` directory.
49
+ - Diff images are saved in the `<root>/__vis__/__diffs__` directory.
50
+
51
+ You can customize the configuration:
52
+
53
+ ```ts
54
+ // vitest.config.ts
55
+ import { defineConfig } from 'vitest/config'
56
+ import { vis } from 'vitest-plugin-vis/config'
57
+
58
+ export default defineConfig({
59
+ plugins: [
60
+ vis({
61
+ preset: 'auto', // preset is disabled by default when you provide a custom config
62
+ snapshotRootDir: '__vis__',
63
+ customizeSnapshotSubpath: (subpath) => subpath,
64
+ customizeSnapshotId: (id, index) => `${id}-${index}`,
65
+ diffOptions: undefined, // pixelmatch options
66
+ failureThresholdType: 'pixel',
67
+ failureThreshold: 0,
68
+ timeout: 5000 // 30000 on CI
69
+ })
70
+ ],
71
+ test:{
72
+ browser:{/* ... */}
73
+ }
74
+ })
75
+ ```
76
+
77
+ If you want to do a custom snapshot at the end of each test,
78
+ such as taking a snapshot for each theme,
79
+ you can set it up using a custom setup file:
80
+
81
+ ```ts
82
+ // vitest.config.ts
83
+ import { defineConfig } from 'vitest/config'
84
+ import { vis } from 'vitest-plugin-vis/config'
85
+
86
+ export default defineConfig({
87
+ plugins: [
88
+ // preset is disabled by default when you provide a custom config
89
+ vis({}) // or vis({ preset: 'none' })
90
+ ],
91
+ test:{
92
+ browser:{/* ... */},
93
+ setupFiles: ['vitest.setup.ts']
94
+ }
95
+ })
96
+
97
+ // vitest.setup.ts
98
+ import { vis } from 'vitest-plugin-vis/setup'
99
+
100
+ vis.presets.theme({
101
+ light() { document.body.classList.remove('dark') },
102
+ dark() { document.body.classList.add('dark') },
103
+ })
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ ### Auto Snapshot
109
+
110
+ By default, the plugin will use the `auto` preset, which will take a snapshot at the end of each test.
111
+
112
+ You can control how the auto snapshot is taken using the `setAutoSnapshotOptions` function:
113
+
114
+ ```ts
115
+ import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
116
+ import { beforeEach, it } from 'vitest'
117
+
118
+ beforeAll((suite) => {
119
+ // Apply options to all tests in the current suite (file)
120
+ setAutoSnapshotOptions(suite, /* options */)
121
+ })
122
+
123
+ beforeEach(({ task }) => {
124
+ // Apply options to all tests in the current scope
125
+ setAutoSnapshotOptions(task, /* options */)
126
+ // or
127
+ setAutoSnapshotOptions(/* options */)
128
+ })
129
+
130
+
131
+ it('disable snapshot per test', async ({ task }) => {
132
+ // Apply options to this test only
133
+ setAutoSnapshotOptions(task, /* options */)
134
+ // or
135
+ setAutoSnapshotOptions(/* options */)
136
+ })
137
+
138
+ describe('nested scope', () => {
139
+ beforeEach(({ task }) => {
140
+ // Apply options to all tests in the current scope
141
+ setAutoSnapshotOptions(task, /* options */)
142
+ // or
143
+ setAutoSnapshotOptions(/* options */)
144
+ })
145
+ })
146
+ ```
147
+
148
+ ### Manual Snapshot
149
+
150
+ You can also set the preset to `manual` and compare snapshots manually:
151
+
152
+ ```ts
153
+ // vitest.config.ts
154
+ import { defineConfig } from 'vitest/config'
155
+
156
+ export default defineConfig({
157
+ plugins: [
158
+ vis({ preset: 'manual' })
159
+ ],
160
+ test:{
161
+ browser:{/* ... */}
162
+ }
163
+ })
164
+
165
+ // some.test.ts
166
+ import 'vitest-browser-react' // do this in your setup file to get `page.render`
167
+ import { page } from '@vitest/browser/context'
168
+ import { expect, it } from 'vitest'
169
+
170
+ it('manual snapshot', async () => {
171
+ page.render(<div data-testid="subject">hello world</div>)
172
+ await expect(document.body).toMatchImageSnapshot(/* options */)
173
+ // or
174
+ const subject = page.getByTestId('subject')
175
+ await expect(subject).toMatchImageSnapshot(/* options */)
176
+ })
177
+ ```
178
+
179
+ You can customize the snapshot comparison options per assertion:
180
+
181
+ ```ts
182
+ // some.test.ts
183
+ import 'vitest-browser-react' // do this in your setup file to get `page.render`
184
+ import { page } from '@vitest/browser/context'
185
+ import { expect, it } from 'vitest'
186
+
187
+ it('manual snapshot with options', async () => {
188
+ page.render(<div data-testid="subject">hello world</div>)
189
+ const subject = page.getByTestId('subject')
190
+ await expect(subject).toMatchImageSnapshot({
191
+ customizeSnapshotId: (id, index) => `${id}-${index}`,
192
+ failureThreshold: 0.01,
193
+ failureThresholdType: 'percent',
194
+ diffOptions: {
195
+ threshold: 0.1
196
+ },
197
+ timeout: 15000
198
+ })
199
+ })
200
+ ```
201
+
202
+ ### Has Snapshot
203
+
204
+ While less common, you can also check if a snapshot exists:
205
+
206
+ ```ts
207
+ import { page } from '@vitest/browser/context'
208
+ import { expect, it } from 'vitest'
209
+
210
+ it('Has Snapshot', async () => {
211
+ const hasSnapshot = await page.hasImageSnapshot(/* options */)
212
+ if (!hasSnapshot) {
213
+ // do something
214
+ }
215
+ else {
216
+ // do something else
217
+ }
218
+ })
219
+ ```
220
+
221
+ This is useful when you are performing some negative test.
222
+
223
+ ## Git Ignore
224
+
225
+ The local snapshots, current run results, and diffs should be ignored by git.
226
+ Add the following lines to your `.gitignore` file:
227
+
228
+ ```sh
229
+ **/__vis__/__diffs__
230
+ **/__vis__/__results__
231
+ **/__vis__/local
232
+ ```
233
+
234
+ ## Vitest Browser Mode
235
+
236
+ Vitest visual testing plugin runs on [Vitest Browser Mode][vitest-browser-mode].
237
+ Please follow its guide to set up your environment.
238
+
239
+ Bonus note, if you want to install [Firefox] on WSL,
240
+ you can follow these steps: [Install Firefox on Ubuntu 22.04](https://askubuntu.com/a/1444967).
241
+
242
+ ## Running on CI
243
+
244
+ When running on CI, the plugin will save the image snapshots in the `<root>/__vis__/<process.platform>` directory.
245
+
246
+ The image snapshots are taken on the server side using `playwright` or `webdriverio` depending on your browser provider.
247
+ It is recommended to run your tests serially to avoid flakiness.
248
+
249
+ ## FAQ
250
+
251
+ > feature X in [`jest-image-snapshot`][jest-image-snapshot] is missing
252
+
253
+ Some features in [`jest-image-snapshot`][jest-image-snapshot] are not implemented in [`vitest-plugin-vis`][vitest-plugin-vis] yet.
254
+ This is because through our own usage, we do not found a good use case for them.
255
+ For example, the `ssim` comparison method is not implemented in [`vitest-plugin-vis`][vitest-plugin-vis],
256
+ because we found that figuring out what is the delta using `ssim` is quite difficult.
257
+
258
+ If you have a good use case for these features, please open an issue or PR.
259
+
260
+ [firefox]: https://www.mozilla.org/en-US/firefox/
261
+ [jest-image-snapshot]: https://github.com/americanexpress/jest-image-snapshot
262
+ [storybook-addon-vis]: https://github.com/repobuddy/storybook-addon-vis
263
+ [vitest-browser-mode]: https://vitest.dev/guide/browser/
264
+ [vitest-plugin-vis]: https://www.npmjs.com/package/vitest-plugin-vis
@@ -0,0 +1,8 @@
1
+ import { getCurrentTest } from 'vitest/suite'
2
+
3
+ export const ctx = {
4
+ getCurrentTest,
5
+ __test__reset() {
6
+ ctx.getCurrentTest = getCurrentTest
7
+ },
8
+ }
@@ -0,0 +1,7 @@
1
+ import type { SyncExpectationResult } from '@vitest/expect'
2
+
3
+ export const success: SyncExpectationResult = {
4
+ pass: true,
5
+ /* v8 ignore next */
6
+ message: () => '',
7
+ }
@@ -0,0 +1,12 @@
1
+ import { expect } from 'vitest'
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
+ }
11
+
12
+ expect.extend({ toMatchImageSnapshot })
@@ -0,0 +1,11 @@
1
+ import { isBase64String } from '../../shared/base64.ts'
2
+ import { convertElementToCssSelector } from '../selector.ts'
3
+
4
+ export function parseImageSnapshotSubject(subject: any) {
5
+ if (subject instanceof Element) return convertElementToCssSelector(subject)
6
+ if (subject?.['selector']) return subject['selector']
7
+ if (isBase64String(subject)) return subject
8
+ throw new Error(
9
+ `'toMatchImageSnapshot()' expects the subject to be an element, locator, or image encoded in base64 string, but got: ${subject}`,
10
+ )
11
+ }
@@ -0,0 +1,43 @@
1
+ import { commands } from '@vitest/browser/context'
2
+ import type { AsyncExpectationResult } from '@vitest/expect'
3
+ import { ctx } from '../ctx.ts'
4
+ import { toTaskId } from '../task_id.ts'
5
+ import { success } from './expectation_result.ts'
6
+ import { parseImageSnapshotSubject } from './to_match_image_snapshot.logic.ts'
7
+ import type { ToMatchImageSnapshotOptions } from './to_match_image_snapshot.types.ts'
8
+
9
+ export function toMatchImageSnapshot(
10
+ /**
11
+ * The element or locator to take the snapshot of,
12
+ * or the base64 value of the image to compare against.
13
+ */
14
+ subject: any,
15
+ options?: ToMatchImageSnapshotOptions | undefined,
16
+ ): AsyncExpectationResult {
17
+ const test = ctx.getCurrentTest()
18
+ if (!test) {
19
+ throw new Error('`toMatchImageSnapshot()` must be called in a test.')
20
+ }
21
+
22
+ if (test.concurrent) {
23
+ throw new Error(
24
+ '`toMatchImageSnapshot()` cannot be called in a concurrent test because ' +
25
+ "concurrent tests run at the same time in the same iframe and affect each other's environment. ",
26
+ )
27
+ }
28
+
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)
43
+ }
@@ -0,0 +1,14 @@
1
+ import type {
2
+ ImageSnapshotCompareOptions,
3
+ ImageSnapshotIdOptions,
4
+ ImageSnapshotTimeoutOptions,
5
+ } from '../../shared/types.ts'
6
+
7
+ export interface ImageSnapshotMatcher {
8
+ toMatchImageSnapshot(options?: ToMatchImageSnapshotOptions | undefined): Promise<void>
9
+ }
10
+
11
+ export interface ToMatchImageSnapshotOptions
12
+ extends ImageSnapshotTimeoutOptions,
13
+ ImageSnapshotIdOptions,
14
+ ImageSnapshotCompareOptions {}
@@ -0,0 +1,8 @@
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
+ }
7
+
8
+ page.extend({ hasImageSnapshot })
@@ -0,0 +1,33 @@
1
+ import { type BrowserPage, commands } from '@vitest/browser/context'
2
+ import type { ImageSnapshotIdOptions } from '../../shared/types.ts'
3
+ import { ctx } from '../ctx.ts'
4
+ import { toTaskId } from '../task_id.ts'
5
+
6
+ export interface HasImageSnapshotAction {
7
+ hasImageSnapshot(this: BrowserPage, options?: ImageSnapshotIdOptions | undefined): Promise<boolean>
8
+ }
9
+
10
+ /**
11
+ * Check if the snapshot image exists.
12
+ */
13
+ export function hasImageSnapshot(this: BrowserPage, options?: ImageSnapshotIdOptions | undefined) {
14
+ const test = ctx.getCurrentTest()
15
+ if (!test) {
16
+ throw new Error('`hasImageSnapshot()` must be called in a test.')
17
+ }
18
+
19
+ if (test.concurrent) {
20
+ throw new Error(
21
+ '`hasImageSnapshot()` cannot be called in a concurrent test because ' +
22
+ "concurrent tests run at the same time in the same iframe and affect each other's environment. ",
23
+ )
24
+ }
25
+ const taskId = toTaskId(test)
26
+ if (options?.customizeSnapshotId) {
27
+ return commands
28
+ .imageSnapshotNextIndex(taskId)
29
+ .then((index) => commands.hasImageSnapshot(taskId, options.customizeSnapshotId!(taskId, index)))
30
+ }
31
+
32
+ return commands.hasImageSnapshot(taskId)
33
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Most of the code in this file is copied from Vitest.
3
+ *
4
+ * @see https://github.com/vitest-dev/vitest/blob/main/packages/browser/src/client/utils.ts
5
+ */
6
+ import type { Locator } from '@vitest/browser/context'
7
+
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
+ export function convertElementToCssSelector(element: Element) {
21
+ if (!element || !(element instanceof Element)) {
22
+ throw new Error(`Expected DOM element to be an instance of Element, received ${typeof element}`)
23
+ }
24
+
25
+ return getUniqueCssSelector(element)
26
+ }
27
+
28
+ function escapeIdForCSSSelector(id: string) {
29
+ return id
30
+ .split('')
31
+ .map((char) => {
32
+ const code = char.charCodeAt(0)
33
+
34
+ if (
35
+ char === ' ' ||
36
+ char === '#' ||
37
+ char === '.' ||
38
+ char === ':' ||
39
+ char === '[' ||
40
+ char === ']' ||
41
+ char === '>' ||
42
+ char === '+' ||
43
+ char === '~' ||
44
+ char === '\\'
45
+ ) {
46
+ // Escape common special characters with backslashes
47
+ return `\\${char}`
48
+ }
49
+ if (code >= 0x10000) {
50
+ // Unicode escape for characters outside the BMP
51
+ return `\\${code.toString(16).toUpperCase().padStart(6, '0')} `
52
+ }
53
+ if (code < 0x20 || code === 0x7f) {
54
+ // Non-printable ASCII characters (0x00-0x1F and 0x7F) are escaped
55
+ return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
56
+ }
57
+ if (code >= 0x80) {
58
+ // Non-ASCII characters (0x80 and above) are escaped
59
+ return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
60
+ }
61
+ // Allowable characters are used directly
62
+ return char
63
+ })
64
+ .join('')
65
+ }
66
+
67
+ function getUniqueCssSelector(el: Element) {
68
+ const path = []
69
+ let parent: null | ParentNode
70
+ let hasShadowRoot = false
71
+ // eslint-disable-next-line no-cond-assign
72
+ while ((parent = getParent(el))) {
73
+ if ((parent as Element).shadowRoot) {
74
+ hasShadowRoot = true
75
+ }
76
+
77
+ const tag = el.tagName
78
+ if (el.id) {
79
+ path.push(`#${escapeIdForCSSSelector(el.id)}`)
80
+ } else if (!el.nextElementSibling && !el.previousElementSibling) {
81
+ path.push(tag.toLowerCase())
82
+ } else {
83
+ let index = 0
84
+ let sameTagSiblings = 0
85
+ let elementIndex = 0
86
+
87
+ for (const sibling of parent.children) {
88
+ index++
89
+ if (sibling.tagName === tag) {
90
+ sameTagSiblings++
91
+ }
92
+ if (sibling === el) {
93
+ elementIndex = index
94
+ }
95
+ }
96
+
97
+ if (sameTagSiblings > 1) {
98
+ path.push(`${tag.toLowerCase()}:nth-child(${elementIndex})`)
99
+ } else {
100
+ path.push(tag.toLowerCase())
101
+ }
102
+ }
103
+ el = parent as Element
104
+ }
105
+ return `${getBrowserState().provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`
106
+ }
107
+
108
+ function getParent(el: Element) {
109
+ const parent = el.parentNode
110
+ if (parent instanceof ShadowRoot) {
111
+ return parent.host
112
+ }
113
+ return parent
114
+ }
115
+ function getBrowserState(): { provider: 'webdriverio' | 'playwright' } {
116
+ // @ts-expect-error not typed global
117
+ return window.__vitest_browser_runner__
118
+ }
@@ -0,0 +1,15 @@
1
+ import { ctx } from './ctx.ts'
2
+ import { getAutoSnapshotOptions } from './snapshot_options.internal.ts'
3
+ import type { SnapshotMeta } from './snapshot_options.ts'
4
+
5
+ /**
6
+ * Determine should snapshot be taken.
7
+ *
8
+ * not story: false
9
+ * no `snapshot` tag: false
10
+ * disabled by `!snapshot` tag: false
11
+ */
12
+ export function shouldTakeSnapshot(meta?: SnapshotMeta | undefined) {
13
+ const m = meta ?? getAutoSnapshotOptions(ctx.getCurrentTest())
14
+ return document.body.childElementCount > 0 && m?.enable !== false
15
+ }