Skip to content

Commit e1f0e02

Browse files
nizosclaude
andcommitted
feat(jest): report module import errors as failed tests
Previously, Jest reporter did not capture module import errors when tests failed to load due to missing dependencies or syntax errors. This meant the error information was lost, making it harder to diagnose test failures. Now the reporter: - Detects when a test module fails to load (testExecError present with no test results) - Creates a synthetic failed test entry with descriptive name like "Module failed to load (SyntaxError)" - Preserves full error details including message, stack trace, error type, and error code - Provides consistent error reporting across all supported test frameworks This improves debugging experience by ensuring all test failures are visible in the captured test results, matching the behavior of Vitest and other framework reporters. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 606cdbf commit e1f0e02

File tree

7 files changed

+190
-10
lines changed

7 files changed

+190
-10
lines changed

reporters/jest/src/JestReporter.test-data.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,37 @@ export function createUnhandledError(
163163
// SerializableError might have additional properties but these are the required ones
164164
}
165165
}
166+
167+
// Create a module error (testExecError) for import failures
168+
export function createModuleError(
169+
overrides: Partial<{
170+
name: string
171+
message: string
172+
stack: string
173+
type: string
174+
code: string
175+
}> = {}
176+
): TestResult['testExecError'] {
177+
return {
178+
message: overrides.message ?? "Cannot find module './non-existent-module'",
179+
stack:
180+
overrides.stack ??
181+
"Error: Cannot find module './non-existent-module'\n at Resolver.resolveModule",
182+
...(overrides.name && { name: overrides.name }),
183+
...(overrides.type && { type: overrides.type }),
184+
...(overrides.code && { code: overrides.code }),
185+
}
186+
}
187+
188+
// Create a TestResult with module import error
189+
export function createTestResultWithModuleError(
190+
overrides?: Partial<TestResult>
191+
): TestResult {
192+
return createTestResult({
193+
testExecError: createModuleError(),
194+
testResults: [], // No test results when module fails to load
195+
numFailingTests: 0,
196+
numPassingTests: 0,
197+
...overrides,
198+
})
199+
}

reporters/jest/src/JestReporter.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
createTestResult,
88
createAggregatedResult,
99
createUnhandledError,
10+
createModuleError,
11+
createTestResultWithModuleError,
1012
} from './JestReporter.test-data'
1113

1214
describe('JestReporter', () => {
@@ -174,6 +176,101 @@ describe('JestReporter', () => {
174176
expect(parsed.unhandledErrors[0].name).toBe('Error')
175177
expect(parsed.unhandledErrors[0].stack).toBe('at test.js:1:1')
176178
})
179+
180+
it('includes module import errors as failed tests', async () => {
181+
const test = createTest()
182+
const testResult = createTestResultWithModuleError()
183+
const aggregatedResult = createAggregatedResult()
184+
185+
sut.reporter.onTestResult(test, testResult)
186+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
187+
188+
const parsed = await sut.getParsedData()
189+
const module = parsed.testModules[0]
190+
expect(module.tests).toHaveLength(1)
191+
192+
const importErrorTest = module.tests[0]
193+
expect(importErrorTest.name).toBe('Module failed to load (Error)')
194+
expect(importErrorTest.fullName).toBe('Module failed to load (Error)')
195+
expect(importErrorTest.state).toBe('failed')
196+
expect(importErrorTest.errors).toHaveLength(1)
197+
expect(importErrorTest.errors[0].message).toBe(
198+
"Cannot find module './non-existent-module'"
199+
)
200+
})
201+
202+
it('preserves error stack trace from module import errors', async () => {
203+
const test = createTest()
204+
const moduleError = createModuleError({
205+
message: "Cannot find module './helpers'",
206+
stack:
207+
"Error: Cannot find module './helpers'\n at Function.Module._resolveFilename",
208+
name: 'Error',
209+
})
210+
const testResult = createTestResultWithModuleError({
211+
testExecError: moduleError,
212+
})
213+
const aggregatedResult = createAggregatedResult()
214+
215+
sut.reporter.onTestResult(test, testResult)
216+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
217+
218+
const parsed = await sut.getParsedData()
219+
const importErrorTest = parsed.testModules[0].tests[0]
220+
221+
expect(importErrorTest.errors[0].stack).toBe(
222+
"Error: Cannot find module './helpers'\n at Function.Module._resolveFilename"
223+
)
224+
expect(importErrorTest.errors[0].name).toBe('Error')
225+
})
226+
227+
it('uses error type in test name for module import errors', async () => {
228+
const test = createTest()
229+
const testResult = createTestResultWithModuleError({
230+
testExecError: createModuleError({
231+
message: 'Module parse failed',
232+
stack: 'SyntaxError: Unexpected token',
233+
name: 'SyntaxError',
234+
}),
235+
})
236+
const aggregatedResult = createAggregatedResult()
237+
238+
sut.reporter.onTestResult(test, testResult)
239+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
240+
241+
const parsed = await sut.getParsedData()
242+
const importErrorTest = parsed.testModules[0].tests[0]
243+
244+
expect(importErrorTest.name).toBe('Module failed to load (SyntaxError)')
245+
expect(importErrorTest.fullName).toBe(
246+
'Module failed to load (SyntaxError)'
247+
)
248+
})
249+
250+
it('handles SerializableError with type field for module import errors', async () => {
251+
const test = createTest()
252+
const testResult = createTestResultWithModuleError({
253+
testExecError: createModuleError({
254+
message: 'Module error',
255+
stack: 'at test.js:1',
256+
type: 'ReferenceError',
257+
code: 'ERR_MODULE_NOT_FOUND',
258+
}),
259+
})
260+
const aggregatedResult = createAggregatedResult()
261+
262+
sut.reporter.onTestResult(test, testResult)
263+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
264+
265+
const parsed = await sut.getParsedData()
266+
const importErrorTest = parsed.testModules[0].tests[0]
267+
268+
expect(importErrorTest.name).toBe(
269+
'Module failed to load (ReferenceError)'
270+
)
271+
expect(importErrorTest.errors[0].name).toBe('ReferenceError')
272+
expect(importErrorTest.errors[0].operator).toBe('ERR_MODULE_NOT_FOUND')
273+
})
177274
})
178275
})
179276

