From f83f338b8b4a54faecb9062312f65dbe3ccda3b3 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Mon, 19 Feb 2024 15:12:36 +0000 Subject: [PATCH 01/12] Apply Feature configuration tags to suite --- .gitignore | 1 + features/suite_only_options.feature | 31 +++++++++++++++++++++++++++++ lib/browser-runtime.ts | 26 +++++++++++++++++++----- lib/constants.ts | 2 ++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 features/suite_only_options.feature diff --git a/.gitignore b/.gitignore index bd55b46a..c3c9e730 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ lib/version.ts # Temporary directory for test execution tmp/ +node_modules diff --git a/features/suite_only_options.feature b/features/suite_only_options.feature new file mode 100644 index 00000000..e1725cc8 --- /dev/null +++ b/features/suite_only_options.feature @@ -0,0 +1,31 @@ +Feature: suite only options + Scenario: suite specific test isolation + 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 diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 3bf2a4d6..422cbf90 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, + SUITE_CONFIGURATION_OPTIONS, } 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) => + SUITE_CONFIGURATION_OPTIONS.includes(key) + ); + }) + .reduce(Object.assign, {}); + + describe(feature.name || "", suiteOptions, () => { before(function () { beforeHandler.call(this, context); }); @@ -423,6 +434,11 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { const suiteOptions = tags .filter(looksLikeOptions) .map(tagToCypressOptions) + .filter((tag) => + Object.keys(tag).every( + (key) => !SUITE_CONFIGURATION_OPTIONS.includes(key) + ) + ) .reduce(Object.assign, {}); if (suiteOptions.env) { diff --git a/lib/constants.ts b/lib/constants.ts index e6beb50c..cd6fcadb 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 SUITE_CONFIGURATION_OPTIONS = ["testIsolation"]; From ffc224618ee32db2d782d68a00919d8d326dde91 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 22 Feb 2024 09:34:16 +0000 Subject: [PATCH 02/12] Remove node_modules from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index c3c9e730..bd55b46a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ lib/version.ts # Temporary directory for test execution tmp/ -node_modules From 0f1b16c07bb1c0881c78bce6003c60d7dd35af99 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 22 Feb 2024 09:48:36 +0000 Subject: [PATCH 03/12] Only run testIsolation tag test in Cypress >= 12 --- features/suite_only_options.feature | 1 + features/support/helpers.ts | 13 +++++++++++-- features/support/hooks.ts | 10 ++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/features/suite_only_options.feature b/features/suite_only_options.feature index e1725cc8..9e481444 100644 --- a/features/suite_only_options.feature +++ b/features/suite_only_options.feature @@ -1,3 +1,4 @@ +@cypress>=12 Feature: suite only options Scenario: suite specific test isolation Given additional Cypress configuration diff --git a/features/support/helpers.ts b/features/support/helpers.ts index 07eee394..13f3cbdf 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 && From a0230123b0ddf417751da10c8a149d8a070413f3 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 22 Feb 2024 13:35:06 +0000 Subject: [PATCH 04/12] There is only one suite-level option --- lib/browser-runtime.ts | 8 ++++---- lib/constants.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 422cbf90..9ea571dd 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -34,7 +34,7 @@ import { HOOK_FAILURE_EXPR, INTERNAL_SPEC_PROPERTIES, INTERNAL_SUITE_PROPERTIES, - SUITE_CONFIGURATION_OPTIONS, + TEST_ISOLATION_CONFIGURATION_OPTION, } from "./constants"; import { @@ -291,8 +291,8 @@ function createFeature(context: CompositionContext, feature: messages.Feature) { .filter(looksLikeOptions) .map(tagToCypressOptions) .filter((tag) => { - return Object.keys(tag).every((key) => - SUITE_CONFIGURATION_OPTIONS.includes(key) + return Object.keys(tag).every( + (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION ); }) .reduce(Object.assign, {}); @@ -436,7 +436,7 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { .map(tagToCypressOptions) .filter((tag) => Object.keys(tag).every( - (key) => !SUITE_CONFIGURATION_OPTIONS.includes(key) + (key) => key !== TEST_ISOLATION_CONFIGURATION_OPTION ) ) .reduce(Object.assign, {}); diff --git a/lib/constants.ts b/lib/constants.ts index cd6fcadb..08a71495 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -8,4 +8,4 @@ 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 SUITE_CONFIGURATION_OPTIONS = ["testIsolation"]; +export const TEST_ISOLATION_CONFIGURATION_OPTION = "testIsolation"; From cfe822c70a5a9d0a5c68a231a129283dce40cdae Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 22 Feb 2024 13:50:23 +0000 Subject: [PATCH 05/12] Support testIsolation tag on Rule --- features/suite_only_options.feature | 34 ++++++++++++++++++++++++++++- lib/browser-runtime.ts | 12 +++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/features/suite_only_options.feature b/features/suite_only_options.feature index 9e481444..16f79d69 100644 --- a/features/suite_only_options.feature +++ b/features/suite_only_options.feature @@ -1,6 +1,6 @@ @cypress>=12 Feature: suite only options - Scenario: suite specific test isolation + Scenario: Configuring testIsolation on a Feature Given additional Cypress configuration """ { @@ -30,3 +30,35 @@ Feature: suite only options """ 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 diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 9ea571dd..a8ac1e2c 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -357,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) { From 1569aedccac63445aabcbc1c5b3fb4bd99a69427 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 22 Feb 2024 13:53:17 +0000 Subject: [PATCH 06/12] Prettier --- features/support/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/support/helpers.ts b/features/support/helpers.ts index 13f3cbdf..9a47253d 100644 --- a/features/support/helpers.ts +++ b/features/support/helpers.ts @@ -114,4 +114,4 @@ export function isPost12() { export function isPre12() { return !isPost12(); -} +} From a6746ed9790d49234ee8a86ef8f173f745caaf07 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Fri, 23 Feb 2024 15:06:11 +0000 Subject: [PATCH 07/12] Throw an error if testIsolation used on a scenario --- features/suite_only_options.feature | 42 ++++++++++++++++++++++++----- lib/browser-runtime.ts | 18 +++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/features/suite_only_options.feature b/features/suite_only_options.feature index 16f79d69..840ef350 100644 --- a/features/suite_only_options.feature +++ b/features/suite_only_options.feature @@ -52,13 +52,43 @@ Feature: suite only options """ 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"); + 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 + """ diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index a8ac1e2c..33037560 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -441,6 +441,24 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { [INTERNAL_SPEC_PROPERTIES]: internalProperties, }; + pickle.tags.forEach((pickleTag) => { + for (const node of traverseGherkinDocument(gherkinDocument)) { + if ("tags" in node) { + for (const tag of node.tags) { + if ( + tag.id === pickleTag.astNodeId && + "id" in node && + node.id === pickle.astNodeIds[0] + ) { + throw new Error( + `Tag ${tag.name} can only be used on a Feature or a Rule` + ); + } + } + } + } + }); + const suiteOptions = tags .filter(looksLikeOptions) .map(tagToCypressOptions) From e0436a27d89188b39294f9523cf4f84dfe0c14b9 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Fri, 23 Feb 2024 15:59:31 +0000 Subject: [PATCH 08/12] Only throw error for testIsolation tag! --- lib/browser-runtime.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 33037560..5369dae0 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -446,6 +446,10 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { if ("tags" in node) { for (const tag of node.tags) { if ( + looksLikeOptions(tag.name) && + Object.keys(tagToCypressOptions(tag.name)).every( + (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION + ) && tag.id === pickleTag.astNodeId && "id" in node && node.id === pickle.astNodeIds[0] From 5cde757ceda36790cb51d5e7de7bc785267bf317 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Fri, 23 Feb 2024 16:22:14 +0000 Subject: [PATCH 09/12] Use astIdsMap to check testIsolation on Scenario --- lib/browser-runtime.ts | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 5369dae0..013fc3f3 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -441,27 +441,31 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { [INTERNAL_SPEC_PROPERTIES]: internalProperties, }; - pickle.tags.forEach((pickleTag) => { - for (const node of traverseGherkinDocument(gherkinDocument)) { - if ("tags" in node) { - for (const tag of node.tags) { - if ( - looksLikeOptions(tag.name) && - Object.keys(tagToCypressOptions(tag.name)).every( - (key) => key === TEST_ISOLATION_CONFIGURATION_OPTION - ) && - tag.id === pickleTag.astNodeId && - "id" in node && - node.id === pickle.astNodeIds[0] - ) { - throw new Error( - `Tag ${tag.name} can only be used on a Feature or a Rule` - ); - } - } + + 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) { + for (const tag of scenario.tags) { + if ( + looksLikeOptions(tag.name) && + 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) From 99e9df3a8f291a549938288cadc99367e99fd694 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Fri, 23 Feb 2024 16:47:37 +0000 Subject: [PATCH 10/12] fmt --- lib/browser-runtime.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 013fc3f3..4f672b61 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -441,7 +441,6 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { [INTERNAL_SPEC_PROPERTIES]: internalProperties, }; - const scenario = assertAndReturn( context.astIdsMap.get( assertAndReturn( @@ -452,7 +451,7 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { `Expected to find scenario associated with id = ${pickle.astNodeIds?.[0]}` ); - if ('tags' in scenario) { + if ("tags" in scenario) { for (const tag of scenario.tags) { if ( looksLikeOptions(tag.name) && From e8893bffe9df2e7beef9d924214095c6c73e0182 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 29 Feb 2024 12:29:12 +0000 Subject: [PATCH 11/12] Check for testIsolation on Examples --- features/suite_only_options.feature | 68 +++++++++++++++++++++++++++++ lib/browser-runtime.ts | 18 +++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/features/suite_only_options.feature b/features/suite_only_options.feature index 840ef350..9feb29b9 100644 --- a/features/suite_only_options.feature +++ b/features/suite_only_options.feature @@ -92,3 +92,71 @@ Feature: suite only options """ 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/lib/browser-runtime.ts b/lib/browser-runtime.ts index 4f672b61..150fd07a 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -451,10 +451,24 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { `Expected to find scenario associated with id = ${pickle.astNodeIds?.[0]}` ); - if ("tags" in scenario) { - for (const tag of scenario.tags) { + 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 ) From 3e3b311ea960516a20f34cf5f7dda3fe68889f17 Mon Sep 17 00:00:00 2001 From: Mark Allison Date: Thu, 29 Feb 2024 12:34:35 +0000 Subject: [PATCH 12/12] FMT! --- lib/browser-runtime.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 150fd07a..313d7f63 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -451,13 +451,16 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { `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 ("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) { + if ("examples" in scenario) { for (const example of scenario.examples) { example.tags.forEach((tag) => { tagsDefinedOnThisScenarioTagNameAstIdMap[tag.name] = tag.id;