[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.

feat: add assertion helper to hide internal stack traces (#9594)

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

authored by

Hiroshi Ogawa
Claude Opus 4.6
and committed by
GitHub
(Feb 7, 2026, 12:44 PM +0100) eeb0ae2f 89cbdaea

+665 -1
+39
docs/api/vi.md
··· 1335 1335 ``` 1336 1336 1337 1337 If [`vi.setConfig`](#vi-setconfig) was called before, this will reset config to the original state. 1338 + 1339 + ### vi.defineHelper <Version>4.1.0</Version> {#vi-defineHelper} 1340 + 1341 + ```ts 1342 + function defineHelper<F extends (...args: any) => any>(fn: F): F 1343 + ``` 1344 + 1345 + Wraps a function to create an assertion helper. When an assertion fails inside the helper, the error stack trace will point to where the helper was called, not inside the helper itself. This makes it easier to identify the source of test failures when using custom assertion functions. 1346 + 1347 + Works with both synchronous and asynchronous functions, and supports `expect.soft()`. 1348 + 1349 + ```ts 1350 + import { expect, vi } from 'vitest' 1351 + 1352 + const assertPair = vi.defineHelper((a, b) => { 1353 + expect(a).toEqual(b) 1354 + }) 1355 + 1356 + test('example', () => { 1357 + assertPair('left', 'right') // Error points to this line 1358 + }) 1359 + ``` 1360 + 1361 + Example output: 1362 + 1363 + <!-- eslint-skip --> 1364 + ```js 1365 + FAIL example.test.ts > example 1366 + AssertionError: expected 'left' to deeply equal 'right' 1367 + 1368 + Expected: "right" 1369 + Received: "left" 1370 + 1371 + ❯ example.test.ts:8:3 1372 + 7| test('example', () => { 1373 + 8| assertPair('left', 'right') 1374 + | ^ 1375 + 9| }) 1376 + ```
+14
test/test-utils/index.ts
··· 599 599 600 600 return tree 601 601 } 602 + 603 + export function buildTestProjectTree(testModules: TestModule[], onTestCase?: (result: TestCase) => unknown) { 604 + const projectTree: Record<string, Record<string, any>> = {} 605 + 606 + for (const testModule of testModules) { 607 + const projectName = testModule.project.name 608 + projectTree[projectName] = { 609 + ...projectTree[projectName], 610 + ...buildTestTree([testModule], onTestCase), 611 + } 612 + } 613 + 614 + return projectTree 615 + }
+7 -1
packages/utils/src/source-map.ts
··· 228 228 options: StackTraceParserOptions = {}, 229 229 ): ParsedStack[] { 230 230 const { ignoreStackEntries = stackIgnorePatterns } = options 231 - const stacks = !CHROME_IE_STACK_REGEXP.test(stack) 231 + let stacks = !CHROME_IE_STACK_REGEXP.test(stack) 232 232 ? parseFFOrSafariStackTrace(stack) 233 233 : parseV8Stacktrace(stack) 234 + 235 + // remove assertion helper's internal stacks 236 + const helperIndex = stacks.findLastIndex(s => s.method === '__VITEST_HELPER__' || s.method === 'async*__VITEST_HELPER__') 237 + if (helperIndex >= 0) { 238 + stacks = stacks.slice(helperIndex + 1) 239 + } 234 240 235 241 return stacks.map((stack) => { 236 242 if (options.getUrlId) {
+208
test/browser/specs/assertion-helper.test.ts
··· 1 + import path from 'node:path' 2 + import { expect, test } from 'vitest' 3 + import { buildTestProjectTree } from '../../test-utils' 4 + import { instances, runBrowserTests } from './utils' 5 + 6 + test('vi.defineHelper hides internal stack traces', async () => { 7 + const { results, ctx } = await runBrowserTests({ 8 + root: './fixtures/assertion-helper', 9 + }) 10 + 11 + const projectTree = buildTestProjectTree(results, (testCase) => { 12 + const result = testCase.result() 13 + return result.errors.map((e) => { 14 + const stacks = e.stacks.map(s => ({ 15 + ...s, 16 + file: path.relative(ctx.config.root, s.file), 17 + })) 18 + return ({ message: e.message, stacks }) 19 + }) 20 + }) 21 + expect(Object.keys(projectTree).sort()).toEqual(instances.map(i => i.browser).sort()) 22 + 23 + for (const [name, tree] of Object.entries(projectTree)) { 24 + if (name === 'firefox') { 25 + expect.soft(tree).toMatchInlineSnapshot(` 26 + { 27 + "basic.test.ts": { 28 + "async": [ 29 + { 30 + "message": "expected 'async' to deeply equal 'x'", 31 + "stacks": [ 32 + { 33 + "column": 8, 34 + "file": "basic.test.ts", 35 + "line": 26, 36 + "method": "", 37 + }, 38 + ], 39 + }, 40 + ], 41 + "soft": [ 42 + { 43 + "message": "expected 'soft' to deeply equal 'x'", 44 + "stacks": [ 45 + { 46 + "column": 14, 47 + "file": "basic.test.ts", 48 + "line": 30, 49 + "method": "", 50 + }, 51 + ], 52 + }, 53 + ], 54 + "soft async": [ 55 + { 56 + "message": "expected 'soft async' to deeply equal 'x'", 57 + "stacks": [ 58 + { 59 + "column": 8, 60 + "file": "basic.test.ts", 61 + "line": 34, 62 + "method": "", 63 + }, 64 + ], 65 + }, 66 + ], 67 + "sync": [ 68 + { 69 + "message": "expected 'sync' to deeply equal 'x'", 70 + "stacks": [ 71 + { 72 + "column": 10, 73 + "file": "basic.test.ts", 74 + "line": 22, 75 + "method": "", 76 + }, 77 + ], 78 + }, 79 + ], 80 + }, 81 + } 82 + `) 83 + } 84 + else if (name === 'webkit') { 85 + // async stack trace is incomplete on webkit 86 + // waiting for https://github.com/WebKit/WebKit/pull/57832 to land on playwright 87 + // bun has already landed https://github.com/oven-sh/bun/pull/22517 88 + expect.soft(tree).toMatchInlineSnapshot(` 89 + { 90 + "basic.test.ts": { 91 + "async": [ 92 + { 93 + "message": "expected 'async' to deeply equal 'x'", 94 + "stacks": [ 95 + { 96 + "column": 20, 97 + "file": "basic.test.ts", 98 + "line": 9, 99 + "method": "", 100 + }, 101 + ], 102 + }, 103 + ], 104 + "soft": [ 105 + { 106 + "message": "expected 'soft' to deeply equal 'x'", 107 + "stacks": [ 108 + { 109 + "column": 14, 110 + "file": "basic.test.ts", 111 + "line": 30, 112 + "method": "", 113 + }, 114 + ], 115 + }, 116 + ], 117 + "soft async": [ 118 + { 119 + "message": "expected 'soft async' to deeply equal 'x'", 120 + "stacks": [ 121 + { 122 + "column": 25, 123 + "file": "basic.test.ts", 124 + "line": 18, 125 + "method": "", 126 + }, 127 + ], 128 + }, 129 + ], 130 + "sync": [ 131 + { 132 + "message": "expected 'sync' to deeply equal 'x'", 133 + "stacks": [ 134 + { 135 + "column": 10, 136 + "file": "basic.test.ts", 137 + "line": 22, 138 + "method": "", 139 + }, 140 + ], 141 + }, 142 + ], 143 + }, 144 + } 145 + `) 146 + } 147 + else { 148 + expect.soft(tree).toMatchInlineSnapshot(` 149 + { 150 + "basic.test.ts": { 151 + "async": [ 152 + { 153 + "message": "expected 'async' to deeply equal 'x'", 154 + "stacks": [ 155 + { 156 + "column": 2, 157 + "file": "basic.test.ts", 158 + "line": 26, 159 + "method": "", 160 + }, 161 + ], 162 + }, 163 + ], 164 + "soft": [ 165 + { 166 + "message": "expected 'soft' to deeply equal 'x'", 167 + "stacks": [ 168 + { 169 + "column": 2, 170 + "file": "basic.test.ts", 171 + "line": 30, 172 + "method": "", 173 + }, 174 + ], 175 + }, 176 + ], 177 + "soft async": [ 178 + { 179 + "message": "expected 'soft async' to deeply equal 'x'", 180 + "stacks": [ 181 + { 182 + "column": 2, 183 + "file": "basic.test.ts", 184 + "line": 34, 185 + "method": "", 186 + }, 187 + ], 188 + }, 189 + ], 190 + "sync": [ 191 + { 192 + "message": "expected 'sync' to deeply equal 'x'", 193 + "stacks": [ 194 + { 195 + "column": 2, 196 + "file": "basic.test.ts", 197 + "line": 22, 198 + "method": "", 199 + }, 200 + ], 201 + }, 202 + ], 203 + }, 204 + } 205 + `) 206 + } 207 + } 208 + })
+197
test/cli/test/assertion-helper.test.ts
··· 1 + import { resolve } from 'pathe' 2 + import { expect, it } from 'vitest' 3 + import { runVitest } from '../../test-utils' 4 + 5 + it('assertion helper', async () => { 6 + const { stderr, errorTree } = await runVitest({ 7 + root: resolve(import.meta.dirname, '../fixtures/assertion-helper'), 8 + printConsoleTrace: true, 9 + }) 10 + expect(stderr).toMatchInlineSnapshot(` 11 + "stderr | basic.test.ts > helper with logs 12 + [test-myHelperWithLogs] 13 + ❯ basic.test.ts:105:3 14 + 15 + 16 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 8 ⎯⎯⎯⎯⎯⎯⎯ 17 + 18 + FAIL basic.test.ts > sync 19 + AssertionError: expected 'sync' to deeply equal 'x' 20 + 21 + Expected: "x" 22 + Received: "sync" 23 + 24 + ❯ basic.test.ts:22:3 25 + 20| 26 + 21| test("sync", () => { 27 + 22| myEqual("sync", "x"); 28 + | ^ 29 + 23| }); 30 + 24| 31 + 32 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/9]⎯ 33 + 34 + FAIL basic.test.ts > async 35 + AssertionError: expected 'async' to deeply equal 'x' 36 + 37 + Expected: "x" 38 + Received: "async" 39 + 40 + ❯ basic.test.ts:26:3 41 + 24| 42 + 25| test("async", async () => { 43 + 26| await myEqualAsync("async", "x"); 44 + | ^ 45 + 27| }); 46 + 28| 47 + 48 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/9]⎯ 49 + 50 + FAIL basic.test.ts > soft 51 + AssertionError: expected 'soft' to deeply equal 'x' 52 + 53 + Expected: "x" 54 + Received: "soft" 55 + 56 + ❯ basic.test.ts:30:3 57 + 28| 58 + 29| test("soft", () => { 59 + 30| myEqualSoft("soft", "x"); 60 + | ^ 61 + 31| }); 62 + 32| 63 + 64 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/9]⎯ 65 + 66 + FAIL basic.test.ts > soft async 67 + AssertionError: expected 'soft async' to deeply equal 'x' 68 + 69 + Expected: "x" 70 + Received: "soft async" 71 + 72 + ❯ basic.test.ts:34:3 73 + 32| 74 + 33| test("soft async", async () => { 75 + 34| await myEqualSoftAsync("soft async", "x"); 76 + | ^ 77 + 35| }); 78 + 36| 79 + 80 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/9]⎯ 81 + 82 + FAIL basic.test.ts > nested 83 + AssertionError: expected 'nested' to deeply equal 'x' 84 + 85 + Expected: "x" 86 + Received: "nested" 87 + 88 + ❯ basic.test.ts:46:3 89 + 44| 90 + 45| test("nested", () => { 91 + 46| outerHelper("nested", "x"); 92 + | ^ 93 + 47| }); 94 + 48| 95 + 96 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/9]⎯ 97 + 98 + FAIL basic.test.ts > multiple soft 99 + AssertionError: expected 'first' to deeply equal 'x' 100 + 101 + Expected: "x" 102 + Received: "first" 103 + 104 + ❯ basic.test.ts:77:3 105 + 75| // Multiple soft errors in one test 106 + 76| test("multiple soft", () => { 107 + 77| myEqualSoft("first", "x"); 108 + | ^ 109 + 78| myEqualSoft("second", "y"); 110 + 79| }); 111 + 112 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/9]⎯ 113 + 114 + FAIL basic.test.ts > multiple soft 115 + AssertionError: expected 'second' to deeply equal 'y' 116 + 117 + Expected: "y" 118 + Received: "second" 119 + 120 + ❯ basic.test.ts:78:3 121 + 76| test("multiple soft", () => { 122 + 77| myEqualSoft("first", "x"); 123 + 78| myEqualSoft("second", "y"); 124 + | ^ 125 + 79| }); 126 + 80| 127 + 128 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/9]⎯ 129 + 130 + FAIL basic.test.ts > custom error 131 + Error: custom error from helper 132 + ❯ basic.test.ts:87:3 133 + 85| 134 + 86| test("custom error", () => { 135 + 87| throwCustom(); 136 + | ^ 137 + 88| }); 138 + 89| 139 + 140 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/9]⎯ 141 + 142 + FAIL basic.test.ts > non-helper wrapper 143 + AssertionError: expected 'wrapper' to deeply equal 'x' 144 + 145 + Expected: "x" 146 + Received: "wrapper" 147 + 148 + ❯ assertEqualValues basic.test.ts:92:3 149 + 90| // non-helper wrapper calling a helper: stack should include the wrapp… 150 + 91| function assertEqualValues(a: any, b: any) { 151 + 92| myEqual(a, b); 152 + | ^ 153 + 93| } 154 + 94| 155 + ❯ basic.test.ts:96:3 156 + 157 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/9]⎯ 158 + 159 + " 160 + `) 161 + expect(errorTree()).toMatchInlineSnapshot(` 162 + { 163 + "basic.test.ts": { 164 + "async": [ 165 + "expected 'async' to deeply equal 'x'", 166 + ], 167 + "custom error": [ 168 + "custom error from helper", 169 + ], 170 + "helper with logs": "passed", 171 + "multiple soft": [ 172 + "expected 'first' to deeply equal 'x'", 173 + "expected 'second' to deeply equal 'y'", 174 + ], 175 + "nested": [ 176 + "expected 'nested' to deeply equal 'x'", 177 + ], 178 + "non-helper wrapper": [ 179 + "expected 'wrapper' to deeply equal 'x'", 180 + ], 181 + "pass async": "passed", 182 + "pass sync": "passed", 183 + "return async": "passed", 184 + "return sync": "passed", 185 + "soft": [ 186 + "expected 'soft' to deeply equal 'x'", 187 + ], 188 + "soft async": [ 189 + "expected 'soft async' to deeply equal 'x'", 190 + ], 191 + "sync": [ 192 + "expected 'sync' to deeply equal 'x'", 193 + ], 194 + }, 195 + } 196 + `) 197 + })
+45
packages/vitest/src/integrations/vi.ts
··· 169 169 waitFor: typeof waitFor 170 170 171 171 /** 172 + * Wraps a function to create an assertion helper. When an assertion fails inside the helper, 173 + * the error stack trace will point to where the helper was called, not inside the helper itself. 174 + * Works with both synchronous and asynchronous functions, and supports `expect.soft()`. 175 + * 176 + * @example 177 + * ```ts 178 + * const myEqual = vi.defineHelper((x, y) => { 179 + * expect(x).toEqual(y) 180 + * }) 181 + * 182 + * test('example', () => { 183 + * myEqual('left', 'right') // Error points to this line 184 + * }) 185 + * ``` 186 + * Example output: 187 + * ``` 188 + * FAIL example.test.ts > example 189 + * AssertionError: expected 'left' to deeply equal 'right' 190 + * 191 + * Expected: "right" 192 + * Received: "left" 193 + * 194 + * ❯ example.test.ts:6:3 195 + * 4| test('example', () => { 196 + * 5| myEqual('left', 'right') 197 + * | ^ 198 + * 6| }) 199 + * ``` 200 + * @param fn The assertion function to wrap 201 + * @returns A wrapped function with the same signature 202 + */ 203 + defineHelper: <F extends (...args: any) => any>(fn: F) => F 204 + 205 + /** 172 206 * This is similar to [`vi.waitFor`](https://vitest.dev/api/vi#vi-waitfor), but if the callback throws any errors, execution is immediately interrupted and an error message is received. 173 207 * 174 208 * If the callback returns a falsy value, the next check will continue until a truthy value is returned. This is useful when you need to wait for something to exist before taking the next step. ··· 576 610 fn, 577 611 waitFor, 578 612 waitUntil, 613 + defineHelper: (fn) => { 614 + return function __VITEST_HELPER__(...args: any[]): any { 615 + const result = fn(...args) 616 + if (result && typeof result === 'object' && typeof result.then === 'function') { 617 + return (async function __VITEST_HELPER__() { 618 + return await result 619 + })() 620 + } 621 + return result 622 + } as any 623 + }, 579 624 hoisted<T>(factory: () => T): T { 580 625 assertTypes(factory, '"vi.hoisted" factory', ['function']) 581 626 return factory()
+35
test/browser/fixtures/assertion-helper/basic.test.ts
··· 1 + import { expect, test, vi } from 'vitest' 2 + 3 + const myEqual = vi.defineHelper((a: any, b: any) => { 4 + expect(a).toEqual(b) 5 + }) 6 + 7 + const myEqualAsync = vi.defineHelper(async (a: any, b: any) => { 8 + await new Promise(r => setTimeout(r, 1)) 9 + expect(a).toEqual(b) 10 + }) 11 + 12 + const myEqualSoft = vi.defineHelper((a: any, b: any) => { 13 + expect.soft(a).toEqual(b) 14 + }) 15 + 16 + const myEqualSoftAsync = vi.defineHelper(async (a: any, b: any) => { 17 + await new Promise(r => setTimeout(r, 1)) 18 + expect.soft(a).toEqual(b) 19 + }) 20 + 21 + test('sync', () => { 22 + myEqual('sync', 'x') 23 + }) 24 + 25 + test('async', async () => { 26 + await myEqualAsync('async', 'x') 27 + }) 28 + 29 + test('soft', () => { 30 + myEqualSoft('soft', 'x') 31 + }) 32 + 33 + test('soft async', async () => { 34 + await myEqualSoftAsync('soft async', 'x') 35 + })
+14
test/browser/fixtures/assertion-helper/vitest.config.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { defineConfig } from 'vitest/config' 3 + import { instances, provider } from '../../settings' 4 + 5 + export default defineConfig({ 6 + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), 7 + test: { 8 + browser: { 9 + enabled: true, 10 + provider, 11 + instances, 12 + }, 13 + }, 14 + })
+106
test/cli/fixtures/assertion-helper/basic.test.ts
··· 1 + import { expect, test, vi } from "vitest"; 2 + 3 + const myEqual = vi.defineHelper((a: any, b: any) => { 4 + expect(a).toEqual(b); 5 + }); 6 + 7 + const myEqualAsync = vi.defineHelper(async (a: any, b: any) => { 8 + await new Promise((r) => setTimeout(r, 1)); 9 + expect(a).toEqual(b); 10 + }); 11 + 12 + const myEqualSoft = vi.defineHelper((a: any, b: any) => { 13 + expect.soft(a).toEqual(b); 14 + }); 15 + 16 + const myEqualSoftAsync = vi.defineHelper(async (a: any, b: any) => { 17 + await new Promise((r) => setTimeout(r, 1)); 18 + expect.soft(a).toEqual(b); 19 + }); 20 + 21 + test("sync", () => { 22 + myEqual("sync", "x"); 23 + }); 24 + 25 + test("async", async () => { 26 + await myEqualAsync("async", "x"); 27 + }); 28 + 29 + test("soft", () => { 30 + myEqualSoft("soft", "x"); 31 + }); 32 + 33 + test("soft async", async () => { 34 + await myEqualSoftAsync("soft async", "x"); 35 + }); 36 + 37 + // Nested helpers: outermost marker wins 38 + const innerHelper = vi.defineHelper((a: any, b: any) => { 39 + expect(a).toEqual(b); 40 + }); 41 + const outerHelper = vi.defineHelper((a: any, b: any) => { 42 + innerHelper(a, b); 43 + }); 44 + 45 + test("nested", () => { 46 + outerHelper("nested", "x"); 47 + }); 48 + 49 + // Helper that passes 50 + test("pass sync", () => { 51 + myEqual(1, 1); 52 + }); 53 + 54 + test("pass async", async () => { 55 + await myEqualAsync(1, 1); 56 + }); 57 + 58 + // Helper returning value 59 + const myAdd = vi.defineHelper((a: number, b: number) => { 60 + return a + b; 61 + }); 62 + const myAddAsync = vi.defineHelper(async (a: number, b: number) => { 63 + await new Promise((r) => setTimeout(r, 1)); 64 + return a + b; 65 + }); 66 + 67 + test("return sync", () => { 68 + expect(myAdd(1, 2)).toBe(3); 69 + }); 70 + 71 + test("return async", async () => { 72 + expect(await myAddAsync(1, 2)).toBe(3); 73 + }); 74 + 75 + // Multiple soft errors in one test 76 + test("multiple soft", () => { 77 + myEqualSoft("first", "x"); 78 + myEqualSoft("second", "y"); 79 + }); 80 + 81 + // Custom error in helper 82 + const throwCustom = vi.defineHelper(() => { 83 + throw new Error("custom error from helper"); 84 + }); 85 + 86 + test("custom error", () => { 87 + throwCustom(); 88 + }); 89 + 90 + // non-helper wrapper calling a helper: stack should include the wrapper 91 + function assertEqualValues(a: any, b: any) { 92 + myEqual(a, b); 93 + } 94 + 95 + test("non-helper wrapper", () => { 96 + assertEqualValues("wrapper", "x"); 97 + }); 98 + 99 + // printConsoleTrace also hides internal stacks 100 + const myHelperWithLogs = vi.defineHelper(() => { 101 + console.error("[test-myHelperWithLogs]"); 102 + }); 103 + 104 + test("helper with logs", () => { 105 + myHelperWithLogs(); 106 + });