reporters/jest/src/JestReporter.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,54 @@ export class JestReporter extends BaseReporter {
127127
}
128128
}
129129

130+
private createTestFromExecError(execError: unknown): CapturedTest {
131+
const errorObj = execError as Record<string, unknown>
132+
const message = String(errorObj.message ?? 'Unknown error')
133+
134+
const error: CapturedError = {
135+
message,
136+
name: typeof errorObj.name === 'string' ? errorObj.name : 'Error',
137+
stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
138+
}
139+
140+
// Extract additional fields from Jest's SerializableError
141+
if ('code' in errorObj && errorObj.code !== undefined) {
142+
error.operator = String(errorObj.code)
143+
}
144+
if ('type' in errorObj && typeof errorObj.type === 'string') {
145+
error.name = errorObj.type
146+
}
147+
148+
const errorType = error.name ?? 'Error'
149+
const testName = `Module failed to load (${errorType})`
150+
151+
return {
152+
name: testName,
153+
fullName: testName,
154+
state: 'failed',
155+
errors: [error],
156+
}
157+
}
158+
130159
private buildTestModules(): CapturedModule[] {
131-
return Array.from(this.testModules.entries()).map(([path, data]) => ({
132-
moduleId: path,
133-
tests: data.testResult.testResults.map((test: AssertionResult) =>
134-
this.mapTestResult(test)
135-
),
136-
}))
160+
return Array.from(this.testModules.entries()).map(([path, data]) => {
161+
const { testResult } = data
162+
163+
// Handle module/import errors
164+
if (testResult.testExecError && testResult.testResults.length === 0) {
165+
return {
166+
moduleId: path,
167+
tests: [this.createTestFromExecError(testResult.testExecError)],
168+
}
169+
}
170+
171+
return {
172+
moduleId: path,
173+
tests: testResult.testResults.map((test: AssertionResult) =>
174+
this.mapTestResult(test)
175+
),
176+
}
177+
})
137178
}
138179

139180
private buildUnhandledErrors(

reporters/test/artifacts/jest/single-import-error.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { nonExistentFunction } = require('./non-existent-module')
22

33
describe('Calculator', () => {
44
test('should add numbers correctly', () => {
5+
nonExistentFunction()
56
expect(2 + 3).toBe(5)
67
})
78
})

reporters/test/artifacts/pytest/test_single_import_error.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
class TestCalculator:
44
def test_should_add_numbers_correctly(self):
5+
non_existent_module()
56
assert 2 + 3 == 5

reporters/test/artifacts/vitest/single-import-error.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { nonExistentFunction } from './non-existent-module'
33

44
describe('Calculator', () => {
55
test('should add numbers correctly', () => {
6+
nonExistentFunction()
67
expect(2 + 3).toBe(5)
78
})
89
})

reporters/test/reporters.integration.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ describe('Reporters', () => {
119119
name: ReporterName
120120
expected: string | undefined
121121
}> = [
122-
{ name: 'jest', expected: undefined },
122+
{ name: 'jest', expected: 'Module failed to load (Error)' },
123123
{ name: 'vitest', expected: undefined },
124124
{ name: 'phpunit', expected: 'testShouldAddNumbersCorrectly' },
125125
{
@@ -207,7 +207,7 @@ describe('Reporters', () => {
207207
name: ReporterName
208208
expected: string | undefined
209209
}> = [
210-
{ name: 'jest', expected: undefined },
210+
{ name: 'jest', expected: 'Module failed to load (Error)' },
211211
{ name: 'vitest', expected: undefined },
212212
{
213213
name: 'phpunit',
@@ -259,7 +259,7 @@ describe('Reporters', () => {
259259
name: ReporterName
260260
expected: string | undefined
261261
}> = [
262-
{ name: 'jest', expected: undefined },
262+
{ name: 'jest', expected: 'failed' },
263263
{ name: 'vitest', expected: undefined },
264264
{ name: 'phpunit', expected: 'errored' },
265265
{ name: 'pytest', expected: 'failed' },
@@ -360,7 +360,12 @@ describe('Reporters', () => {
360360
name: ReporterName
361361
expected: string[] | undefined
362362
}> = [
363-
{ name: 'jest', expected: undefined },
363+
{
364+
name: 'jest',
365+
expected: [
366+
"Cannot find module './non-existent-module' from 'single-import-error.test.js'",
367+
],
368+
},
364369
{ name: 'vitest', expected: undefined },
365370
{ name: 'phpunit', expected: ['Class', 'not found'] },
366371
{

0 commit comments

Comments
 (0)