Skip to content

Commit

Permalink
feat: point timeout and regression errors to the source of the test c…
Browse files Browse the repository at this point in the history
…allback
  • Loading branch information
thetutlage committed Jan 4, 2025
1 parent f9b6a07 commit 84a9966
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 8 deletions.
29 changes: 24 additions & 5 deletions src/test/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ export class Test<
#executed: boolean = false
#failed: boolean = false

/**
* Debugging Error is used to point the errors to the source of
* the test.
*
* Since tests are executed after they are created, the errors thrown
* by the internals of Japa will never point to the original test.
* Therefore, this debuggingError property is used to retain
* the source of the test callback.
*/
#debuggingError: Error | null = null

/**
* Reference to registered hooks
*/
Expand Down Expand Up @@ -352,7 +363,8 @@ export class Test<
/**
* Define the test executor function
*/
run(executor: TestExecutor<Context, TestData>): this {
run(executor: TestExecutor<Context, TestData>, debuggingError?: Error): this {
this.#debuggingError = debuggingError || new Error()
this.options.executor = executor
return this
}
Expand Down Expand Up @@ -457,6 +469,7 @@ export class Test<
executing: self.executingCallbacks,
executed: self.executedCallbacks,
},
this.#debuggingError,
index
)

Expand All @@ -483,10 +496,16 @@ export class Test<
*/
await this.#computeContext()

this.#activeRunner = new TestRunner(this, this.#hooks, this.#emitter, {
executing: self.executingCallbacks,
executed: self.executedCallbacks,
})
this.#activeRunner = new TestRunner(
this,
this.#hooks,
this.#emitter,
{
executing: self.executingCallbacks,
executed: self.executedCallbacks,
},
this.#debuggingError
)

await this.#activeRunner.run()
this.#failed = this.#activeRunner.failed
Expand Down
26 changes: 23 additions & 3 deletions src/test/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class DummyRunner {
*/
export class TestRunner {
#emitter: Emitter
#debuggingError: Error | null

/**
* Timeout timer and promise reject method references to
Expand Down Expand Up @@ -163,12 +164,14 @@ export class TestRunner {
executing: ((test: Test<any, any>) => void)[]
executed: ((test: Test<any, any>, hasError: boolean, errors: TestEndNode['errors']) => void)[]
},
debuggingError: Error | null,
datasetCurrentIndex?: number
) {
this.#test = test
this.#hooks = hooks
this.#emitter = emitter
this.#callbacks = callbacks
this.#debuggingError = debuggingError
this.#datasetCurrentIndex = datasetCurrentIndex
this.#setupRunner = hooks.runner('setup')
this.#teardownRunner = hooks.runner('teardown')
Expand Down Expand Up @@ -218,6 +221,21 @@ export class TestRunner {
this.#emitter.emit('test:start', startOptions)
}

/**
* Creates an error to be used for reporting errors with the
* execution of the test.
*
* First, the `debuggingError` property is used (if exists), otherwise
* an inline Error instance is created
*/
#createError(message: string): Error {
if (this.#debuggingError) {
this.#debuggingError.message = message
return this.#debuggingError
}
return new Error(message)
}

/**
* Notify the reporter about the test start
*/
Expand Down Expand Up @@ -353,7 +371,7 @@ export class TestRunner {
debug('wrapping test in timeout timer')
this.#timeout = {
reject,
timer: setTimeout(() => this.#timeout!.reject(new Error('Test timeout')), duration),
timer: setTimeout(() => this.#timeout!.reject(this.#createError('Test timeout')), duration),
}
})
}
Expand All @@ -366,7 +384,7 @@ export class TestRunner {
debug('resetting timer')
clearTimeout(this.#timeout.timer)
this.#timeout.timer = setTimeout(
() => this.#timeout!.reject(new Error('Test timeout')),
() => this.#timeout!.reject(this.#createError('Test timeout')),
duration
)
}
Expand Down Expand Up @@ -396,7 +414,9 @@ export class TestRunner {
;(this.#test.options.waitsForDone ? this.#runTestWithDone() : this.#runTest())
.then(() => {
reject(
new Error('Expected regression test to fail, instead it finished without any errors')
this.#createError(
'Expected regression test to fail, instead it finished without any errors'
)
)
})
.catch(() => resolve())
Expand Down
54 changes: 54 additions & 0 deletions tests/test/execute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,29 @@ test.describe('execute | async', () => {
assert.deepEqual(stack, ['executed'])
})

test('report timeout error with a predefined error instance', async () => {
const stack: string[] = []
const timeoutError = new Error()
const emitter = new Emitter()
const refiner = new Refiner({})

const testInstance = new Test('2 + 2 = 4', new TestContext(), emitter, refiner)
testInstance.run(async () => {
stack.push('executed')
await sleep(4000)
}, timeoutError)

const [, event] = await Promise.all([testInstance.exec(), pEvent(emitter, 'test:end', 5000)])

assert.isTrue(event!.hasError)
assert.lengthOf(event!.errors, 1)
assert.equal(event!.errors[0].phase, 'test')
assert.instanceOf(event!.errors[0].error, Error)
assert.match(event!.errors[0].error.stack?.split('\n')[1]!, /execute\.spec\.ts\:199/)
assert.equal(event!.errors[0].error.message, 'Test timeout')
assert.deepEqual(stack, ['executed'])
})

test('increase test timeout from within the callback', async () => {
const stack: string[] = []
const emitter = new Emitter()
Expand Down Expand Up @@ -231,6 +254,7 @@ test.describe('execute | async', () => {

assert.isFalse(event!.hasError)
assert.isFalse(testInstance.failed)
assert.isTrue(event!.isFailing)
assert.lengthOf(event!.errors, 0)
assert.deepEqual(stack, ['executed'])
})
Expand All @@ -253,11 +277,41 @@ test.describe('execute | async', () => {
assert.isTrue(event!.hasError)
assert.isTrue(testInstance.failed)
assert.lengthOf(event!.errors, 1)
assert.isTrue(event!.isFailing)
assert.equal(event!.errors[0].phase, 'test')
assert.equal(
event!.errors[0].error.message,
'Expected regression test to fail, instead it finished without any errors'
)
assert.deepEqual(stack, ['executed'])
})

test('report regression test error with a predefined error instance', async () => {
const stack: string[] = []
const regressionError = new Error()
const emitter = new Emitter()
const refiner = new Refiner({})

const testInstance = new Test('2 + 2 = 4', new TestContext(), emitter, refiner)
testInstance
.run(async () => {
stack.push('executed')
}, regressionError)
.fails()

const [, event] = await Promise.all([testInstance.exec(), pEvent(emitter, 'test:end')])
assert.isDefined(event)

assert.isTrue(event!.hasError)
assert.isTrue(testInstance.failed)
assert.isTrue(event!.isFailing)
assert.lengthOf(event!.errors, 1)
assert.equal(event!.errors[0].phase, 'test')
assert.equal(
event!.errors[0].error.message,
'Expected regression test to fail, instead it finished without any errors'
)
assert.match(event!.errors[0].error.stack?.split('\n')[1]!, /execute\.spec\.ts\:291/)
assert.deepEqual(stack, ['executed'])
})

Expand Down

0 comments on commit 84a9966

Please sign in to comment.