diff --git a/CHANGELOG.md b/CHANGELOG.md index 9658af869..10016b7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Added +- Add support for named hooks (see [documentation](./docs/support_files/hooks.md#named-hooks)) ([#1994](https://github.com/cucumber/cucumber-js/pull/1994)) + ### Changed - Rename the `cucumber-js` binary's underlying file to be `cucumber.js`, so it doesn't fall foul of Node.js module conventions and plays nicely with ESM loaders (see [documentation](./docs/esm.md#transpiling)) ([#1993](https://github.com/cucumber/cucumber-js/pull/1993)) diff --git a/compatibility/features/hooks/hooks.ts b/compatibility/features/hooks/hooks.ts index 1ce001c6f..073b355c8 100644 --- a/compatibility/features/hooks/hooks.ts +++ b/compatibility/features/hooks/hooks.ts @@ -6,6 +6,10 @@ Before(function () { // no-op }) +Before({ name: 'A named hook' }, function () { + // no-op +}) + When('a step passes', function () { // no-op }) diff --git a/docs/support_files/hooks.md b/docs/support_files/hooks.md index df9e25a63..c7aba803d 100644 --- a/docs/support_files/hooks.md +++ b/docs/support_files/hooks.md @@ -6,7 +6,7 @@ Note that your hook functions cannot reference the [world](./world.md) as `this` arrow functions. See [FAQ](../faq.md) for details. ```javascript -var {After, Before} = require('@cucumber/cucumber'); +const {After, Before} = require('@cucumber/cucumber'); // Synchronous Before(function () { @@ -33,12 +33,28 @@ After(function () { }); ``` +## Named hooks + +ℹ️ Added in v8.1.0 + +Hooks can optionally be named: + +```javascript +const {Before} = require('@cucumber/cucumber'); + +Before({name: "Set up some test state"}, function () { +// do stuff here +}); +``` + +Such hooks will then be referenced by name in [formatter](../formatters.md) output, which can be useful to help you understand what's happening with your tests. + ## Tagged hooks Hooks can be conditionally selected for execution based on the tags of the scenario. ```javascript -var {After, Before} = require('@cucumber/cucumber'); +const {After, Before} = require('@cucumber/cucumber'); Before(function () { // This hook will be executed before all scenarios @@ -85,7 +101,7 @@ If you have some setup / teardown that needs to be done before or after all scen Unlike `Before` / `After` these methods will not have a world instance as `this`. This is because each scenario gets its own world instance and these hooks run before / after **all** scenarios. ```javascript -var {AfterAll, BeforeAll} = require('@cucumber/cucumber'); +const {AfterAll, BeforeAll} = require('@cucumber/cucumber'); // Synchronous BeforeAll(function () { @@ -111,7 +127,7 @@ AfterAll(function () { If you have some code execution that needs to be done before or after all steps, use `BeforeStep` / `AfterStep`. Like the `Before` / `After` hooks, these also have a world instance as 'this', and can be conditionally selected for execution based on the tags of the scenario. ```javascript -var {AfterStep, BeforeStep} = require('@cucumber/cucumber'); +const {AfterStep, BeforeStep} = require('@cucumber/cucumber'); BeforeStep({tags: "@foo"}, function () { // This hook will be executed before all steps in a scenario with tag @foo diff --git a/features/named_hooks.feature b/features/named_hooks.feature new file mode 100644 index 000000000..be68a4f34 --- /dev/null +++ b/features/named_hooks.feature @@ -0,0 +1,35 @@ +Feature: Named hooks + + As a developer + I want to name a `Before` or `After` hook + So that I can easily identify which hooks are run when reporting + + Scenario: Hook is named and then referenced by its name in formatter output + Given a file named "features/a.feature" with: + """ + Feature: some feature + Scenario: some scenario + Given a step + """ + And a file named "features/step_definitions/hooks.js" with: + """ + const {After, Before} = require('@cucumber/cucumber') + + Before({name: 'hook 1'}, function() {}) + Before({name: 'hook 2'}, function() {}) + After({name: 'hook 3'}, function() {}) + """ + And a file named "features/step_definitions/steps.js" with: + """ + const {Given} = require('@cucumber/cucumber') + + Given('a step', function() { + throw 'nope' + }) + """ + When I run cucumber-js + Then it fails + And the output contains the text: + """ + Before (hook 2) # + """ diff --git a/package-lock.json b/package-lock.json index 4e436d2bb..fa57ace66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@cucumber/gherkin": "23.0.1", "@cucumber/gherkin-streams": "5.0.1", "@cucumber/gherkin-utils": "^7.0.0", - "@cucumber/html-formatter": "19.0.0", + "@cucumber/html-formatter": "19.1.0", "@cucumber/messages": "18.0.0", "@cucumber/tag-expressions": "4.1.0", "assertion-error-formatter": "^3.0.0", @@ -48,7 +48,7 @@ "cucumber-js": "bin/cucumber.js" }, "devDependencies": { - "@cucumber/compatibility-kit": "9.1.2", + "@cucumber/compatibility-kit": "9.2.0", "@cucumber/message-streams": "4.0.1", "@cucumber/query": "11.0.0", "@microsoft/api-documenter": "7.17.0", @@ -537,10 +537,13 @@ "integrity": "sha512-da6H/wtVerhGUP4OCWTOmbNd4+gC1FhAcLzYgn6O68HgQbMwkmV3M8AwtbQWZkfF+Ph7z0M/UQYYdNIDu5V5MA==" }, "node_modules/@cucumber/compatibility-kit": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-9.1.2.tgz", - "integrity": "sha512-oB01JROFcwFfbqMV+jtJtj8bWU6mrLPUomuki/f9TvXsHMjYgqkBopeJqjcWWtgIfA7Y2CZEnTMWdLxoyBd4RA==", - "dev": true + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-9.2.0.tgz", + "integrity": "sha512-NsXLgmVorgBIJpNrP6ISqIsFD62XCfmzAmgVE0HEz5DkfRbE78adT1UvzzAvrlJPNOG5FX6RF3Ss9Sg/f8gYOg==", + "dev": true, + "dependencies": { + "@cucumber/message-streams": "^4.0.1" + } }, "node_modules/@cucumber/cucumber-expressions": { "version": "15.0.2", @@ -608,11 +611,11 @@ } }, "node_modules/@cucumber/html-formatter": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-19.0.0.tgz", - "integrity": "sha512-7PCnouI7BVmTU0eXFbJHQkxSQVIoAVa6PSmdcjG+jK8yn2X+YFYQinmLrcZkvEWlYgTHB/8GPle/8EGKQ0Ij9A==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-19.1.0.tgz", + "integrity": "sha512-VCsRa34SNg9plfziFwOaoCSfsphHUb1Ivk8px8eLJc0rBFLDPDgJcHJtcufAu6AxFamGiptt2dt0XoqVq2Gr/Q==", "peerDependencies": { - "@cucumber/messages": ">=17" + "@cucumber/messages": ">=18" } }, "node_modules/@cucumber/message-streams": { @@ -8044,10 +8047,13 @@ "integrity": "sha512-da6H/wtVerhGUP4OCWTOmbNd4+gC1FhAcLzYgn6O68HgQbMwkmV3M8AwtbQWZkfF+Ph7z0M/UQYYdNIDu5V5MA==" }, "@cucumber/compatibility-kit": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-9.1.2.tgz", - "integrity": "sha512-oB01JROFcwFfbqMV+jtJtj8bWU6mrLPUomuki/f9TvXsHMjYgqkBopeJqjcWWtgIfA7Y2CZEnTMWdLxoyBd4RA==", - "dev": true + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-9.2.0.tgz", + "integrity": "sha512-NsXLgmVorgBIJpNrP6ISqIsFD62XCfmzAmgVE0HEz5DkfRbE78adT1UvzzAvrlJPNOG5FX6RF3Ss9Sg/f8gYOg==", + "dev": true, + "requires": { + "@cucumber/message-streams": "^4.0.1" + } }, "@cucumber/cucumber-expressions": { "version": "15.0.2", @@ -8103,9 +8109,9 @@ } }, "@cucumber/html-formatter": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-19.0.0.tgz", - "integrity": "sha512-7PCnouI7BVmTU0eXFbJHQkxSQVIoAVa6PSmdcjG+jK8yn2X+YFYQinmLrcZkvEWlYgTHB/8GPle/8EGKQ0Ij9A==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-19.1.0.tgz", + "integrity": "sha512-VCsRa34SNg9plfziFwOaoCSfsphHUb1Ivk8px8eLJc0rBFLDPDgJcHJtcufAu6AxFamGiptt2dt0XoqVq2Gr/Q==", "requires": {} }, "@cucumber/message-streams": { diff --git a/package.json b/package.json index 810146fee..911aa7dc2 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "@cucumber/gherkin": "23.0.1", "@cucumber/gherkin-streams": "5.0.1", "@cucumber/gherkin-utils": "^7.0.0", - "@cucumber/html-formatter": "19.0.0", + "@cucumber/html-formatter": "19.1.0", "@cucumber/messages": "18.0.0", "@cucumber/tag-expressions": "4.1.0", "assertion-error-formatter": "^3.0.0", @@ -230,7 +230,7 @@ "yup": "^0.32.11" }, "devDependencies": { - "@cucumber/compatibility-kit": "9.1.2", + "@cucumber/compatibility-kit": "9.2.0", "@cucumber/message-streams": "4.0.1", "@cucumber/query": "11.0.0", "@microsoft/api-documenter": "7.17.0", diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts index 1d54419f6..866321202 100644 --- a/src/cli/helpers.ts +++ b/src/cli/helpers.ts @@ -195,6 +195,7 @@ function emitTestCaseHooks( const envelope: messages.Envelope = { hook: { id: testCaseHookDefinition.id, + name: testCaseHookDefinition.name, tagExpression: testCaseHookDefinition.tagExpression, sourceReference: { uri: testCaseHookDefinition.uri, diff --git a/src/cli/helpers_spec.ts b/src/cli/helpers_spec.ts index 214269d60..1c658c789 100644 --- a/src/cli/helpers_spec.ts +++ b/src/cli/helpers_spec.ts @@ -220,6 +220,7 @@ describe('helpers', () => { id: '0', line: 3, options: { + name: 'before hook', tags: '@hooks-tho', }, uri: 'features/support/hooks.js', @@ -231,7 +232,9 @@ describe('helpers', () => { unwrappedCode: noopFunction, id: '1', line: 7, - options: {}, + options: { + name: 'after hook', + }, uri: 'features/support/hooks.js', }), new TestCaseHookDefinition({ @@ -249,6 +252,7 @@ describe('helpers', () => { { hook: { id: '0', + name: 'before hook', tagExpression: '@hooks-tho', sourceReference: { uri: 'features/support/hooks.js', @@ -261,6 +265,7 @@ describe('helpers', () => { { hook: { id: '1', + name: 'after hook', tagExpression: undefined, sourceReference: { uri: 'features/support/hooks.js', @@ -273,6 +278,7 @@ describe('helpers', () => { { hook: { id: '2', + name: undefined, tagExpression: undefined, sourceReference: { uri: 'features/support/hooks.js', diff --git a/src/formatter/helpers/test_case_attempt_formatter.ts b/src/formatter/helpers/test_case_attempt_formatter.ts index 87aa30f9b..7988affa6 100644 --- a/src/formatter/helpers/test_case_attempt_formatter.ts +++ b/src/formatter/helpers/test_case_attempt_formatter.ts @@ -49,6 +49,7 @@ function formatStep({ printAttachments, }: IFormatStepRequest): string { const { + name, result: { status }, actionLocation, attachments, @@ -56,6 +57,9 @@ function formatStep({ const colorFn = colorFns.forStatus(status) const identifier = testStep.keyword + valueOrDefault(testStep.text, '') let text = colorFn(`${CHARACTERS.get(status)} ${identifier}`) + if (doesHaveValue(name)) { + text += colorFn(` (${name})`) + } if (doesHaveValue(actionLocation)) { text += ` # ${colorFns.location(formatLocation(actionLocation))}` } diff --git a/src/formatter/helpers/test_case_attempt_parser.ts b/src/formatter/helpers/test_case_attempt_parser.ts index ddf04d4f5..3f92285fa 100644 --- a/src/formatter/helpers/test_case_attempt_parser.ts +++ b/src/formatter/helpers/test_case_attempt_parser.ts @@ -18,6 +18,7 @@ export interface IParsedTestStep { argument?: messages.PickleStepArgument attachments: messages.Attachment[] keyword: string + name?: string result: messages.TestStepResult snippet?: string sourceLocation?: ILineAndUri @@ -87,6 +88,7 @@ function parseStep({ uri: hookDefinition.uri, line: hookDefinition.line, } + out.name = hookDefinition.name } if ( doesHaveValue(testStep.stepDefinitionIds) && diff --git a/src/models/definition.ts b/src/models/definition.ts index 171e89648..a7a64d9ae 100644 --- a/src/models/definition.ts +++ b/src/models/definition.ts @@ -20,6 +20,7 @@ export interface IDefinitionOptions { } export interface IHookDefinitionOptions extends IDefinitionOptions { + name?: string tags?: string } diff --git a/src/models/test_case_hook_definition.ts b/src/models/test_case_hook_definition.ts index 21d8b47e7..737eb5ada 100644 --- a/src/models/test_case_hook_definition.ts +++ b/src/models/test_case_hook_definition.ts @@ -12,11 +12,13 @@ export default class TestCaseHookDefinition extends Definition implements IDefinition { + public readonly name: string public readonly tagExpression: string private readonly pickleTagFilter: PickleTagFilter constructor(data: IDefinitionParameters) { super(data) + this.name = data.options.name this.tagExpression = data.options.tags this.pickleTagFilter = new PickleTagFilter(data.options.tags) } diff --git a/src/support_code_library_builder/types.ts b/src/support_code_library_builder/types.ts index 1dae3542b..5c03d3f77 100644 --- a/src/support_code_library_builder/types.ts +++ b/src/support_code_library_builder/types.ts @@ -48,6 +48,7 @@ export interface IDefineStepOptions { } export interface IDefineTestCaseHookOptions { + name?: string tags?: string timeout?: number } diff --git a/test-d/hooks.ts b/test-d/hooks.ts index 3446f3bbe..7b582c2f4 100644 --- a/test-d/hooks.ts +++ b/test-d/hooks.ts @@ -23,6 +23,10 @@ After(function (param: ITestCaseHookParameter) {}) BeforeStep(function (param: ITestStepHookParameter) {}) AfterStep(function (param: ITestStepHookParameter) {}) +// should allow an object with tags and/or name in hooks +Before({ tags: '@foo', name: 'before hook' }, function () {}) +After({ tags: '@foo', name: 'after hook' }, function () {}) + // should allow us to return 'skipped' from a test case hook Before(async function () { return 'skipped'