Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9c24fa
feat: add assertion helper to hide internal stack traces
hi-ogawa Feb 5, 2026
66efef8
chore: lint
hi-ogawa Feb 5, 2026
8d15b04
feat: move to vi.helper
hi-ogawa Feb 5, 2026
78b2dda
docs: tweak
hi-ogawa Feb 5, 2026
0fad874
docs: add to vi.md
hi-ogawa Feb 5, 2026
2568053
test: browser mode
hi-ogawa Feb 5, 2026
623b509
fix: handle async* marker on firefox
hi-ogawa Feb 5, 2026
754b634
test: debug webkit
hi-ogawa Feb 5, 2026
f2b288c
debug
hi-ogawa Feb 5, 2026
38821f3
test: wip
hi-ogawa Feb 6, 2026
adae110
Merge branch 'main' into 02-05-feat_assertion_helper_to_filter_out_in…
hi-ogawa Feb 6, 2026
481e1d4
test: snapshot
hi-ogawa Feb 6, 2026
563ad03
test: tweak
hi-ogawa Feb 6, 2026
f695937
chore: todo
hi-ogawa Feb 6, 2026
67acd07
test: fix snapshot
hi-ogawa Feb 6, 2026
5855c1c
test: update
hi-ogawa Feb 6, 2026
2253631
test: tweak
hi-ogawa Feb 6, 2026
14dd475
debug ci
hi-ogawa Feb 6, 2026
6c82a95
test: add buildTestProjectTree
hi-ogawa Feb 6, 2026
c7bc604
test: sort before toEqual
hi-ogawa Feb 6, 2026
a1613ff
Revert "debug ci"
hi-ogawa Feb 6, 2026
37254d0
test: add more assertion helper edge cases
hi-ogawa Feb 6, 2026
31ef487
test: remove dev-note numbering from comments
hi-ogawa Feb 6, 2026
45cbe1c
chore: cleanup
hi-ogawa Feb 6, 2026
1a96a7e
test: snapshot
hi-ogawa Feb 6, 2026
e42788f
test: printConsoleTrace
hi-ogawa Feb 6, 2026
1fb35f8
test: update
hi-ogawa Feb 6, 2026
d27e2fe
chore: rename vi.helper to vi.defineHelper
hi-ogawa Feb 7, 2026
8483a9e
fix: rename function helper to defineHelper in docs
hi-ogawa Feb 7, 2026
d2a7d36
Merge branch 'main' into 02-05-feat_assertion_helper_to_filter_out_in…
hi-ogawa Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -1335,3 +1335,42 @@ function resetConfig(): void
```

If [`vi.setConfig`](#vi-setconfig) was called before, this will reset config to the original state.

### vi.defineHelper <Version>4.1.0</Version> {#vi-defineHelper}

```ts
function defineHelper<F extends (...args: any) => any>(fn: F): F
```

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.

Works with both synchronous and asynchronous functions, and supports `expect.soft()`.

```ts
import { expect, vi } from 'vitest'

const assertPair = vi.defineHelper((a, b) => {
expect(a).toEqual(b)
})

test('example', () => {
assertPair('left', 'right') // Error points to this line
})
```

Example output:

<!-- eslint-skip -->
```js
FAIL example.test.ts > example
AssertionError: expected 'left' to deeply equal 'right'

Expected: "right"
Received: "left"

