diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc1700e..acb0c2ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. - Add support for skipped / pending scenario hooks, fixes [#1159](https://github.com/badeball/cypress-cucumber-preprocessor/issues/1159). +- Add support for suite-level test configuration, fixes [#1158](https://github.com/badeball/cypress-cucumber-preprocessor/issues/1158). + ## 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/features/suite_only_options.feature b/features/suite_only_options.feature new file mode 100644 index 00000000..9feb29b9 --- /dev/null +++ b/features/suite_only_options.feature @@ -0,0 +1,162 @@ +@cypress>=12 +Feature: suite only options + Scenario: Configuring testIsolation on a Feature + Given additional Cypress configuration + """ + { + "e2e": { + "testIsolation": true + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + @testIsolation(false) + Feature: a feature + Scenario: a scenario + Given a step + Scenario: another scenario + Then another step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given, Then } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", () => { + cy.get("body").invoke('html', 'Hello world') + }); + Given("another step", () => { + cy.contains("Hello world").should("exist"); + }); + """ + When I run cypress + Then it passes + + Scenario: Configuring testIsolation on a Rule + Given additional Cypress configuration + """ + { + "e2e": { + "testIsolation": true + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + @testIsolation(false) + Rule: a rule + Scenario: a scenario + Given a step + Scenario: another scenario + Then another step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given, Then } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", () => { + cy.get("body").invoke('html', 'Hello world') + }); + Given("another step", () => { + cy.contains("Hello world").should("exist"); + }); + """ + When I run cypress + Then it passes + + Scenario: Configuring testIsolation on a Scenario fails + Given additional Cypress configuration + """ + { + "e2e": { + "testIsolation": true + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + @testIsolation(false) + Scenario: a scenario + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given, Then } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", () => { + cy.get("body").invoke('html', 'Hello world') + }); + """ + When I run cypress + Then it fails + And the output should contain + """ + Tag @testIsolation(false) can only be used on a Feature or a Rule + """ + + Scenario: Configuring testIsolation on a Scenario Outline fails + Given additional Cypress configuration + """ + { + "e2e": { + "testIsolation": true + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + @testIsolation(false) + Scenario Outline: a scenario + Given a step + + Examples: + | foo | + | bar | + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given, Then } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", () => { + cy.get("body").invoke('html', 'Hello world') + }); + """ + When I run cypress + Then it fails + And the output should contain + """ + Tag @testIsolation(false) can only be used on a Feature or a Rule + """ + + Scenario: Configuring testIsolation on Examples fails + Given additional Cypress configuration + """ + { + "e2e": { + "testIsolation": true + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature + Scenario Outline: a scenario + Given a step + + @testIsolation(false) + Examples: + | foo | + | bar | + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given, Then } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", () => { + cy.get("body").invoke('html', 'Hello world') + }); + """ + When I run cypress + Then it fails + And the output should contain + """ + Tag @testIsolation(false) can only be used on a Feature or a Rule + """ diff --git a/features/support/helpers.ts b/features/support/helpers.ts index 07eee394..9a47253d 100644 --- a/features/support/helpers.ts +++ b/features/support/helpers.ts @@ -1,6 +1,7 @@ -import path from "path"; -import { promises as fs } from "fs"; import assert from "assert"; +import { version as cypressVersion } from "cypress/package.json"; +import { promises as fs } from "fs"; +import path from "path"; export async function writeFile(filePath: string, fileContent: string) { await fs.mkdir(path.dirname(filePath), { recursive: true }); @@ -106,3 +107,11 @@ export function stringToNdJson(content: string) { export function ndJsonToString(ndjson: any) { return ndjson.map((o: any) => JSON.stringify(o)).join("\n") + "\n"; } + +export function isPost12() { + return parseInt(cypressVersion.split(".")[0], 10) >= 12; +} + +export function isPre12() { + return !isPost12(); +} diff --git a/features/support/hooks.ts b/features/support/hooks.ts index f7286a7c..78343e06 100644 --- a/features/support/hooks.ts +++ b/features/support/hooks.ts @@ -1,8 +1,8 @@ import { After, Before, formatterHelpers } from "@cucumber/cucumber"; -import path from "path"; import assert from "assert"; import { promises as fs } from "fs"; -import { writeFile } from "./helpers"; +import path from "path"; +import { isPre12, writeFile } from "./helpers"; const projectPath = path.join(__dirname, "..", ".."); @@ -95,6 +95,12 @@ Before({ tags: "not @no-default-plugin" }, async function () { ); }); +Before({ tags: "@cypress>=12" }, async function () { + if (isPre12()) { + return "skipped"; + } +}); + After(function () { if ( this.lastRun != null && diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 85eed948..b6f72891 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -34,19 +34,20 @@ import { HOOK_FAILURE_EXPR, INTERNAL_SPEC_PROPERTIES, INTERNAL_SUITE_PROPERTIES, + TEST_ISOLATION_CONFIGURATION_OPTION, } from "./constants"; import { ITaskSpecEnvelopes, - ITaskTestCaseStarted, ITaskTestCaseFinished, - ITaskTestStepStarted, + ITaskTestCaseStarted, ITaskTestStepFinished, + ITaskTestStepStarted, TASK_SPEC_ENVELOPES, - TASK_TEST_CASE_STARTED, TASK_TEST_CASE_FINISHED, - TASK_TEST_STEP_STARTED, + TASK_TEST_CASE_STARTED, TASK_TEST_STEP_FINISHED, + TASK_TEST_STEP_STARTED, } from "./cypress-task-definitions"; import { notNull } from "./helpers/type-guards"; @@ -286,7 +287,17 @@ function createStepDescription({ } function createFeature(context: CompositionContext, feature: messages.Feature) { - describe(feature.name || "", () => { + const suiteOptions = collectTagNames(feature.tags) + .filter(looksLikeOptions) + .map(tagToCypressOptions) + .filter((tag) => { + return Object.keys(tag).every( + (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION + ); + }) + .reduce(Object.assign, {}); + + describe(feature.name || "", suiteOptions, () => { before(function () { beforeHandler.call(this, context); }); @@ -346,7 +357,17 @@ function createRule(context: CompositionContext, rule: messages.Rule) { } } - describe(rule.name || "", () => { + const suiteOptions = collectTagNames(rule.tags) + .filter(looksLikeOptions) + .map(tagToCypressOptions) + .filter((tag) => { + return Object.keys(tag).every( + (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION + ); + }) + .reduce(Object.assign, {}); + + describe(rule.name || "", suiteOptions, () => { if (rule.children) { for (const child of rule.children) { if (child.scenario) { @@ -420,9 +441,56 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { [INTERNAL_SPEC_PROPERTIES]: internalProperties, }; + const scenario = assertAndReturn( + context.astIdsMap.get( + assertAndReturn( + pickle.astNodeIds?.[0], + "Expected to find at least one astNodeId" + ) + ), + `Expected to find scenario associated with id = ${pickle.astNodeIds?.[0]}` + ); + + if ("tags" in scenario && "id" in scenario) { + const tagsDefinedOnThisScenarioTagNameAstIdMap = scenario.tags.reduce( + (acc, tag) => { + acc[tag.name] = tag.id; + return acc; + }, + {} as Record + ); + + if ("examples" in scenario) { + for (const example of scenario.examples) { + example.tags.forEach((tag) => { + tagsDefinedOnThisScenarioTagNameAstIdMap[tag.name] = tag.id; + }); + } + } + + for (const tag of pickle.tags) { + if ( + looksLikeOptions(tag.name) && + tagsDefinedOnThisScenarioTagNameAstIdMap[tag.name] === tag.astNodeId && + Object.keys(tagToCypressOptions(tag.name)).every( + (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION + ) + ) { + throw new Error( + `Tag ${tag.name} can only be used on a Feature or a Rule` + ); + } + } + } + const suiteOptions = tags .filter(looksLikeOptions) .map(tagToCypressOptions) + .filter((tag) => + Object.keys(tag).every( + (key) => key !== TEST_ISOLATION_CONFIGURATION_OPTION + ) + ) .reduce(Object.assign, {}); if (suiteOptions.env) { diff --git a/lib/constants.ts b/lib/constants.ts index e6beb50c..08a71495 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -7,3 +7,5 @@ export const INTERNAL_SUITE_PROPERTIES = INTERNAL_PROPERTY_NAME + "_suite"; export const HOOK_FAILURE_EXPR = /Because this error occurred during a `[^`]+` hook we are skipping all of the remaining tests\./; + +export const TEST_ISOLATION_CONFIGURATION_OPTION = "testIsolation";