vitest-plugin-vis 3.1.0 → 3.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.
@@ -0,0 +1,298 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="expect/toMatchImageSnapshot" />
4
+
5
+ # toMatchImageSnapshot
6
+
7
+ `expect().toMatchImageSnapshot()` is a custom matcher that compares the current DOM to a snapshot image.
8
+
9
+ It can be used whether you have enabled the `auto` preset or not.
10
+
11
+ But typically, you would disable the auto snapshot when doing manual snapshot testing.
12
+
13
+ You can disable the auto snapshot at various levels:
14
+
15
+ ```ts
16
+ // vitest.setup.ts
17
+ import { vis } from 'vitest-plugin-vis/setup'
18
+
19
+ // Setup without auto snapshots
20
+ vis.setup()
21
+ ```
22
+
23
+ ```ts
24
+ // some.test.ts
25
+ import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
26
+
27
+ beforeAll(() => {
28
+ setAutoSnapshotOptions(false)
29
+ })
30
+
31
+ beforeEach(() => {
32
+ setAutoSnapshotOptions(false)
33
+ })
34
+
35
+ it('...', () => {
36
+ setAutoSnapshotOptions(false)
37
+ })
38
+ ```
39
+
40
+ ## Basic Usage
41
+
42
+ ```ts
43
+ // some.test.ts
44
+ import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
45
+
46
+ it('should take snapshot manually', async ({ expect }) => {
47
+ // Your test code here
48
+ render(<MyComponent />)
49
+
50
+ // Manual snapshot
51
+ await expect(document.body).toMatchImageSnapshot()
52
+ })
53
+ ```
54
+
55
+ ## Options
56
+
57
+ The `toMatchImageSnapshot()` matcher accepts an optional options object with the following properties:
58
+
59
+ ### Snapshot Identification
60
+
61
+ #### `snapshotKey`
62
+
63
+ Customize the snapshot key/name for the snapshot file.
64
+
65
+ ```ts
66
+ it('should use custom snapshot key', async ({ expect }) => {
67
+ render(<MyComponent />)
68
+
69
+ await expect(document.body).toMatchImageSnapshot({
70
+ snapshotKey: 'my_custom_snapshot'
71
+ })
72
+ })
73
+ ```
74
+
75
+ ### Failure Threshold
76
+
77
+ #### `failureThreshold` and `failureThresholdType`
78
+
79
+ Control how much difference is allowed before the test fails.
80
+
81
+ ```ts
82
+ // Allow up to 50 pixels to be different
83
+ it('should allow pixel differences', async ({ expect }) => {
84
+ render(<MyComponent />)
85
+
86
+ await expect(document.body).toMatchImageSnapshot({
87
+ failureThreshold: 50,
88
+ failureThresholdType: 'pixel' // default
89
+ })
90
+ })
91
+
92
+ // Allow up to 1% difference
93
+ it('should allow percentage differences', async ({ expect }) => {
94
+ render(<MyComponent />)
95
+
96
+ await expect(document.body).toMatchImageSnapshot({
97
+ failureThreshold: 0.01,
98
+ failureThresholdType: 'percent'
99
+ })
100
+ })
101
+ ```
102
+
103
+ ### Comparison Methods
104
+
105
+ #### `comparisonMethod`
106
+
107
+ Choose between pixel-by-pixel comparison or structural similarity comparison.
108
+
109
+ ```ts
110
+ // Default pixel comparison
111
+ it('should use pixel comparison', async ({ expect }) => {
112
+ render(<MyComponent />)
113
+
114
+ await expect(document.body).toMatchImageSnapshot({
115
+ comparisonMethod: 'pixel' // default
116
+ })
117
+ })
118
+
119
+ // SSIM (Structural Similarity) comparison
120
+ it('should use SSIM comparison', async ({ expect }) => {
121
+ render(<MyComponent />)
122
+
123
+ await expect(document.body).toMatchImageSnapshot({
124
+ comparisonMethod: 'ssim'
125
+ })
126
+ })
127
+ ```
128
+
129
+ ### Diff Options
130
+
131
+ #### `diffOptions` for Pixel Comparison
132
+
133
+ When using `comparisonMethod: 'pixel'` (default), you can customize pixelmatch options:
134
+
135
+ ```ts
136
+ it('should use custom pixel diff options', async ({ expect }) => {
137
+ render(<MyComponent />)
138
+
139
+ await expect(document.body).toMatchImageSnapshot({
140
+ comparisonMethod: 'pixel',
141
+ diffOptions: {
142
+ threshold: 0.1, // Matching threshold (0-1, default: 0.1)
143
+ includeAA: false, // Include anti-aliased pixels (default: false)
144
+ alpha: 0.1, // Blending factor for unchanged pixels (default: 0.1)
145
+ aaColor: [255, 255, 0], // Color for anti-aliased pixels [R, G, B]
146
+ diffColor: [255, 0, 0], // Color for different pixels [R, G, B]
147
+ diffColorAlt: null, // Alternative color for dark-on-light differences
148
+ diffMask: false // Draw diff over transparent background (default: false)
149
+ }
150
+ })
151
+ })
152
+ ```
153
+
154
+ #### `diffOptions` for SSIM Comparison
155
+
156
+ When using `comparisonMethod: 'ssim'`, you can customize SSIM options:
157
+
158
+ ```ts
159
+ it('should use custom SSIM diff options', async ({ expect }) => {
160
+ render(<MyComponent />)
161
+
162
+ await expect(document.body).toMatchImageSnapshot({
163
+ comparisonMethod: 'ssim',
164
+ diffOptions: {
165
+ ssim: 'bezkrovny', // 'bezkrovny' (fast, default) | 'fast' (accurate) | 'weber' | 'original'
166
+ windowSize: 11, // Window size for SSIM calculation (default: 11)
167
+ k1: 0.01, // First stability constant (default: 0.01)
168
+ k2: 0.03, // Second stability constant (default: 0.03)
169
+ bitDepth: 8, // Bits per channel (default: 8)
170
+ downsample: 'original' // 'original' | 'fast' | false
171
+ }
172
+ })
173
+ })
174
+ ```
175
+
176
+ ### Timeout
177
+
178
+ #### `timeout`
179
+
180
+ Set a timeout for taking the snapshot.
181
+
182
+ ```ts
183
+ it('should use custom timeout', async ({ expect }) => {
184
+ render(<MyComponent />)
185
+
186
+ await expect(document.body).toMatchImageSnapshot({
187
+ timeout: 60000 // 60 seconds (default: 30000ms)
188
+ })
189
+ })
190
+ ```
191
+
192
+ ### Testing Utilities
193
+
194
+ #### `expectToFail`
195
+
196
+ Expect the matcher to fail - useful for testing that changes are detected.
197
+
198
+ ```ts
199
+ it('should detect differences', async ({ expect }) => {
200
+ render(<MyComponent />)
201
+
202
+ // First, create the baseline
203
+ if (!(await page.hasImageSnapshot({ snapshotKey: 'test_changes' }))) {
204
+ await expect(document.body).toMatchImageSnapshot({
205
+ snapshotKey: 'test_changes'
206
+ })
207
+ return
208
+ }
209
+
210
+ // Modify the component
211
+ document.body.style.backgroundColor = 'red'
212
+
213
+ // Expect the test to fail due to changes
214
+ await expect(document.body)
215
+ .toMatchImageSnapshot({
216
+ snapshotKey: 'test_changes',
217
+ expectToFail: true
218
+ })
219
+ .then(
220
+ () => {
221
+ throw new Error('Should not reach - expected failure')
222
+ },
223
+ (error) => {
224
+ expect(error.message).toMatch(/Expected image to match but was differ by \d+ pixels/)
225
+ }
226
+ )
227
+ })
228
+ ```
229
+
230
+ ## Complete Example
231
+
232
+ Here's a comprehensive example showing multiple options:
233
+
234
+ ```ts
235
+ it('should demonstrate all options', async ({ expect }) => {
236
+ render(<MyComponent />)
237
+
238
+ await expect(document.body).toMatchImageSnapshot({
239
+ // Snapshot identification
240
+ snapshotKey: 'comprehensive_test',
241
+
242
+ // Failure threshold
243
+ failureThreshold: 0.01,
244
+ failureThresholdType: 'percent',
245
+
246
+ // Comparison method
247
+ comparisonMethod: 'ssim',
248
+
249
+ // SSIM-specific options
250
+ diffOptions: {
251
+ ssim: 'bezkrovny',
252
+ windowSize: 11,
253
+ k1: 0.01,
254
+ k2: 0.03
255
+ },
256
+
257
+ // Timeout
258
+ timeout: 45000,
259
+
260
+ // Testing utility
261
+ expectToFail: false
262
+ })
263
+ })
264
+ ```
265
+
266
+ ## Comparison Method Recommendations
267
+
268
+ ### When to use Pixel Comparison (`comparisonMethod: 'pixel'`)
269
+
270
+ - **Default choice** for most visual regression tests
271
+ - **Precise pixel-by-pixel** comparison
272
+ - **Good for detecting** small UI changes, text changes, layout shifts
273
+ - **Fast execution** for most image sizes
274
+ - **Customizable colors** for diff visualization
275
+
276
+ ### When to use SSIM Comparison (`comparisonMethod: 'ssim'`)
277
+
278
+ - **Structural similarity** comparison that correlates better with human perception
279
+ - **Reduced false positives** - less likely to fail on minor rendering differences
280
+ - **Better for images** with gradients, shadows, or anti-aliasing
281
+ - **Recommended threshold**: 1% (`failureThreshold: 0.01, failureThresholdType: 'percent'`)
282
+ - **Performance considerations**: Use `ssim: 'bezkrovny'` for speed, `ssim: 'fast'` for accuracy
283
+
284
+ ```ts
285
+ // Recommended SSIM configuration
286
+ it('should use recommended SSIM settings', async ({ expect }) => {
287
+ render(<MyComponent />)
288
+
289
+ await expect(document.body).toMatchImageSnapshot({
290
+ comparisonMethod: 'ssim',
291
+ failureThreshold: 0.01,
292
+ failureThresholdType: 'percent',
293
+ diffOptions: {
294
+ ssim: 'bezkrovny' // Fast and accurate for most cases
295
+ }
296
+ })
297
+ })
298
+ ```
@@ -0,0 +1,328 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="page/hasImageSnapshot" />
4
+
5
+ # page.hasImageSnapshot
6
+
7
+ `page.hasImageSnapshot()` is a utility method that checks if an image snapshot exists for the current test.
8
+
9
+ This method allows you to conditionally execute code based on whether a snapshot has been previously created for the current test. It's particularly useful for negative testing scenarios, conditional snapshot creation, and handling different behavior for first-time runs vs. subsequent runs.
10
+
11
+ ## Basic Usage
12
+
13
+ ```ts
14
+ import { page } from '@vitest/browser/context'
15
+
16
+ it('should check if snapshot exists', async ({ expect }) => {
17
+ const hasSnapshot = await page.hasImageSnapshot()
18
+
19
+ if (!hasSnapshot) {
20
+ // First run - create baseline snapshot
21
+ await page.toMatchImageSnapshot()
22
+ } else {
23
+ // Subsequent runs - perform comparison
24
+ await expect(page.getByTestId('component')).toMatchImageSnapshot()
25
+ }
26
+ })
27
+ ```
28
+
29
+ ## Options
30
+
31
+ The `page.hasImageSnapshot()` method accepts an optional options object:
32
+
33
+ ### `snapshotKey`
34
+
35
+ Custom key to identify the specific snapshot. If not provided, uses the default snapshot key for the test.
36
+
37
+ ```ts
38
+ it('should check for specific snapshot', async ({ expect }) => {
39
+ // Check for default snapshot
40
+ const hasDefaultSnapshot = await page.hasImageSnapshot()
41
+
42
+ // Check for custom snapshot
43
+ const hasCustomSnapshot = await page.hasImageSnapshot({
44
+ snapshotKey: 'dark_mode'
45
+ })
46
+
47
+ expect(hasDefaultSnapshot).toBe(false)
48
+ expect(hasCustomSnapshot).toBe(false)
49
+ })
50
+ ```
51
+
52
+ **Important**: The `snapshotKey` cannot contain dash characters (-) as they are reserved for internal path separation.
53
+
54
+ ```ts
55
+ // ❌ This will throw an error
56
+ await page.hasImageSnapshot({ snapshotKey: 'invalid-key' })
57
+ // Error: Snapshot key cannot contain dash
58
+
59
+ // ✅ Use underscores or camelCase instead
60
+ await page.hasImageSnapshot({ snapshotKey: 'valid_key' })
61
+ await page.hasImageSnapshot({ snapshotKey: 'validKey' })
62
+ ```
63
+
64
+ ## Use Cases
65
+
66
+ ### Negative Testing
67
+
68
+ Test behavior when snapshots don't exist:
69
+
70
+ ```ts
71
+ it('should handle missing snapshots gracefully', async ({ expect }) => {
72
+ const hasSnapshot = await page.hasImageSnapshot({ snapshotKey: 'nonexistent' })
73
+ expect(hasSnapshot).toBe(false)
74
+
75
+ // Now create the snapshot
76
+ await page.toMatchImageSnapshot({ snapshotKey: 'nonexistent' })
77
+
78
+ // Verify it now exists
79
+ const hasSnapshotAfter = await page.hasImageSnapshot({ snapshotKey: 'nonexistent' })
80
+ expect(hasSnapshotAfter).toBe(true)
81
+ })
82
+ ```
83
+
84
+ ### Conditional Snapshot Creation
85
+
86
+ Handle different behavior for first-time runs vs. subsequent runs:
87
+
88
+ ```ts
89
+ it('should create or compare snapshots conditionally', async ({ expect }) => {
90
+ const hasSnapshot = await page.hasImageSnapshot()
91
+
92
+ if (!hasSnapshot) {
93
+ // First run - setup and create baseline
94
+ render(<MyComponent theme="light" />)
95
+ await page.toMatchImageSnapshot()
96
+ } else {
97
+ // Subsequent runs - test different scenarios
98
+ render(<MyComponent theme="dark" />)
99
+ await expect(page.getByTestId('component')).toMatchImageSnapshot({
100
+ expectToFail: true,
101
+ snapshotKey: 'dark_theme'
102
+ })
103
+ }
104
+ })
105
+ ```
106
+
107
+ ### Theme or State-Specific Snapshots
108
+
109
+ Work with different themes or application states:
110
+
111
+ ```ts
112
+ it('should support theme-specific snapshots', async ({ expect }) => {
113
+ // Check for dark mode snapshot
114
+ const hasDarkSnapshot = await page.hasImageSnapshot({ snapshotKey: 'dark_mode' })
115
+
116
+ if (!hasDarkSnapshot) {
117
+ // Setup dark mode and create snapshot
118
+ document.body.classList.add('dark')
119
+ render(<MyApp />)
120
+ await page.toMatchImageSnapshot({ snapshotKey: 'dark_mode' })
121
+ }
122
+
123
+ // Check for light mode snapshot
124
+ const hasLightSnapshot = await page.hasImageSnapshot({ snapshotKey: 'light_mode' })
125
+
126
+ if (!hasLightSnapshot) {
127
+ // Setup light mode and create snapshot
128
+ document.body.classList.remove('dark')
129
+ render(<MyApp />)
130
+ await page.toMatchImageSnapshot({ snapshotKey: 'light_mode' })
131
+ }
132
+ })
133
+ ```
134
+
135
+ ### Progressive Snapshot Building
136
+
137
+ Build up complex test scenarios progressively:
138
+
139
+ ```ts
140
+ it('should build snapshots progressively', async ({ expect }) => {
141
+ // Check if we have the base component snapshot
142
+ const hasBaseSnapshot = await page.hasImageSnapshot({ snapshotKey: 'base' })
143
+
144
+ if (!hasBaseSnapshot) {
145
+ render(<BaseComponent />)
146
+ await page.toMatchImageSnapshot({ snapshotKey: 'base' })
147
+ }
148
+
149
+ // Check if we have the enhanced component snapshot
150
+ const hasEnhancedSnapshot = await page.hasImageSnapshot({ snapshotKey: 'enhanced' })
151
+
152
+ if (!hasEnhancedSnapshot) {
153
+ render(<EnhancedComponent />)
154
+ await page.toMatchImageSnapshot({ snapshotKey: 'enhanced' })
155
+ }
156
+
157
+ // Now we can safely compare both exist
158
+ expect(await page.hasImageSnapshot({ snapshotKey: 'base' })).toBe(true)
159
+ expect(await page.hasImageSnapshot({ snapshotKey: 'enhanced' })).toBe(true)
160
+ })
161
+ ```
162
+
163
+ ## Behavior and Limitations
164
+
165
+ ### Test Context Required
166
+
167
+ The method must be called within a test context:
168
+
169
+ ```ts
170
+ // ❌ This will throw an error
171
+ const hasSnapshot = await page.hasImageSnapshot()
172
+ // Error: `hasImageSnapshot()` must be called in a test.
173
+
174
+ // ✅ Call within a test
175
+ it('should work in test context', async () => {
176
+ const hasSnapshot = await page.hasImageSnapshot()
177
+ // This works correctly
178
+ })
179
+ ```
180
+
181
+ ### Concurrent Tests Not Supported
182
+
183
+ Cannot be used in concurrent tests because they share the same iframe environment:
184
+
185
+ ```ts
186
+ // ❌ This will throw an error
187
+ it.concurrent('should not work in concurrent test', async () => {
188
+ const hasSnapshot = await page.hasImageSnapshot()
189
+ // Error: `hasImageSnapshot()` cannot be called in a concurrent test because
190
+ // concurrent tests run at the same time in the same iframe and affect each other's environment.
191
+ })
192
+
193
+ // ✅ Use regular tests
194
+ it('should work in regular test', async () => {
195
+ const hasSnapshot = await page.hasImageSnapshot()
196
+ // This works correctly
197
+ })
198
+ ```
199
+
200
+ ### Async Operation
201
+
202
+ Always returns a Promise, so you must use `await` or `.then()`:
203
+
204
+ ```ts
205
+ // ✅ Using await
206
+ const hasSnapshot = await page.hasImageSnapshot()
207
+
208
+ // ✅ Using .then()
209
+ page.hasImageSnapshot().then(hasSnapshot => {
210
+ if (hasSnapshot) {
211
+ // Handle existing snapshot
212
+ }
213
+ })
214
+
215
+ // ❌ Don't forget await
216
+ const hasSnapshot = page.hasImageSnapshot() // This is a Promise, not a boolean!
217
+ ```
218
+
219
+ ## Error Handling
220
+
221
+ The method will throw errors in specific cases:
222
+
223
+ ```ts
224
+ // Test context errors
225
+ try {
226
+ await page.hasImageSnapshot()
227
+ } catch (error) {
228
+ // Error: `hasImageSnapshot()` must be called in a test.
229
+ }
230
+
231
+ // Concurrent test errors
232
+ it.concurrent('concurrent test', async () => {
233
+ try {
234
+ await page.hasImageSnapshot()
235
+ } catch (error) {
236
+ // Error: `hasImageSnapshot()` cannot be called in a concurrent test because
237
+ // concurrent tests run at the same time in the same iframe and affect each other's environment.
238
+ }
239
+ })
240
+
241
+ // Snapshot key validation errors
242
+ try {
243
+ await page.hasImageSnapshot({ snapshotKey: 'invalid-key' })
244
+ } catch (error) {
245
+ // Error: Snapshot key cannot contain dash
246
+ }
247
+ ```
248
+
249
+ ## Integration with Other Methods
250
+
251
+ `page.hasImageSnapshot()` works seamlessly with other visual testing methods:
252
+
253
+ ```ts
254
+ it('should integrate with other methods', async ({ expect }) => {
255
+ // Check if snapshot exists
256
+ const hasSnapshot = await page.hasImageSnapshot({ snapshotKey: 'integration' })
257
+
258
+ if (!hasSnapshot) {
259
+ // Create page snapshot
260
+ await page.toMatchImageSnapshot({ snapshotKey: 'integration' })
261
+ }
262
+
263
+ // Now test element snapshots
264
+ const element = page.getByTestId('component')
265
+ await expect(element).toMatchImageSnapshot({ snapshotKey: 'element' })
266
+
267
+ // Verify both snapshots exist
268
+ expect(await page.hasImageSnapshot({ snapshotKey: 'integration' })).toBe(true)
269
+ expect(await page.hasImageSnapshot({ snapshotKey: 'element' })).toBe(true)
270
+ })
271
+ ```
272
+
273
+ ## Best Practices
274
+
275
+ ### Use Descriptive Snapshot Keys
276
+
277
+ ```ts
278
+ // ✅ Good - descriptive and clear
279
+ await page.hasImageSnapshot({ snapshotKey: 'homepage_header' })
280
+ await page.hasImageSnapshot({ snapshotKey: 'modal_open_state' })
281
+ await page.hasImageSnapshot({ snapshotKey: 'dark_theme_sidebar' })
282
+
283
+ // ❌ Avoid - unclear purpose
284
+ await page.hasImageSnapshot({ snapshotKey: 'test1' })
285
+ await page.hasImageSnapshot({ snapshotKey: 'snapshot' })
286
+ ```
287
+
288
+ ### Handle Both Cases
289
+
290
+ Always handle both the existence and non-existence cases:
291
+
292
+ ```ts
293
+ it('should handle both snapshot states', async () => {
294
+ const hasSnapshot = await page.hasImageSnapshot({ snapshotKey: 'feature' })
295
+
296
+ if (hasSnapshot) {
297
+ // Test against existing snapshot
298
+ render(<FeatureComponent />)
299
+ await page.toMatchImageSnapshot({ snapshotKey: 'feature' })
300
+ } else {
301
+ // Create initial snapshot
302
+ render(<FeatureComponent />)
303
+ await page.toMatchImageSnapshot({ snapshotKey: 'feature' })
304
+ }
305
+ })
306
+ ```
307
+
308
+ ### Combine with Conditional Logic
309
+
310
+ Use with environment or configuration checks:
311
+
312
+ ```ts
313
+ it('should adapt to environment', async () => {
314
+ const isDarkMode = process.env.THEME === 'dark'
315
+ const snapshotKey = isDarkMode ? 'dark_mode' : 'light_mode'
316
+
317
+ const hasSnapshot = await page.hasImageSnapshot({ snapshotKey })
318
+
319
+ if (!hasSnapshot) {
320
+ // Setup appropriate theme
321
+ if (isDarkMode) {
322
+ document.body.classList.add('dark')
323
+ }
324
+ render(<App />)
325
+ await page.toMatchImageSnapshot({ snapshotKey })
326
+ }
327
+ })
328
+ ```