[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

docs(snapshot): gotchas for custom async inline snaphsot matcher (#10107)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

authored by

Hiroshi Ogawa
Claude Sonnet 4.6
and committed by
GitHub
(Apr 9, 2026, 8:53 AM +0200) 6d989d8a 39b3c851

+137 -49
+20
docs/guide/snapshot.md
··· 261 261 File snapshot matchers must be `async` — `toMatchFileSnapshot` returns a `Promise`. Remember to `await` the result in the matcher and in your test. 262 262 ::: 263 263 264 + ::: warning 265 + When custom inline snapshot matcher is aynchronous, Vitest cannot automatically infer the call location for inline snapshot rewriting. You must capture the call site by setting the `'error'` flag on the chai assertion object: 266 + 267 + ```ts 268 + import { expect, chai, Snapshots } from 'vitest' 269 + 270 + const { toMatchInlineSnapshot } = Snapshots 271 + 272 + expect.extend({ 273 + async toMatchTransformedInlineSnapshot(received: string, inlineSnapshot?: string) { 274 + // capture call site synchronously at the top of matcher implementation 275 + chai.util.flag(this.assertion, 'error', new Error()) 276 + const transformed = await transform(received) 277 + return toMatchInlineSnapshot.call(this, transformed, inlineSnapshot) 278 + }, 279 + }) 280 + ``` 281 + 282 + ::: 283 + 264 284 For TypeScript, extend the `Assertion` interface: 265 285 266 286 ```ts
+91 -48
test/snapshots/test/custom-matcher.test.ts
··· 57 57 "reversed": "eheheh", 58 58 } 59 59 \`) 60 + 61 + expect(\`huhuhu\`).toMatchCustomAsyncInlineSnapshot(\` 62 + Object { 63 + "length": 6, 64 + "reversed": "uhuhuh", 65 + } 66 + \`) 60 67 " 61 68 `) 62 69 expect(result.errorTree()).toMatchInlineSnapshot(` 63 70 Object { 64 71 "basic.test.ts": Object { 72 + "async inline": "passed", 65 73 "file": "passed", 66 74 "inline": "passed", 67 75 "properties 1": "passed", ··· 77 85 .replace('`popopo`', '`popopo-edit`') 78 86 .replace('`pepepe`', '`pepepe-edit`') 79 87 .replace('`hihihi`', '`hihihi-edit`') 88 + .replace('`huhuhu`', '`huhuhu-edit`') 80 89 .replace('`hehehe`', '`hehehe-edit`')) 81 90 82 91 result = await runVitest({ root, update: 'none' }) 83 92 expect(result.stderr).toMatchInlineSnapshot(` 84 93 " 85 - ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯ 94 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 6 ⎯⎯⎯⎯⎯⎯⎯ 86 95 87 96 FAIL basic.test.ts > file 88 97 Error: [custom error] Snapshot \`file 1\` mismatched ··· 97 106 + "reversed": "tide-ahahah", 98 107 } 99 108 100 - ❯ basic.test.ts:50:25 101 - 48| 102 - 49| test('file', () => { 103 - 50| expect(\`hahaha-edit\`).toMatchCustomSnapshot() 109 + ❯ basic.test.ts:66:25 110 + 64| 111 + 65| test('file', () => { 112 + 66| expect(\`hahaha-edit\`).toMatchCustomSnapshot() 104 113 | ^ 105 - 51| }) 106 - 52| 114 + 67| }) 115 + 68| 107 116 108 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/5]⎯ 117 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/6]⎯ 109 118 110 119 FAIL basic.test.ts > properties 1 111 120 Error: [custom error] Snapshot properties mismatched ··· 119 128 + "reversed": "tide-opopop", 120 129 } 121 130 122 - ❯ basic.test.ts:54:25 123 - 52| 124 - 53| test('properties 1', () => { 125 - 54| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 }) 131 + ❯ basic.test.ts:70:25 132 + 68| 133 + 69| test('properties 1', () => { 134 + 70| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 }) 126 135 | ^ 127 - 55| }) 128 - 56| 136 + 71| }) 137 + 72| 129 138 130 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/5]⎯ 139 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/6]⎯ 131 140 132 141 FAIL basic.test.ts > properties 2 133 142 Error: [custom error] Snapshot properties mismatched ··· 141 150 + "reversed": "tide-epepep", 142 151 } 143 152 144 - ❯ basic.test.ts:58:25 145 - 56| 146 - 57| test('properties 2', () => { 147 - 58| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis… 153 + ❯ basic.test.ts:74:25 154 + 72| 155 + 73| test('properties 2', () => { 156 + 74| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis… 148 157 | ^ 149 - 59| }) 150 - 60| 158 + 75| }) 159 + 76| 151 160 152 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/5]⎯ 161 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯ 153 162 154 163 FAIL basic.test.ts > raw 155 164 Error: [custom error] Snapshot \`raw 1\` mismatched ··· 164 173 + "reversed": "tide-ihihih", 165 174 } 166 175 167 - ❯ basic.test.ts:62:3 168 - 60| 169 - 61| test('raw', async () => { 170 - 62| await expect(\`hihihi-edit\`).toMatchCustomFileSnapshot('./__snapshots… 176 + ❯ basic.test.ts:78:3 177 + 76| 178 + 77| test('raw', async () => { 179 + 78| await expect(\`hihihi-edit\`).toMatchCustomFileSnapshot('./__snapshots… 171 180 | ^ 172 - 63| }) 173 - 64| 181 + 79| }) 182 + 80| 174 183 175 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/5]⎯ 184 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯ 176 185 177 186 FAIL basic.test.ts > inline 178 187 Error: [custom error] Snapshot \`inline 1\` mismatched ··· 187 196 + "reversed": "tide-eheheh", 188 197 } 189 198 190 - ❯ basic.test.ts:66:25 191 - 64| 192 - 65| test('inline', () => { 193 - 66| expect(\`hehehe-edit\`).toMatchCustomInlineSnapshot(\` 199 + ❯ basic.test.ts:82:25 200 + 80| 201 + 81| test('inline', () => { 202 + 82| expect(\`hehehe-edit\`).toMatchCustomInlineSnapshot(\` 194 203 | ^ 195 - 67| Object { 196 - 68| "length": 6, 204 + 83| Object { 205 + 84| "length": 6, 197 206 198 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/5]⎯ 207 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯ 208 + 209 + FAIL basic.test.ts > async inline 210 + Error: [custom error] Snapshot \`async inline 1\` mismatched 211 + 212 + - Expected 213 + + Received 214 + 215 + Object { 216 + - "length": 6, 217 + + "length": 11, 218 + - "reversed": "uhuhuh", 219 + + "reversed": "tide-uhuhuh", 220 + } 221 + 222 + ❯ basic.test.ts:91:3 223 + 89| 224 + 90| test('async inline', async () => { 225 + 91| await expect(\`huhuhu-edit\`).toMatchCustomAsyncInlineSnapshot(\` 226 + | ^ 227 + 92| Object { 228 + 93| "length": 6, 229 + 230 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯ 199 231 200 232 " 201 233 `) 202 234 expect(result.errorTree()).toMatchInlineSnapshot(` 203 235 Object { 204 236 "basic.test.ts": Object { 237 + "async inline": Array [ 238 + "[custom error] Snapshot \`async inline 1\` mismatched", 239 + ], 205 240 "file": Array [ 206 241 "[custom error] Snapshot \`file 1\` mismatched", 207 242 ], ··· 239 274 + "reversed": "tide-opopop", 240 275 } 241 276 242 - ❯ basic.test.ts:54:25 243 - 52| 244 - 53| test('properties 1', () => { 245 - 54| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 }) 277 + ❯ basic.test.ts:70:25 278 + 68| 279 + 69| test('properties 1', () => { 280 + 70| expect(\`popopo-edit\`).toMatchCustomSnapshot({ length: 6 }) 246 281 | ^ 247 - 55| }) 248 - 56| 282 + 71| }) 283 + 72| 249 284 250 285 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ 251 286 ··· 261 296 + "reversed": "tide-epepep", 262 297 } 263 298 264 - ❯ basic.test.ts:58:25 265 - 56| 266 - 57| test('properties 2', () => { 267 - 58| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis… 299 + ❯ basic.test.ts:74:25 300 + 72| 301 + 73| test('properties 2', () => { 302 + 74| expect(\`pepepe-edit\`).toMatchCustomSnapshot({ length: expect.toSatis… 268 303 | ^ 269 - 59| }) 270 - 60| 304 + 75| }) 305 + 76| 271 306 272 307 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ 273 308 ··· 312 347 "reversed": "tide-eheheh", 313 348 } 314 349 \`) 350 + 351 + expect(\`huhuhu-edit\`).toMatchCustomAsyncInlineSnapshot(\` 352 + Object { 353 + "length": 11, 354 + "reversed": "tide-uhuhuh", 355 + } 356 + \`) 315 357 " 316 358 `) 317 359 expect(result.errorTree()).toMatchInlineSnapshot(` 318 360 Object { 319 361 "basic.test.ts": Object { 362 + "async inline": "passed", 320 363 "file": "passed", 321 364 "inline": "passed", 322 365 "properties 1": Array [
+26 -1
test/snapshots/test/fixtures/custom-matcher/basic.test.ts
··· 1 - import { expect, test, Snapshots } from 'vitest' 1 + import { expect, test, Snapshots, chai } from 'vitest' 2 2 3 3 const { 4 4 toMatchFileSnapshot, ··· 11 11 toMatchCustomSnapshot: (properties?: object) => R 12 12 toMatchCustomInlineSnapshot: (snapshot?: string) => R 13 13 toMatchCustomFileSnapshot: (filepath: string) => Promise<R> 14 + toMatchCustomAsyncInlineSnapshot: (snapshot?: string) => Promise<R> 14 15 } 15 16 16 17 declare module 'vitest' { ··· 44 45 const result = await toMatchFileSnapshot.call(this, actualCustom, filepath) 45 46 return { ...result, message: () => `[custom error] ${result.message()}` } 46 47 }, 48 + async toMatchCustomAsyncInlineSnapshot( 49 + actual: string, 50 + inlineSnapshot?: string, 51 + ) { 52 + chai.util.flag(this.assertion, 'error', new Error()) 53 + await Promise.resolve() 54 + const inner = async () => { 55 + await Promise.resolve() 56 + const actualCustom = formatCustom(actual) 57 + const result = toMatchInlineSnapshot.call(this, actualCustom, inlineSnapshot) 58 + return { ...result, message: () => `[custom error] ${result.message()}` } 59 + } 60 + const result = await inner(); 61 + return result; 62 + } 47 63 }) 48 64 49 65 test('file', () => { ··· 67 83 Object { 68 84 "length": 6, 69 85 "reversed": "eheheh", 86 + } 87 + `) 88 + }) 89 + 90 + test('async inline', async () => { 91 + await expect(`huhuhu`).toMatchCustomAsyncInlineSnapshot(` 92 + Object { 93 + "length": 6, 94 + "reversed": "uhuhuh", 70 95 } 71 96 `) 72 97 })