❯ example.test.ts:8:3
7| test('example', () => {
8| assertPair('left', 'right')
| ^
9| })
```
8 changes: 7 additions & 1 deletion packages/utils/src/source-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,16 @@ export function parseStacktrace(
options: StackTraceParserOptions = {},
): ParsedStack[] {
const { ignoreStackEntries = stackIgnorePatterns } = options
const stacks = !CHROME_IE_STACK_REGEXP.test(stack)
let stacks = !CHROME_IE_STACK_REGEXP.test(stack)
? parseFFOrSafariStackTrace(stack)
: parseV8Stacktrace(stack)

// remove assertion helper's internal stacks
const helperIndex = stacks.findLastIndex(s => s.method === '__VITEST_HELPER__' || s.method === 'async*__VITEST_HELPER__')
if (helperIndex >= 0) {
stacks = stacks.slice(helperIndex + 1)
}

return stacks.map((stack) => {
if (options.getUrlId) {
stack.file = options.getUrlId(stack.file)
Expand Down
45 changes: 45 additions & 0 deletions packages/vitest/src/integrations/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,40 @@ export interface VitestUtils {
*/
waitFor: typeof waitFor

/**
* 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.
* Works with both synchronous and asynchronous functions, and supports `expect.soft()`.
*
* @example
* ```ts
* const myEqual = vi.defineHelper((x, y) => {
* expect(x).toEqual(y)
* })
*
* test('example', () => {
* myEqual('left', 'right') // Error points to this line
* })
* ```
* Example output:
* ```
* FAIL example.test.ts > example
* AssertionError: expected 'left' to deeply equal 'right'
*
* Expected: "right"
* Received: "left"
*
* ❯ example.test.ts:6:3
* 4| test('example', () => {
* 5| myEqual('left', 'right')
* | ^
* 6| })
* ```
* @param fn The assertion function to wrap
* @returns A wrapped function with the same signature
*/
defineHelper: <F extends (...args: any) => any>(fn: F) => F

/**
* 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.
*
Expand Down Expand Up @@ -576,6 +610,17 @@ function createVitest(): VitestUtils {
fn,
waitFor,
waitUntil,
defineHelper: (fn) => {
return function __VITEST_HELPER__(...args: any[]): any {
const result = fn(...args)
if (result && typeof result === 'object' && typeof result.then === 'function') {
return (async function __VITEST_HELPER__() {
return await result
})()
}
return result
} as any
},
hoisted<T>(factory: () => T): T {
assertTypes(factory, '"vi.hoisted" factory', ['function'])
return factory()
Expand Down
35 changes: 35 additions & 0 deletions test/browser/fixtures/assertion-helper/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test, vi } from 'vitest'

const myEqual = vi.defineHelper((a: any, b: any) => {
expect(a).toEqual(b)
})

const myEqualAsync = vi.defineHelper(async (a: any, b: any) => {
await new Promise(r => setTimeout(r, 1))
expect(a).toEqual(b)
})

const myEqualSoft = vi.defineHelper((a: any, b: any) => {
expect.soft(a).toEqual(b)
})

const myEqualSoftAsync = vi.defineHelper(async (a: any, b: any) => {
await new Promise(r => setTimeout(r, 1))
expect.soft(a).toEqual(b)
})

test('sync', () => {
myEqual('sync', 'x')
})

test('async', async () => {
await myEqualAsync('async', 'x')
})

test('soft', () => {
myEqualSoft('soft', 'x')
})

test('soft async', async () => {
await myEqualSoftAsync('soft async', 'x')
})
14 changes: 14 additions & 0 deletions test/browser/fixtures/assertion-helper/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { instances, provider } from '../../settings'

export default defineConfig({
cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)),
test: {
browser: {
enabled: true,
provider,
instances,
},
},
})
208 changes: 208 additions & 0 deletions test/browser/specs/assertion-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import path from 'node:path'
import { expect, test } from 'vitest'
import { buildTestProjectTree } from '../../test-utils'
import { instances, runBrowserTests } from './utils'

test('vi.defineHelper hides internal stack traces', async () => {
const { results, ctx } = await runBrowserTests({
root: './fixtures/assertion-helper',
})

const projectTree = buildTestProjectTree(results, (testCase) => {
const result = testCase.result()
return result.errors.map((e) => {
const stacks = e.stacks.map(s => ({
...s,
file: path.relative(ctx.config.root, s.file),
}))
return ({ message: e.message, stacks })
})
})
expect(Object.keys(projectTree).sort()).toEqual(instances.map(i => i.browser).sort())

for (const [name, tree] of Object.entries(projectTree)) {
if (name === 'firefox') {
expect.soft(tree).toMatchInlineSnapshot(`
{
"basic.test.ts": {
"async": [
{
"message": "expected 'async' to deeply equal 'x'",
"stacks": [
{
"column": 8,
"file": "basic.test.ts",
"line": 26,
"method": "",
},
],
},
],
"soft": [
{
"message": "expected 'soft' to deeply equal 'x'",
"stacks": [
{
"column": 14,
"file": "basic.test.ts",
"line": 30,
"method": "",
},
],
},
],
"soft async": [
{
"message": "expected 'soft async' to deeply equal 'x'",
"stacks": [
{
"column": 8,
"file": "basic.test.ts",
"line": 34,
"method": "",
},
],
},
],
"sync": [
{
"message": "expected 'sync' to deeply equal 'x'",
"stacks": [
{
"column": 10,
"file": "basic.test.ts",
"line": 22,
"method": "",
},
],
},
],
},
}
`)
}
else if (name === 'webkit') {
// async stack trace is incomplete on webkit
// waiting for https://github.com/WebKit/WebKit/pull/57832 to land on playwright
// bun has already landed https://github.com/oven-sh/bun/pull/22517
expect.soft(tree).toMatchInlineSnapshot(`
{
"basic.test.ts": {
"async": [
{
"message": "expected 'async' to deeply equal 'x'",
"stacks": [
{
"column": 20,
"file": "basic.test.ts",
"line": 9,
"method": "",
},
],
},
],
"soft": [
{
"message": "expected 'soft' to deeply equal 'x'",
"stacks": [
{
"column": 14,
"file": "basic.test.ts",
"line": 30,
"method": "",
},
],
},
],
"soft async": [
{
"message": "expected 'soft async' to deeply equal 'x'",
"stacks": [
{
"column": 25,
"file": "basic.test.ts",
"line": 18,
"method": "",
},
],
},
],
"sync": [
{
"message": "expected 'sync' to deeply equal 'x'",
"stacks": [
{
"column": 10,
"file": "basic.test.ts",
"line": 22,
"method": "",
},
],
},
],
},
}
`)
}
else {
expect.soft(tree).toMatchInlineSnapshot(`
{
"basic.test.ts": {
"async": [
{
"message": "expected 'async' to deeply equal 'x'",
"stacks": [
{
"column": 2,
"file": "basic.test.ts",
"line": 26,
"method": "",
},
],
},
],
"soft": [
{
"message": "expected 'soft' to deeply equal 'x'",
"stacks": [
{
"column": 2,
"file": "basic.test.ts",
"line": 30,
"method": "",
},
],
},
],
"soft async": [
{
"message": "expected 'soft async' to deeply equal 'x'",
"stacks": [
{
"column": 2,
"file": "basic.test.ts",
"line": 34,
"method": "",
},
],
},
],
"sync": [
{
"message": "expected 'sync' to deeply equal 'x'",
"stacks": [
{
"column": 2,
"file": "basic.test.ts",
"line": 22,
"method": "",
},
],
},
],
},
}
`)
}
}
})
Loading