diff --git a/CHANGELOG.md b/CHANGELOG.md index c45d44ca..2dc1700e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## Unreleased + +- Add support for skipped / pending scenario hooks, fixes [#1159](https://github.com/badeball/cypress-cucumber-preprocessor/issues/1159). + ## v20.0.1 - Handle more corner cases related to reload-behavior, fixes [#1142](https://github.com/badeball/cypress-cucumber-preprocessor/issues/1142). diff --git a/docs/cucumber-basics.md b/docs/cucumber-basics.md index 3996bb02..9692151f 100644 --- a/docs/cucumber-basics.md +++ b/docs/cucumber-basics.md @@ -12,6 +12,7 @@ - [Hooks](#hooks) - [Run hooks](#run-hooks) - [Scenario hooks](#scenario-hooks) + - [Pending / skipped scenario hooks](#pending--skipped-scenario-hooks) - [Step hooks](#step-hooks) - [Hook ordering](#hook-ordering) - [Named hooks](#named-hooks) @@ -188,6 +189,10 @@ Before(function ({ pickle, gherkinDocument, testCaseStartedId }) { }); ``` +### Pending / skipped scenario hooks + +Scenario hooks can be made pending or skipped similarly to steps, as explained above, by returning `"pending"` or `"skipped"`, respectively. Both will halt the execution and Cypress will report the test as "skipped". + ## Step hooks `BeforeStep()` and `AfterStep()` are hooks invoked before and after each step, respectively. These too can be selected to conditionally run based on the tags of each scenario, as shown below. diff --git a/features/pending_scenario_hooks.feature b/features/pending_scenario_hooks.feature new file mode 100644 index 00000000..024e1511 --- /dev/null +++ b/features/pending_scenario_hooks.feature @@ -0,0 +1,39 @@ +Feature: pending scenario hooks + + Scenario: pending Before() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + Scenario: a scenario + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, Given } = require("@badeball/cypress-cucumber-preprocessor") + Before(() => { + return "pending" + }) + Given("a step", function() {}) + """ + When I run cypress + Then it passes + And it should appear to have skipped the scenario "a scenario" + + Scenario: pending After() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + Scenario: a scenario + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { After, Given } = require("@badeball/cypress-cucumber-preprocessor") + After(() => { + return "pending" + }) + Given("a step", function() {}) + """ + When I run cypress + Then it passes + And it should appear to have skipped the scenario "a scenario" diff --git a/features/skipped_scenario_hooks.feature b/features/skipped_scenario_hooks.feature new file mode 100644 index 00000000..11aa32ae --- /dev/null +++ b/features/skipped_scenario_hooks.feature @@ -0,0 +1,39 @@ +Feature: skipped scenario hooks + + Scenario: skipped Before() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + Scenario: a scenario + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, Given } = require("@badeball/cypress-cucumber-preprocessor") + Before(() => { + return "skipped" + }) + Given("a step", function() {}) + """ + When I run cypress + Then it passes + And it should appear to have skipped the scenario "a scenario" + + Scenario: skipped After() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + Scenario: a scenario + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { After, Given } = require("@badeball/cypress-cucumber-preprocessor") + After(() => { + return "skipped" + }) + Given("a step", function() {}) + """ + When I run cypress + Then it passes + And it should appear to have skipped the scenario "a scenario" diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index d900def9..4addadb2 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -448,6 +448,92 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { pickle, }; + const onAfterStep = (options: { + testStepId: string; + start: messages.Timestamp; + result: any; + }) => { + const { testStepId, start, result } = options; + + const end = createTimestamp(); + + if (result === "pending" || result === "skipped") { + if (result === "pending") { + taskTestStepFinished(context, { + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.PENDING, + duration: duration(start, end), + }, + timestamp: end, + }); + } else { + taskTestStepFinished(context, { + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.SKIPPED, + duration: duration(start, end), + }, + timestamp: end, + }); + } + + remainingSteps.shift(); + + for (const skippedStep of remainingSteps) { + const hookIdOrPickleStepId = assertAndReturn( + skippedStep.hook?.id ?? skippedStep.pickleStep?.id, + "Expected a step to either be a hook or a pickleStep" + ); + + const testStepId = getTestStepId({ + context, + pickleId: pickle.id, + hookIdOrPickleStepId, + }); + + taskTestStepStarted(context, { + testStepId, + testCaseStartedId, + timestamp: createTimestamp(), + }); + + taskTestStepFinished(context, { + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.SKIPPED, + duration: { + seconds: 0, + nanos: 0, + }, + }, + timestamp: createTimestamp(), + }); + } + + for (let i = 0, count = remainingSteps.length; i < count; i++) { + remainingSteps.pop(); + } + + cy.then(() => this.skip()); + } else { + taskTestStepFinished(context, { + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.PASSED, + duration: duration(start, end), + }, + timestamp: end, + }); + + remainingSteps.shift(); + } + }; + for (const step of steps) { if (step.hook) { const hook = step.hook; @@ -480,29 +566,17 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { testCaseStartedId, }; - runStepWithLogGroup({ + return runStepWithLogGroup({ fn: () => registry.runCaseHook(this, hook, options), keyword: hook.keyword, text: createStepDescription(hook), + }).then((result) => { + return { start, result }; }); - - return cy.wrap(start, { log: false }); }) - .then((start) => { - const end = createTimestamp(); - - taskTestStepFinished(context, { - testStepId, - testCaseStartedId, - testStepResult: { - status: messages.TestStepResultStatus.PASSED, - duration: duration(start, end), - }, - timestamp: end, - }); - - remainingSteps.shift(); - }); + .then(({ start, result }) => + onAfterStep({ start, result, testStepId }) + ); } else if (step.pickleStep) { const pickleStep = step.pickleStep; @@ -616,85 +690,9 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { } }); }) - .then(({ start, result }) => { - const end = createTimestamp(); - - if (result === "pending" || result === "skipped") { - if (result === "pending") { - taskTestStepFinished(context, { - testStepId, - testCaseStartedId, - testStepResult: { - status: messages.TestStepResultStatus.PENDING, - duration: duration(start, end), - }, - timestamp: end, - }); - } else { - taskTestStepFinished(context, { - testStepId, - testCaseStartedId, - testStepResult: { - status: messages.TestStepResultStatus.SKIPPED, - duration: duration(start, end), - }, - timestamp: end, - }); - } - - remainingSteps.shift(); - - for (const skippedStep of remainingSteps) { - const hookIdOrPickleStepId = assertAndReturn( - skippedStep.hook?.id ?? skippedStep.pickleStep?.id, - "Expected a step to either be a hook or a pickleStep" - ); - - const testStepId = getTestStepId({ - context, - pickleId: pickle.id, - hookIdOrPickleStepId, - }); - - taskTestStepStarted(context, { - testStepId, - testCaseStartedId, - timestamp: createTimestamp(), - }); - - taskTestStepFinished(context, { - testStepId, - testCaseStartedId, - testStepResult: { - status: messages.TestStepResultStatus.SKIPPED, - duration: { - seconds: 0, - nanos: 0, - }, - }, - timestamp: createTimestamp(), - }); - } - - for (let i = 0, count = remainingSteps.length; i < count; i++) { - remainingSteps.pop(); - } - - cy.then(() => this.skip()); - } else { - taskTestStepFinished(context, { - testStepId, - testCaseStartedId, - testStepResult: { - status: messages.TestStepResultStatus.PASSED, - duration: duration(start, end), - }, - timestamp: end, - }); - - remainingSteps.shift(); - } - }); + .then(({ start, result }) => + onAfterStep({ start, result, testStepId }) + ); } } });