Skip to content

Commit 44f9bbc

Browse files
authored
Add error to After hook parameter (#2572)
1 parent e9261dc commit 44f9bbc

File tree

8 files changed

+104
-32
lines changed

8 files changed

+104
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber
1010
## [Unreleased]
1111
### Added
1212
- Allow comments inside descriptions ([cucumber/gherkin#334](https://github.com/cucumber/gherkin/pull/334))
13+
- Add original `error` to `After` and `AfterStep` hook parameters ([#2572](https://github.com/cucumber/cucumber-js/pull/2572))
1314

1415
### Changed
1516
- Redesigned HTML formatter header ([cucumber/react-components#381](https://github.com/cucumber/react-components/pull/381))

docs/support_files/api_reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Defines a hook which is run after each scenario.
3939
* `tags`: String tag expression used to apply this hook to only specific scenarios. See [cucumber-tag-expressions](https://github.com/cucumber/tag-expressions) for more information.
4040
* `timeout`: A hook-specific timeout, to override the default timeout.
4141
* `fn`: A function, defined as follows:
42-
* The first argument will be an object of the form `{pickle, gherkinDocument, result, willBeRetried, testCaseStartedId}`
42+
* The first argument will be an object of the form `{pickle, gherkinDocument, result, error, willBeRetried, testCaseStartedId}`
4343
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin/v15.0.2/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
4444
* When using the asynchronous callback interface, have one final argument for the callback function.
4545

exports/api/report.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
55
```ts
66

7-
/// <reference types="node" />
8-
97
import { Envelope } from '@cucumber/messages';
108
import { JsonObject } from 'type-fest';
119
import { Writable } from 'node:stream';

exports/root/report.api.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
55
```ts
66

7-
/// <reference types="node" />
8-
97
import { EventEmitter } from 'node:events';
108
import { Expression } from '@cucumber/cucumber-expressions';
119
import { GeneratedExpression } from '@cucumber/cucumber-expressions';
@@ -29,25 +27,25 @@ import { TestStepResultStatus } from '@cucumber/messages';
2927
import { Writable } from 'node:stream';
3028

3129
// @public (undocumented)
32-
export const After: (<WorldType = IWorld_2<any>>(code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType_1 = IWorld_2<any>>(tags: string, code: TestCaseHookFunction_2<WorldType_1>) => void) & (<WorldType_2 = IWorld_2<any>>(options: IDefineTestCaseHookOptions_2, code: TestCaseHookFunction_2<WorldType_2>) => void);
30+
export const After: (<WorldType = IWorld_2<any>>(code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(tags: string, code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(options: IDefineTestCaseHookOptions_2, code: TestCaseHookFunction_2<WorldType>) => void);
3331

3432
// @public (undocumented)
3533
export const AfterAll: ((code: TestRunHookFunction_2) => void) & ((options: IDefineTestRunHookOptions_2, code: TestRunHookFunction_2) => void);
3634

3735
// @public (undocumented)
38-
export const AfterStep: (<WorldType = IWorld_2<any>>(code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType_1 = IWorld_2<any>>(tags: string, code: TestStepHookFunction_2<WorldType_1>) => void) & (<WorldType_2 = IWorld_2<any>>(options: IDefineTestStepHookOptions_2, code: TestStepHookFunction_2<WorldType_2>) => void);
36+
export const AfterStep: (<WorldType = IWorld_2<any>>(code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(tags: string, code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(options: IDefineTestStepHookOptions_2, code: TestStepHookFunction_2<WorldType>) => void);
3937

4038
// @public (undocumented)
4139
function atMostOnePicklePerTag(tagNames: string[]): ParallelAssignmentValidator;
4240

4341
// @public (undocumented)
44-
export const Before: (<WorldType = IWorld_2<any>>(code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType_1 = IWorld_2<any>>(tags: string, code: TestCaseHookFunction_2<WorldType_1>) => void) & (<WorldType_2 = IWorld_2<any>>(options: IDefineTestCaseHookOptions_2, code: TestCaseHookFunction_2<WorldType_2>) => void);
42+
export const Before: (<WorldType = IWorld_2<any>>(code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(tags: string, code: TestCaseHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(options: IDefineTestCaseHookOptions_2, code: TestCaseHookFunction_2<WorldType>) => void);
4543

4644
// @public (undocumented)
4745
export const BeforeAll: ((code: TestRunHookFunction_2) => void) & ((options: IDefineTestRunHookOptions_2, code: TestRunHookFunction_2) => void);
4846

4947
// @public (undocumented)
50-
export const BeforeStep: (<WorldType = IWorld_2<any>>(code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType_1 = IWorld_2<any>>(tags: string, code: TestStepHookFunction_2<WorldType_1>) => void) & (<WorldType_2 = IWorld_2<any>>(options: IDefineTestStepHookOptions_2, code: TestStepHookFunction_2<WorldType_2>) => void);
48+
export const BeforeStep: (<WorldType = IWorld_2<any>>(code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(tags: string, code: TestStepHookFunction_2<WorldType>) => void) & (<WorldType = IWorld_2<any>>(options: IDefineTestStepHookOptions_2, code: TestStepHookFunction_2<WorldType>) => void);
5149

5250
// @public @deprecated (undocumented)
5351
export const Cli: typeof Cli_2;
@@ -139,7 +137,7 @@ export const FormatterBuilder: {
139137
build(FormatterConstructor: string | typeof Formatter, options: IBuildOptions): Promise<Formatter>;
140138
getConstructorByType(type: string, cwd: string): Promise<typeof Formatter>;
141139
getStepDefinitionSnippetBuilder({ cwd, snippetInterface, snippetSyntax, supportCodeLibrary, }: IGetStepDefinitionSnippetBuilderOptions): Promise<StepDefinitionSnippetBuilder>;
142-
loadCustomClass(type: 'formatter' | 'syntax', descriptor: string, cwd: string): Promise<any>;
140+
loadCustomClass(type: "formatter" | "syntax", descriptor: string, cwd: string): Promise<any>;
143141
loadFile(urlOrName: URL | string): Promise<any>;
144142
resolveConstructor(ImportedCode: any): any;
145143
};
@@ -270,6 +268,8 @@ function isWarning(result: messages.TestStepResult, willBeRetried?: boolean): bo
270268

271269
// @public (undocumented)
272270
export interface ITestCaseHookParameter {
271+
// (undocumented)
272+
error?: any;
273273
// (undocumented)
274274
gherkinDocument: messages.GherkinDocument;
275275
// (undocumented)
@@ -284,6 +284,8 @@ export interface ITestCaseHookParameter {
284284

285285
// @public (undocumented)
286286
export interface ITestStepHookParameter {
287+
// (undocumented)
288+
error?: any;
287289
// (undocumented)
288290
gherkinDocument: messages.GherkinDocument;
289291
// (undocumented)

src/runtime/step_runner.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ export interface IRunOptions {
2020
world: any
2121
}
2222

23+
export interface RunStepResult {
24+
result: messages.TestStepResult
25+
error?: any
26+
}
27+
2328
export async function run({
2429
defaultTimeout,
2530
filterStackTraces,
2631
hookParameter,
2732
step,
2833
stepDefinition,
2934
world,
30-
}: IRunOptions): Promise<messages.TestStepResult> {
35+
}: IRunOptions): Promise<RunStepResult> {
3136
const stopwatch = create().start()
3237
let error: any, result: any, invocationData: IGetInvocationDataResponse
3338

@@ -83,9 +88,12 @@ export async function run({
8388
}
8489

8590
return {
86-
duration,
87-
status,
88-
...details,
91+
result: {
92+
duration,
93+
status,
94+
...details,
95+
},
96+
error,
8997
}
9098
}
9199

src/runtime/test_case_runner.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { doesHaveValue, doesNotHaveValue } from '../value_checker'
1414
import StepDefinition from '../models/step_definition'
1515
import { IWorldOptions } from '../support_code_library_builder/world'
1616
import { timestamp } from './stopwatch'
17-
import StepRunner from './step_runner'
17+
import StepRunner, { RunStepResult } from './step_runner'
1818
import AttachmentManager from './attachment_manager'
1919
import { getAmbiguousStepException } from './helpers'
2020

@@ -136,7 +136,7 @@ export default class TestCaseRunner {
136136
step: messages.PickleStep,
137137
stepDefinition: IDefinition,
138138
hookParameter?: ITestCaseHookParameter
139-
): Promise<messages.TestStepResult> {
139+
): Promise<RunStepResult> {
140140
return await StepRunner.run({
141141
defaultTimeout: this.supportCodeLibrary.defaultTimeout,
142142
filterStackTraces: this.filterStackTraces,
@@ -220,6 +220,7 @@ export default class TestCaseRunner {
220220
this.eventBroadcaster.emit('envelope', testCaseStarted)
221221
// used to determine whether a hook is a Before or After
222222
let didWeRunStepsYet = false
223+
let error = false
223224
for (const testStep of this.testCase.testSteps) {
224225
await this.aroundTestStep(testStep.id, async () => {
225226
if (doesHaveValue(testStep.hookId)) {
@@ -230,6 +231,7 @@ export default class TestCaseRunner {
230231
}
231232
if (didWeRunStepsYet) {
232233
hookParameter.result = this.getWorstStepResult()
234+
hookParameter.error = error
233235
hookParameter.willBeRetried =
234236
this.getWorstStepResult().status ===
235237
messages.TestStepResultStatus.FAILED && moreAttemptsRemaining
@@ -245,7 +247,8 @@ export default class TestCaseRunner {
245247
)
246248
const testStepResult = await this.runStep(pickleStep, testStep)
247249
didWeRunStepsYet = true
248-
return testStepResult
250+
error = testStepResult.error
251+
return testStepResult.result
249252
}
250253
})
251254
}
@@ -276,13 +279,18 @@ export default class TestCaseRunner {
276279
duration: messages.TimeConversion.millisecondsToDuration(0),
277280
}
278281
}
279-
return await this.invokeStep(null, hookDefinition, hookParameter)
282+
const { result } = await this.invokeStep(
283+
null,
284+
hookDefinition,
285+
hookParameter
286+
)
287+
return result
280288
}
281289

282290
async runStepHooks(
283291
stepHooks: TestStepHookDefinition[],
284292
pickleStep: messages.PickleStep,
285-
stepResult?: messages.TestStepResult
293+
stepResult?: RunStepResult
286294
): Promise<messages.TestStepResult[]> {
287295
const stepHooksResult = []
288296
const hookParameter: ITestStepHookParameter = {
@@ -291,44 +299,55 @@ export default class TestCaseRunner {
291299
pickleStep,
292300
testCaseStartedId: this.currentTestCaseStartedId,
293301
testStepId: this.currentTestStepId,
294-
result: stepResult,
302+
result: stepResult?.result,
303+
error: stepResult?.error,
295304
}
296305
for (const stepHookDefinition of stepHooks) {
297-
stepHooksResult.push(
298-
await this.invokeStep(null, stepHookDefinition, hookParameter)
306+
const { result } = await this.invokeStep(
307+
null,
308+
stepHookDefinition,
309+
hookParameter
299310
)
311+
stepHooksResult.push(result)
300312
}
301313
return stepHooksResult
302314
}
303315

304316
async runStep(
305317
pickleStep: messages.PickleStep,
306318
testStep: messages.TestStep
307-
): Promise<messages.TestStepResult> {
319+
): Promise<RunStepResult> {
308320
const stepDefinitions = testStep.stepDefinitionIds.map(
309321
(stepDefinitionId) => {
310322
return findStepDefinition(stepDefinitionId, this.supportCodeLibrary)
311323
}
312324
)
313325
if (stepDefinitions.length === 0) {
314326
return {
315-
status: messages.TestStepResultStatus.UNDEFINED,
316-
duration: messages.TimeConversion.millisecondsToDuration(0),
327+
result: {
328+
status: messages.TestStepResultStatus.UNDEFINED,
329+
duration: messages.TimeConversion.millisecondsToDuration(0),
330+
},
317331
}
318332
} else if (stepDefinitions.length > 1) {
319333
return {
320-
message: getAmbiguousStepException(stepDefinitions),
321-
status: messages.TestStepResultStatus.AMBIGUOUS,
322-
duration: messages.TimeConversion.millisecondsToDuration(0),
334+
result: {
335+
message: getAmbiguousStepException(stepDefinitions),
336+
status: messages.TestStepResultStatus.AMBIGUOUS,
337+
duration: messages.TimeConversion.millisecondsToDuration(0),
338+
},
323339
}
324340
} else if (this.isSkippingSteps()) {
325341
return {
326-
status: messages.TestStepResultStatus.SKIPPED,
327-
duration: messages.TimeConversion.millisecondsToDuration(0),
342+
result: {
343+
status: messages.TestStepResultStatus.SKIPPED,
344+
duration: messages.TimeConversion.millisecondsToDuration(0),
345+
},
328346
}
329347
}
330348

331349
let stepResult
350+
let error
332351
let stepResults = await this.runStepHooks(
333352
this.getBeforeStepHookDefinitions(),
334353
pickleStep
@@ -338,7 +357,8 @@ export default class TestCaseRunner {
338357
messages.TestStepResultStatus.FAILED
339358
) {
340359
stepResult = await this.invokeStep(pickleStep, stepDefinitions[0])
341-
stepResults.push(stepResult)
360+
stepResults.push(stepResult.result)
361+
error = stepResult.error
342362
}
343363
const afterStepHookResults = await this.runStepHooks(
344364
this.getAfterStepHookDefinitions(),
@@ -356,7 +376,10 @@ export default class TestCaseRunner {
356376
)
357377
}
358378
finalStepResult.duration = finalDuration
359-
return finalStepResult
379+
return {
380+
result: finalStepResult,
381+
error,
382+
}
360383
}
361384
}
362385

src/runtime/test_case_runner_spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,42 @@ describe('TestCaseRunner', () => {
186186
)
187187
expect(result).to.eql(messages.TestStepResultStatus.FAILED)
188188
})
189+
190+
it('should provide the error to AfterStep and After hooks', async () => {
191+
// Arrange
192+
const error = new Error('fail')
193+
const afterStepStub = sinon.stub()
194+
const afterStub = sinon.stub()
195+
const supportCodeLibrary = buildSupportCodeLibrary(
196+
({ Given, AfterStep, After }) => {
197+
Given('a step', function () {
198+
throw error
199+
})
200+
AfterStep(afterStepStub)
201+
After(afterStub)
202+
}
203+
)
204+
const {
205+
gherkinDocument,
206+
pickles: [pickle],
207+
} = await parse({
208+
data: ['Feature: a', 'Scenario: b', 'Given a step'].join('\n'),
209+
uri: 'a.feature',
210+
})
211+
212+
// Act
213+
await testRunner({
214+
gherkinDocument,
215+
pickle,
216+
supportCodeLibrary,
217+
})
218+
219+
// Assert
220+
expect(afterStepStub).to.have.been.calledOnce()
221+
expect(afterStepStub.lastCall.firstArg.error).to.eq(error)
222+
expect(afterStub).to.have.been.calledOnce()
223+
expect(afterStub.lastCall.firstArg.error).to.eq(error)
224+
})
189225
})
190226

191227
describe('with an ambiguous step', () => {
@@ -522,6 +558,7 @@ describe('TestCaseRunner', () => {
522558
testCaseStartedId: envelopes[1].testStepStarted.testCaseStartedId,
523559
testStepId: envelopes[1].testStepStarted.testStepId,
524560
result: undefined,
561+
error: undefined,
525562
})
526563
expect(afterStep).to.have.been.calledOnceWith({
527564
gherkinDocument,
@@ -530,6 +567,7 @@ describe('TestCaseRunner', () => {
530567
testCaseStartedId: envelopes[2].testStepFinished.testCaseStartedId,
531568
testStepId: envelopes[2].testStepFinished.testStepId,
532569
result: envelopes[2].testStepFinished.testStepResult,
570+
error: undefined,
533571
})
534572
})
535573
})

src/support_code_library_builder/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ITestCaseHookParameter {
1616
gherkinDocument: messages.GherkinDocument
1717
pickle: messages.Pickle
1818
result?: messages.TestStepResult
19+
error?: any
1920
willBeRetried?: boolean
2021
testCaseStartedId: string
2122
}
@@ -25,6 +26,7 @@ export interface ITestStepHookParameter {
2526
pickle: messages.Pickle
2627
pickleStep: messages.PickleStep
2728
result: messages.TestStepResult
29+
error?: any
2830
testCaseStartedId: string
2931
testStepId: string
3032
}

0 commit comments

Comments
 (0)