Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 20 additions & 12 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,7 +1098,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {

return (...args: any[]) => {
utils.flag(this, '_name', key)
const promise = obj.then(
const promise = Promise.resolve(obj).then(
(value: any) => {
utils.flag(this, 'object', value)
return result.call(this, ...args)
Expand All @@ -1111,13 +1111,17 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
{ showDiff: false },
) as Error
_error.cause = err
_error.stack = (error.stack as string).replace(
error.message,
_error.message,
)
throw _error
},
)
).catch((err: any) => {
if (isError(err) && error.stack) {
err.stack = error.stack.replace(
error.message,
err.message,
)
}
throw err
})

return recordAsyncExpect(
test,
Expand Down Expand Up @@ -1166,7 +1170,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {

return (...args: any[]) => {
utils.flag(this, '_name', key)
const promise = wrapper.then(
const promise = Promise.resolve(wrapper).then(
(value: any) => {
const _error = new AssertionError(
`promise resolved "${utils.inspect(
Expand All @@ -1178,17 +1182,21 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
actual: value,
},
) as any
_error.stack = (error.stack as string).replace(
error.message,
_error.message,
)
throw _error
},
(err: any) => {
utils.flag(this, 'object', err)
return result.call(this, ...args)
},
)
).catch((err: any) => {
if (isError(err) && error.stack) {
err.stack = error.stack.replace(
error.message,
err.message,
)
}
throw err
})

return recordAsyncExpect(
test,
Expand Down
52 changes: 50 additions & 2 deletions packages/vitest/src/runtime/vm/commonjs-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,18 +200,38 @@ export class CommonjsExecutor {
m.exports = JSON.parse(code)
}

private static cjsConditions: Set<string> | undefined
private static getCjsConditions(): Set<string> {
if (!CommonjsExecutor.cjsConditions) {
CommonjsExecutor.cjsConditions = parseCjsConditions(
process.execArgv,
process.env.NODE_OPTIONS,
)
}
return CommonjsExecutor.cjsConditions
}

public createRequire = (filename: string | URL): NodeJS.Require => {
const _require = createRequire(filename)
const resolve = (id: string, options?: { paths?: string[] }) => {
return _require.resolve(id, {
...options,
// Works on Node 22.12+ where _resolveFilename supports conditions.
// Silently ignored on older Node versions.
conditions: CommonjsExecutor.getCjsConditions(),
} as any)
}
const require = ((id: string) => {
const resolved = _require.resolve(id)
const resolved = resolve(id)
const ext = extname(resolved)
if (ext === '.node' || isBuiltin(resolved)) {
return this.requireCoreModule(resolved)
}
const module = new this.Module(resolved)
return this.loadCommonJSModule(module, resolved)
}) as NodeJS.Require
require.resolve = _require.resolve
require.resolve = resolve as NodeJS.RequireResolve
require.resolve.paths = _require.resolve.paths
Object.defineProperty(require, 'extensions', {
get: () => this.extensions,
set: () => {},
Expand Down Expand Up @@ -381,3 +401,31 @@ export class CommonjsExecutor {
return moduleExports
}
}

// The "module-sync" exports condition (added in Node 22.12/20.19 when
// require(esm) was unflagged) can resolve to ESM files that our CJS
// vm.Script executor cannot handle. We exclude it by passing explicit
// CJS conditions to require.resolve (Node 22.12+).
// Must be a Set because Node's internal resolver calls conditions.has().
// User-specified --conditions/-C flags are respected, except module-sync.
export function parseCjsConditions(
execArgv: string[],
nodeOptions?: string,
): Set<string> {
const conditions = ['node', 'require', 'node-addons']
const args = [
...execArgv,
...(nodeOptions?.split(/\s+/) ?? []),
]
for (let i = 0; i < args.length; i++) {
const arg = args[i]
const eqMatch = arg.match(/^(?:--conditions|-C)=(.+)$/)
if (eqMatch) {
conditions.push(eqMatch[1])
}
else if ((arg === '--conditions' || arg === '-C') && i + 1 < args.length) {
conditions.push(args[++i])
}
}
return new Set(conditions.filter(c => c !== 'module-sync'))
}
122 changes: 121 additions & 1 deletion test/cli/test/stacktraces.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve } from 'pathe'
import { glob } from 'tinyglobby'
import { describe, expect, it } from 'vitest'
import { runVitest } from '../../test-utils'
import { runInlineTests, runVitest } from '../../test-utils'

// To prevent the warning coming up in snapshots
process.setMaxListeners(20)
Expand Down Expand Up @@ -193,3 +193,123 @@ it('custom helper with captureStackTrace', async () => {
}
`)
})

it('resolves/rejects', async () => {
const { stderr, errorTree } = await runInlineTests({
'repro.test.ts': `
import { test, expect } from 'vitest'

test('resolves: resolved promise with mismatched value', async () => {
await expect(Promise.resolve(3)).resolves.toBe(4)
})

test('rejects: rejected promise with mismatched value', async () => {
await expect(Promise.reject(3)).rejects.toBe(4)
})

test('rejects: resolves when rejection expected', async () => {
await expect(Promise.resolve(3)).rejects.toBe(4)
})

test('resolves: rejects when resolve expected', async () => {
await expect(Promise.reject(3)).resolves.toBe(4)
})
`,
})

expect(stderr).toMatchInlineSnapshot(`
"
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯

FAIL repro.test.ts > resolves: resolved promise with mismatched value
AssertionError: expected 3 to be 4 // Object.is equality

- Expected
+ Received

- 4
+ 3

❯ repro.test.ts:5:40
3|
4| test('resolves: resolved promise with mismatched value', async (…
5| await expect(Promise.resolve(3)).resolves.toBe(4)
| ^
6| })
7|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯

FAIL repro.test.ts > rejects: rejected promise with mismatched value
AssertionError: expected 3 to be 4 // Object.is equality

- Expected
+ Received

- 4
+ 3

❯ repro.test.ts:9:39
7|
8| test('rejects: rejected promise with mismatched value', async ()…
9| await expect(Promise.reject(3)).rejects.toBe(4)
| ^
10| })
11|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/4]⎯

FAIL repro.test.ts > rejects: resolves when rejection expected
AssertionError: promise resolved "3" instead of rejecting

- Expected:
Error {
"message": "rejected promise",
}

+ Received:
3

❯ repro.test.ts:13:40
11|
12| test('rejects: resolves when rejection expected', async () => {
13| await expect(Promise.resolve(3)).rejects.toBe(4)
| ^
14| })
15|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/4]⎯

FAIL repro.test.ts > resolves: rejects when resolve expected
AssertionError: promise rejected "3" instead of resolving
❯ repro.test.ts:17:39
15|
16| test('resolves: rejects when resolve expected', async () => {
17| await expect(Promise.reject(3)).resolves.toBe(4)
| ^
18| })
19|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/4]⎯

"
`)
expect(errorTree()).toMatchInlineSnapshot(`
{
"repro.test.ts": {
"rejects: rejected promise with mismatched value": [
"expected 3 to be 4 // Object.is equality",
],
"rejects: resolves when rejection expected": [
"promise resolved "3" instead of rejecting",
],
"resolves: rejects when resolve expected": [
"promise rejected "3" instead of resolving",
],
"resolves: resolved promise with mismatched value": [
"expected 3 to be 4 // Object.is equality",
],
},
}
`)
})
58 changes: 57 additions & 1 deletion test/cli/test/vm-threads.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest'

import { createFile, resolvePath, runVitest } from '../../test-utils'
import { createFile, resolvePath, runInlineTests, runVitest } from '../../test-utils'

test('importing files in restricted fs works correctly', async () => {
createFile(
Expand All @@ -15,3 +15,59 @@ test('importing files in restricted fs works correctly', async () => {
expect(stderr).toBe('')
expect(exitCode).toBe(0)
})

// The module-sync condition was added in Node 22.12/20.19 when require(esm)
// was unflagged. The fix uses the _resolveFilename conditions option which
// is only available on Node 22.12+. Node 20 is unfixable and reaches EOL
// April 2026.
const nodeMajor = Number(process.versions.node.split('.')[0])
test.skipIf(nodeMajor < 22)('can require package with module-sync exports condition', async () => {
const { stderr, exitCode } = await runInlineTests({
// .mjs module-sync entry
'node_modules/module-sync-mjs/package.json': JSON.stringify({
name: 'module-sync-mjs',
exports: {
'.': {
'module-sync': './index.mjs',
'require': './index.cjs',
},
},
}),
'node_modules/module-sync-mjs/index.mjs': 'export const value = "esm";',
'node_modules/module-sync-mjs/index.cjs': 'module.exports = { value: "cjs" };',
// .js module-sync entry with "type": "module"
'node_modules/module-sync-js/package.json': JSON.stringify({
name: 'module-sync-js',
type: 'module',
exports: {
'.': {
'module-sync': './index.js',
'require': './index.cjs',
},
},
}),
'node_modules/module-sync-js/index.js': 'export const value = "esm";',
'node_modules/module-sync-js/index.cjs': 'module.exports = { value: "cjs" };',
'basic.test.js': `
import { createRequire } from 'node:module'
import { expect, test } from 'vitest'

const require = createRequire(import.meta.url)

test('require loads cjs entry for module-sync package (.mjs)', () => {
const mod = require('module-sync-mjs')
expect(mod.value).toBe('cjs')
})

test('require loads cjs entry for module-sync package (.js with type: module)', () => {
const mod = require('module-sync-js')
expect(mod.value).toBe('cjs')
})
`,
}, {
pool: 'vmThreads',
})

expect(stderr).toBe('')
expect(exitCode).toBe(0)
})
Loading
Loading