From bcedb7771abd25767636190db9c4eb7d8517db6f Mon Sep 17 00:00:00 2001 From: Jonas Amundsen Date: Tue, 24 Sep 2024 11:03:40 +0200 Subject: [PATCH] Replace diagnostics with a more proper dry-run This is "more proper" in the sense that it's still executing tests an actual Cypress environments, while still being reasonably quick. Having a postinstall not run upon installation turned out to be less trivial than I had hoped for, however SO came to the rescue [1]. This is related to #1120 [2]. This closes #1129 [3]. [1] https://stackoverflow.com/q/54212147/4098802 [2] https://github.com/badeball/cypress-cucumber-preprocessor/issues/1120 [3] https://github.com/badeball/cypress-cucumber-preprocessor/issues/1129 --- CHANGELOG.md | 4 +- augmentations.d.ts | 6 + docs/configuration.md | 3 + docs/diagnostics.md | 39 -- docs/dry-run.md | 15 + docs/readme.md | 4 +- docs/source-maps.md | 50 +++ docs/usage-report.md | 45 ++ examples/esbuild-cjs/package.json | 3 +- examples/esbuild-esm/package.json | 3 +- examples/esbuild-ts/package.json | 1 - features/diagnostics.feature | 291 ------------- features/dry_run.feature | 253 ++++++++++++ features/experimental_source_map.feature | 216 ++++++++++ .../fixtures/another-passed-example.ndjson | 2 +- .../fixtures/attachments/screenshot.ndjson | 2 +- .../fixtures/experimental-source-map.json | 53 +++ features/fixtures/failing-after.ndjson | 4 +- features/fixtures/failing-before.ndjson | 4 +- features/fixtures/failing-step.ndjson | 6 +- features/fixtures/multiple-features.ndjson | 4 +- .../multiple-scenarios-reloaded.ndjson | 4 +- features/fixtures/passed-example.ndjson | 2 +- features/fixtures/passed-outline.ndjson | 2 +- features/fixtures/pending-steps.ndjson | 6 +- features/fixtures/rescued-error.ndjson | 2 +- features/fixtures/retried.ndjson | 2 +- .../fixtures/skipped-all-scenarios.ndjson | 2 +- .../fixtures/skipped-first-scenario.ndjson | 2 +- .../fixtures/skipped-second-scenario.ndjson | 2 +- features/fixtures/skipped-steps.ndjson | 6 +- .../fixtures/skipped-third-scenario.ndjson | 2 +- features/fixtures/undefined-steps.ndjson | 4 +- features/issues/736.feature | 4 + features/reporters/usage.feature | 196 +++++++++ features/step_definitions/cli_steps.ts | 18 +- features/step_definitions/usage_steps.ts | 33 ++ features/support/ICustomWorld.ts | 2 - features/support/helpers.ts | 10 + features/support/world.ts | 16 - lib/add-cucumber-preprocessor-plugin.ts | 4 + lib/bin/diagnostics.ts | 10 - lib/browser-runtime.ts | 80 ++-- lib/diagnostics/diagnose.ts | 372 ----------------- lib/diagnostics/index.ts | 384 ------------------ lib/entrypoint-browser.ts | 2 +- lib/helpers/dry-run.ts | 14 + lib/helpers/formatters.ts | 57 ++- lib/helpers/messages.ts | 66 +++ lib/helpers/prepare-registry.ts | 2 +- lib/helpers/source-map.ts | 105 ++++- lib/plugin-event-handlers.ts | 43 +- lib/preprocessor-configuration.test.ts | 19 + lib/preprocessor-configuration.ts | 88 ++++ lib/registry.ts | 46 ++- lib/subpath-entrypoints/browserify.ts | 7 +- lib/subpath-entrypoints/esbuild.ts | 16 +- lib/subpath-entrypoints/rollup.ts | 2 +- lib/subpath-entrypoints/webpack.ts | 4 +- lib/template.ts | 7 + package.json | 15 +- patches/@cucumber+cucumber+10.9.0.patch | 25 ++ 62 files changed, 1449 insertions(+), 1242 deletions(-) delete mode 100644 docs/diagnostics.md create mode 100644 docs/dry-run.md create mode 100644 docs/source-maps.md create mode 100644 docs/usage-report.md delete mode 100644 features/diagnostics.feature create mode 100644 features/dry_run.feature create mode 100644 features/experimental_source_map.feature create mode 100644 features/fixtures/experimental-source-map.json create mode 100644 features/reporters/usage.feature create mode 100644 features/step_definitions/usage_steps.ts delete mode 100644 lib/bin/diagnostics.ts delete mode 100644 lib/diagnostics/diagnose.ts delete mode 100755 lib/diagnostics/index.ts create mode 100644 lib/helpers/dry-run.ts create mode 100644 patches/@cucumber+cucumber+10.9.0.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 06716025..0eb7b182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,7 @@ Breaking changes: - User of `@badeball/cypress-cucumber-preprocessor/browserify` should change their Cypress config in accordance with the related [examples](examples). -- The executable `cypress-cucumber-diagnostics` no longer respect flags such as `--project` or `--env`. The long-term plan is to rewamp dry run altogether, and run it in a Cypress environment. - -- `esbuild` is now an optional peer dependency. This is relevant for users using `esbuild` as their bundler, as well as users of `cypress-cucumber-diagnostics`. +- The executable `cypress-cucumber-diagnostics` has been replaced by a [`dryRun` option](docs/dry-run.md). Other changees: diff --git a/augmentations.d.ts b/augmentations.d.ts index a7be2414..e847ab64 100644 --- a/augmentations.d.ts +++ b/augmentations.d.ts @@ -2,6 +2,8 @@ import messages from "@cucumber/messages"; import { Registry } from "./lib/registry"; +import { MochaGlobals } from "mocha"; + declare module "@cucumber/cucumber" { interface IWorld { tmpDir: string; @@ -25,6 +27,10 @@ declare global { var __cypress_cucumber_preprocessor_registry_dont_use_this: | Registry | undefined; + + var __cypress_cucumber_preprocessor_mocha_dont_use_this: + | Pick + | undefined; } interface Window { diff --git a/docs/configuration.md b/docs/configuration.md index 663b42ad..5330de2d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -80,10 +80,13 @@ Every configuration option has a similar key which can be use to override it, sh | `json.output` | `jsonOutput` | `cucumber-report.json` | | `html.enabled` | `htmlEnabled` | `true`, `false` | | `html.output` | `htmlOutput` | `cucumber-report.html` | +| `usage.enabled` | `usageEnabled` | `true`, `false` | +| `usage.output` | `usageOutput` | `stdout` | | `pretty.enabled` | `prettyEnabled` | `true`, `false` | | `filterSpecsMixedMode` | `filterSpecsMixedMode` | `hide`, `show`, `empty-set` | | `filterSpecs` | `filterSpecs` | `true`, `false` | | `omitFiltered` | `omitFiltered` | `true`, `false` | +| `dryRun` | `dryRun` | `true`, `false` | ## Test configuration diff --git a/docs/diagnostics.md b/docs/diagnostics.md deleted file mode 100644 index 451afa96..00000000 --- a/docs/diagnostics.md +++ /dev/null @@ -1,39 +0,0 @@ -[← Back to documentation](readme.md) - -# Diagnostics / dry run - -A diagnostics utility is provided to verify that each step matches one, and only one, step definition. This can be run as shown below. - -``` -$ npx cypress-cucumber-diagnostics -``` - -This requires `esbuild`, which is an _optional peer dependency_ of this library. - -``` -$ npm install esbuild -``` - -The observant user might notice that some transitive dependencies will install `esbuild` for you, making the above-mentioned command unnecessary. However, this is not guaranteed and users should install it explicitly to protect themselves from future changes to the transitive dependency chain. - -## Limitations - -In order to obtain structured information about step definitions, these files are resolved and evaluated in a Node environment. This environment differs from the normal Cypress environment in that it's not a browser environment and Cypress globals are mocked and imitated to some degree. - -This means that expressions such as that shown below will work. - -```ts -import { Given } from "@badeball/cypress-cucumber-preprocessor"; - -const foo = Cypress.env("foo"); - -Given("a step", () => { - if (foo) { - // ... - } -}); -``` - -However, other may not. Cypress globals are mocked on a best-effort and need-to-have basis. If you're code doesn't run correctly during diagnostics, you may open up an issue on the tracker. - -Furthermore, this requires that `cypress.config.*` is placed in the root directory of your project. diff --git a/docs/dry-run.md b/docs/dry-run.md new file mode 100644 index 00000000..335bfd2c --- /dev/null +++ b/docs/dry-run.md @@ -0,0 +1,15 @@ +[← Back to documentation](readme.md) + +# Dry run + +Dry run is a run mode in which no steps or any type of hooks are executed. A few examples where this is useful: + +- Finding unused step definitions with [usage reports](usage-report.md) +- Generating snippets for all undefined steps +- Checking if your path, tag expression, etc. matches the scenarios you expect it to + +Dry run can be enabled using `dryRun`, like seen below. + +``` +$ cypress run --env dryRun=true +``` diff --git a/docs/readme.md b/docs/readme.md index 98bd870a..e5034e53 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -7,15 +7,17 @@ * [Step definitions](step-definitions.md) * [Tags](tags.md) * [Pretty output](pretty-output.md) +* [Source maps](source-maps.md) +* [Dry run](dry-run.md) * Reports * [Messages report](messages-report.md) * [JSON report](json-report.md) * [HTML report](html-report.md) + * [Usage report](usage-report.md) * [Localisation](localisation.md) * [Configuration](configuration.md) * [Test configuration](test-configuration.md) * CLI utilities - * [Diagnostics / dry run](diagnostics.md) * [JSON formatter](json-formatter.md) * [HTML formatter](html-formatter.md) * [Parallelization using Cypress Cloud & merging reports](merging-reports.md) diff --git a/docs/source-maps.md b/docs/source-maps.md new file mode 100644 index 00000000..d3b7e2c7 --- /dev/null +++ b/docs/source-maps.md @@ -0,0 +1,50 @@ +[← Back to documentation](readme.md) + +# Source maps + +How to enable source maps for each bundler is shown below. + +## esbuild + +```js +const { defineConfig } = require("cypress"); +const createBundler = require("@bahmutov/cypress-esbuild-preprocessor"); +const { + addCucumberPreprocessorPlugin, +} = require("@badeball/cypress-cucumber-preprocessor"); +const { + createEsbuildPlugin, +} = require("@badeball/cypress-cucumber-preprocessor/esbuild"); + +async function setupNodeEvents(on, config) { + // This is required for the preprocessor to be able to generate JSON reports after each run, and more, + await addCucumberPreprocessorPlugin(on, config); + + on( + "file:preprocessor", + createBundler({ + plugins: [createEsbuildPlugin(config)], + sourcemap: "inline" + }) + ); + + // Make sure to return the config object as it might have been modified by the plugin. + return config; +} + +module.exports = defineConfig({ + e2e: { + baseUrl: "https://duckduckgo.com", + specPattern: "**/*.feature", + setupNodeEvents, + }, +}); +``` + +## Webpack + +Source maps are enabled by default. + +## Browserify + +Source maps are enabled by default. diff --git a/docs/usage-report.md b/docs/usage-report.md new file mode 100644 index 00000000..16f98d1c --- /dev/null +++ b/docs/usage-report.md @@ -0,0 +1,45 @@ +[← Back to documentation](readme.md) + +# Usage reports + +> :warning: This requires you to have [source maps](source-maps.md) enabled. + +The usage report lists your step definitions and tells you about usages in your scenarios, including the duration of each usage, and any unused steps. Here's an example of the output: + +``` +┌───────────────────────────────────────┬──────────┬─────────────────────────────────┐ +│ Pattern / Text │ Duration │ Location │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ an empty todo list │ 760.33ms │ support/steps/steps.ts:6 │ +│ an empty todo list │ 820ms │ features/empty.feature:4 │ +│ an empty todo list │ 761ms │ features/adding-todos.feature:4 │ +│ an empty todo list │ 700ms │ features/empty.feature:4 │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ I add the todo {string} │ 432.00ms │ support/steps/steps.ts:10 │ +│ I add the todo "buy some cheese" │ 432ms │ features/adding-todos.feature:5 │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ my cursor is ready to create a todo │ 53.00ms │ support/steps/steps.ts:27 │ +│ my cursor is ready to create a todo │ 101ms │ features/empty.feature:10 │ +│ my cursor is ready to create a todo │ 5ms │ features/adding-todos.feature:8 │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ no todos are listed │ 46.00ms │ support/steps/steps.ts:15 │ +│ no todos are listed │ 46ms │ features/empty.feature:7 │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ the todos are: │ 31.00ms │ support/steps/steps.ts:21 │ +│ the todos are: │ 31ms │ features/adding-todos.feature:6 │ +├───────────────────────────────────────┼──────────┼─────────────────────────────────┤ +│ I remove the todo {string} │ UNUSED │ support/steps/steps.ts:33 │ +└───────────────────────────────────────┴──────────┴─────────────────────────────────┘ +``` + +Usage reports can be enabled using the `usage.enabled` property. The preprocessor uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig), which means you can place configuration options in EG. `.cypress-cucumber-preprocessorrc.json` or `package.json`. An example configuration is shown below. + +```json +{ + "usage": { + "enabled": true + } +} +``` + +The report is outputted to stdout (your console) by default, but can be configured to be written to a file through the `usage.output` property. diff --git a/examples/esbuild-cjs/package.json b/examples/esbuild-cjs/package.json index c008dafa..49c5c7f9 100644 --- a/examples/esbuild-cjs/package.json +++ b/examples/esbuild-cjs/package.json @@ -2,7 +2,6 @@ "dependencies": { "@badeball/cypress-cucumber-preprocessor": "latest", "@bahmutov/cypress-esbuild-preprocessor": "latest", - "cypress": "latest", - "esbuild": "latest" + "cypress": "latest" } } diff --git a/examples/esbuild-esm/package.json b/examples/esbuild-esm/package.json index c008dafa..49c5c7f9 100644 --- a/examples/esbuild-esm/package.json +++ b/examples/esbuild-esm/package.json @@ -2,7 +2,6 @@ "dependencies": { "@badeball/cypress-cucumber-preprocessor": "latest", "@bahmutov/cypress-esbuild-preprocessor": "latest", - "cypress": "latest", - "esbuild": "latest" + "cypress": "latest" } } diff --git a/examples/esbuild-ts/package.json b/examples/esbuild-ts/package.json index b84a61b9..9a254d85 100644 --- a/examples/esbuild-ts/package.json +++ b/examples/esbuild-ts/package.json @@ -3,7 +3,6 @@ "@badeball/cypress-cucumber-preprocessor": "latest", "@bahmutov/cypress-esbuild-preprocessor": "latest", "cypress": "latest", - "esbuild": "latest", "typescript": "latest" } } diff --git a/features/diagnostics.feature b/features/diagnostics.feature deleted file mode 100644 index 33472e33..00000000 --- a/features/diagnostics.feature +++ /dev/null @@ -1,291 +0,0 @@ -Feature: diagnostics - Rule: usage should be grouped by step definition - Scenario: one definition - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:2 │ - │ a step │ cypress/e2e/a.feature:3 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Scenario: one definition, repeated - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - And a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:2 │ - │ a step │ cypress/e2e/a.feature:3 │ - │ a step │ cypress/e2e/a.feature:4 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Scenario: two definitions - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - And another step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Given("a step", function() {}); - Given("another step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:2 │ - │ a step │ cypress/e2e/a.feature:3 │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'another step' │ cypress/support/step_definitions/steps.js:3 │ - │ another step │ cypress/e2e/a.feature:4 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Rule: it should report any problem - Scenario: no step definition files - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - When I run diagnostics - Then it fails - And the output should contain - """ - Found 1 problem(s): - - 1) Error: Step implementation missing at cypress/e2e/a.feature:3 - - a step - - We tried searching for files containing step definitions using the following search pattern template(s): - - - cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx} - - cypress/e2e/[filepath].{js,mjs,ts,tsx} - - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx} - - These templates resolved to the following search pattern(s): - - - cypress/e2e/a/**/*.{js,mjs,ts,tsx} - - cypress/e2e/a.{js,mjs,ts,tsx} - - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx} - - These patterns matched *no files* containing step definitions. This almost certainly means that you have misconfigured `stepDefinitions`. Alternatively, you can implement it using the suggestion(s) below. - - Given("a step", function () { - return "pending"; - }); - """ - - Scenario: step defintions, but none matching - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Given("another step", function() {}); - """ - When I run diagnostics - Then it fails - And the output should contain - """ - Found 1 problem(s): - - 1) Error: Step implementation missing at cypress/e2e/a.feature:3 - - a step - - We tried searching for files containing step definitions using the following search pattern template(s): - - - cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx} - - cypress/e2e/[filepath].{js,mjs,ts,tsx} - - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx} - - These templates resolved to the following search pattern(s): - - - cypress/e2e/a/**/*.{js,mjs,ts,tsx} - - cypress/e2e/a.{js,mjs,ts,tsx} - - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx} - - These patterns matched the following file(s): - - - cypress/support/step_definitions/steps.js - - However, none of these files contained a matching step definition. You can implement it using the suggestion(s) below. - - Given("a step", function () { - return "pending"; - }); - """ - - Scenario: ambiguous step - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Given("a step", function() {}); - Given(/a step/, function() {}); - """ - When I run diagnostics - Then it fails - And the output should contain - """ - Found 1 problem(s): - - 1) Error: Multiple matching step definitions at cypress/e2e/a.feature:3 for - - a step - - Step matched the following definitions: - - - 'a step' (cypress/support/step_definitions/steps.js:2) - - /a step/ (cypress/support/step_definitions/steps.js:3) - """ - - Scenario: module package - Given a file named "package.json" with: - """ - { - "type": "module" - } - """ - And a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - import { Given } from "@badeball/cypress-cucumber-preprocessor"; - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:2 │ - │ a step │ cypress/e2e/a.feature:3 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Rule: it should works despite accessing a variety of globals on root-level - - Scenario: Cypress.env - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - const foo = Cypress.env("foo"); - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:3 │ - │ a step │ cypress/e2e/a.feature:3 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Scenario: Cypress.on - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - Cypress.on("uncaught:exception", () => {}); - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:3 │ - │ a step │ cypress/e2e/a.feature:3 │ - └────────────────┴─────────────────────────────────────────────┘ - """ - - Scenario: Cypress.config - Given a file named "cypress/e2e/a.feature" with: - """ - Feature: a feature name - Scenario: a scenario name - Given a step - """ - And a file named "cypress/support/step_definitions/steps.js" with: - """ - const { Given } = require("@badeball/cypress-cucumber-preprocessor"); - const foo = Cypress.config("foo"); - Given("a step", function() {}); - """ - When I run diagnostics - Then the output should contain - """ - ┌────────────────┬─────────────────────────────────────────────┐ - │ Pattern / Text │ Location │ - ├────────────────┼─────────────────────────────────────────────┤ - │ 'a step' │ cypress/support/step_definitions/steps.js:3 │ - │ a step │ cypress/e2e/a.feature:3 │ - └────────────────┴─────────────────────────────────────────────┘ - """ diff --git a/features/dry_run.feature b/features/dry_run.feature new file mode 100644 index 00000000..bdbc8cc6 --- /dev/null +++ b/features/dry_run.feature @@ -0,0 +1,253 @@ +Feature: dry run + + Background: + Given additional preprocessor configuration + """ + { + "dryRun": true + } + """ + + Rule: it should only fail upon undefined or ambiguous steps + + Scenario: undefined step + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given an undefined step + """ + When I run cypress + Then it fails + + Scenario: ambiguous step + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + Given(/a step/, function() {}); + """ + When I run cypress + Then it fails + + Scenario: failing step + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() { + throw "some error"; + }); + """ + When I run cypress + Then it passes + + Scenario: failing Before() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, Given } = require("@badeball/cypress-cucumber-preprocessor"); + Before(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing BeforeAll() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { BeforeAll, Given } = require("@badeball/cypress-cucumber-preprocessor"); + BeforeAll(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing BeforeStep() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { BeforeStep, Given } = require("@badeball/cypress-cucumber-preprocessor"); + BeforeStep(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing After() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { After, Given } = require("@badeball/cypress-cucumber-preprocessor"); + After(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing AfterAll() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { AfterAll, Given } = require("@badeball/cypress-cucumber-preprocessor"); + AfterAll(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing AfterStep() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { AfterStep, Given } = require("@badeball/cypress-cucumber-preprocessor"); + AfterStep(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing before() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + before(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing beforeEach() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + beforeEach(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing after() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + after(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing afterEach() hook + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + afterEach(function() { + throw "some error"; + }); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + + Scenario: failing support file + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + And a file named "cypress/support/e2e.js" with: + """ + throw "some error"; + """ + When I run cypress + Then it passes diff --git a/features/experimental_source_map.feature b/features/experimental_source_map.feature new file mode 100644 index 00000000..e6f73c1d --- /dev/null +++ b/features/experimental_source_map.feature @@ -0,0 +1,216 @@ +@no-default-plugin +Feature: experimental source map + + Background: + Given additional preprocessor configuration + """ + { + "json": { + "enabled": true + } + } + """ + + Rule: it should work with esbuild + + Background: + Given a file named "setupNodeEvents.js" with: + """ + const { addCucumberPreprocessorPlugin } = require("@badeball/cypress-cucumber-preprocessor"); + const { createEsbuildPlugin } = require("@badeball/cypress-cucumber-preprocessor/esbuild"); + const createBundler = require("@bahmutov/cypress-esbuild-preprocessor"); + + module.exports = async (on, config) => { + await addCucumberPreprocessorPlugin(on, config); + + on( + "file:preprocessor", + createBundler({ + plugins: [createEsbuildPlugin(config)], + sourcemap: "inline" + }) + ); + + return config; + } + """ + + Scenario: ambiguous step definitions + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + Given(/a step/, function() {}); + """ + When I run cypress + Then it fails + And the output should contain + """ + Multiple matching step definitions for: a step + a step - cypress/support/step_definitions/steps.js:2 + /a step/ - cypress/support/step_definitions/steps.js:3 + """ + + Scenario: json report + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, After, Given } = require("@badeball/cypress-cucumber-preprocessor"); + Before(function() {}); + After(function() {}); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + And there should be a JSON output similar to "fixtures/experimental-source-map.json" + + Rule: it should work with webpack + + Background: + Given a file named "setupNodeEvents.js" with: + """ + const webpack = require("@cypress/webpack-preprocessor"); + const { addCucumberPreprocessorPlugin } = require("@badeball/cypress-cucumber-preprocessor"); + + module.exports = async (on, config) => { + await addCucumberPreprocessorPlugin(on, config); + + on( + "file:preprocessor", + webpack({ + webpackOptions: { + resolve: { + extensions: [".ts", ".js"] + }, + module: { + rules: [ + { + test: /\.feature$/, + use: [ + { + loader: "@badeball/cypress-cucumber-preprocessor/webpack", + options: config + } + ] + } + ] + } + } + }) + ); + + return config; + }; + """ + + Scenario: ambiguous step definitions + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + Given(/a step/, function() {}); + """ + When I run cypress + Then it fails + And the output should contain + """ + Multiple matching step definitions for: a step + a step - cypress/support/step_definitions/steps.js:2 + /a step/ - cypress/support/step_definitions/steps.js:3 + """ + + Scenario: json report + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, After, Given } = require("@badeball/cypress-cucumber-preprocessor"); + Before(function() {}); + After(function() {}); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + And there should be a JSON output similar to "fixtures/experimental-source-map.json" + + Rule: it should work with browserify + + Background: + Given a file named "setupNodeEvents.js" with: + """ + const browserify = require("@cypress/browserify-preprocessor"); + const { addCucumberPreprocessorPlugin } = require("@badeball/cypress-cucumber-preprocessor"); + const { preprendTransformerToOptions } = require("@badeball/cypress-cucumber-preprocessor/browserify"); + + module.exports = async (on, config) => { + await addCucumberPreprocessorPlugin(on, config); + + on( + "file:preprocessor", + browserify(preprendTransformerToOptions(config, browserify.defaultOptions)), + ); + + return config; + }; + """ + + Scenario: ambiguous step definitions + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + Given(/a step/, function() {}); + """ + When I run cypress + Then it fails + And the output should contain + """ + Multiple matching step definitions for: a step + a step - cypress/support/step_definitions/steps.js:2 + /a step/ - cypress/support/step_definitions/steps.js:3 + """ + + Scenario: json report + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Before, After, Given } = require("@badeball/cypress-cucumber-preprocessor"); + Before(function() {}); + After(function() {}); + Given("a step", function() {}); + """ + When I run cypress + Then it passes + And there should be a JSON output similar to "fixtures/experimental-source-map.json" diff --git a/features/fixtures/another-passed-example.ndjson b/features/fixtures/another-passed-example.ndjson index 56c8ea87..c15af6b3 100644 --- a/features/fixtures/another-passed-example.ndjson +++ b/features/fixtures/another-passed-example.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: another feature\n Scenario: another scenario\n Given a step","uri":"cypress/e2e/b.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"another feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"another scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/b.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/b.feature","astNodeIds":["id"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/attachments/screenshot.ndjson b/features/fixtures/attachments/screenshot.ndjson index 48c39490..e87a2ac2 100644 --- a/features/fixtures/attachments/screenshot.ndjson +++ b/features/fixtures/attachments/screenshot.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/experimental-source-map.json b/features/fixtures/experimental-source-map.json new file mode 100644 index 00000000..0be4dbf5 --- /dev/null +++ b/features/fixtures/experimental-source-map.json @@ -0,0 +1,53 @@ +[ + { + "description": "", + "elements": [ + { + "description": "", + "id": "a-feature-name;a-scenario-name", + "keyword": "Scenario", + "line": 2, + "name": "a scenario name", + "steps": [ + { + "keyword": "Before", + "hidden": true, + "result": { + "status": "passed", + "duration": 0 + } + }, + { + "arguments": [], + "keyword": "Given ", + "line": 3, + "name": "a step", + "match": { + "location": "cypress/support/step_definitions/steps.js:4" + }, + "result": { + "status": "passed", + "duration": 0 + } + }, + { + "keyword": "After", + "hidden": true, + "result": { + "status": "passed", + "duration": 0 + } + } + ], + "tags": [], + "type": "scenario" + } + ], + "id": "a-feature-name", + "line": 1, + "keyword": "Feature", + "name": "a feature name", + "tags": [], + "uri": "cypress/e2e/a.feature" + } +] diff --git a/features/fixtures/failing-after.ndjson b/features/fixtures/failing-after.ndjson index b623c322..06eec35c 100644 --- a/features/fixtures/failing-after.ndjson +++ b/features/fixtures/failing-after.ndjson @@ -3,8 +3,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"hook":{"id":"id","sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"hook":{"id":"id","sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","hookId":"id"}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/failing-before.ndjson b/features/fixtures/failing-before.ndjson index 63261f7e..0a28dbe9 100644 --- a/features/fixtures/failing-before.ndjson +++ b/features/fixtures/failing-before.ndjson @@ -3,8 +3,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"hook":{"id":"id","sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"hook":{"id":"id","sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","hookId":"id"},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/failing-step.ndjson b/features/fixtures/failing-step.ndjson index 7c76256b..037041b4 100644 --- a/features/fixtures/failing-step.ndjson +++ b/features/fixtures/failing-step.ndjson @@ -3,9 +3,9 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a preceding step\n And a failing step\n And a succeeding step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a preceding step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a failing step"},{"id":"id","location":{"line":5,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a succeeding step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a preceding step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a failing step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a succeeding step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/multiple-features.ndjson b/features/fixtures/multiple-features.ndjson index 2476126e..15e384f8 100644 --- a/features/fixtures/multiple-features.ndjson +++ b/features/fixtures/multiple-features.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} @@ -12,7 +12,7 @@ {"source":{"data":"Feature: another feature\n Scenario: another scenario\n Given a step","uri":"cypress/e2e/b.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"another feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"another scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/b.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/b.feature","astNodeIds":["id"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/multiple-scenarios-reloaded.ndjson b/features/fixtures/multiple-scenarios-reloaded.ndjson index 1ec7a7d4..9889f285 100644 --- a/features/fixtures/multiple-scenarios-reloaded.ndjson +++ b/features/fixtures/multiple-scenarios-reloaded.ndjson @@ -4,8 +4,8 @@ {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[{"location":{"line":2,"column":3},"name":"@env(origin=\"https://duckduckgo.com/\")","id":"id"}],"location":{"line":3,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}},{"scenario":{"id":"id","tags":[{"location":{"line":6,"column":3},"name":"@env(origin=\"https://google.com/\")","id":"id"}],"location":{"line":7,"column":3},"keyword":"Scenario","name":"another scenario","description":"","steps":[{"id":"id","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"another step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@env(origin=\"https://duckduckgo.com/\")","astNodeId":"id"}],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@env(origin=\"https://google.com/\")","astNodeId":"id"}],"name":"another scenario","language":"en","steps":[{"id":"id","text":"another step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"another step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"another step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/passed-example.ndjson b/features/fixtures/passed-example.ndjson index 2f9a9a95..b80d1fe2 100644 --- a/features/fixtures/passed-example.ndjson +++ b/features/fixtures/passed-example.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/passed-outline.ndjson b/features/fixtures/passed-outline.ndjson index 2c154cfe..3f70c3ec 100644 --- a/features/fixtures/passed-outline.ndjson +++ b/features/fixtures/passed-outline.ndjson @@ -4,7 +4,7 @@ {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario Outline","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[{"id":"id","tags":[],"location":{"line":4,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"id","location":{"line":5,"column":7},"cells":[{"location":{"line":5,"column":9},"value":"value"}]},"tableBody":[{"id":"id","location":{"line":6,"column":7},"cells":[{"location":{"line":6,"column":9},"value":"foo"}]},{"id":"id","location":{"line":7,"column":7},"cells":[{"location":{"line":7,"column":9},"value":"bar"}]}]}]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id","id"],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id","id"]}],"tags":[]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id","id"],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id","id"]}],"tags":[]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/pending-steps.ndjson b/features/fixtures/pending-steps.ndjson index 87a69776..73c82473 100644 --- a/features/fixtures/pending-steps.ndjson +++ b/features/fixtures/pending-steps.ndjson @@ -3,9 +3,9 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a preceding step\n And a pending step\n And a succeeding step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a preceding step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a pending step"},{"id":"id","location":{"line":5,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a succeeding step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a preceding step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a pending step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a succeeding step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a pending step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a pending step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/rescued-error.ndjson b/features/fixtures/rescued-error.ndjson index 5eb64d50..089be5aa 100644 --- a/features/fixtures/rescued-error.ndjson +++ b/features/fixtures/rescued-error.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a failing step\n And an unimplemented step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a failing step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an unimplemented step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a failing step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"an unimplemented step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":[]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/retried.ndjson b/features/fixtures/retried.ndjson index 5029b268..6351d015 100644 --- a/features/fixtures/retried.ndjson +++ b/features/fixtures/retried.ndjson @@ -3,7 +3,7 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/skipped-all-scenarios.ndjson b/features/fixtures/skipped-all-scenarios.ndjson index 7c629fda..4def0a05 100644 --- a/features/fixtures/skipped-all-scenarios.ndjson +++ b/features/fixtures/skipped-all-scenarios.ndjson @@ -5,7 +5,7 @@ {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"first scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"second scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"third scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} diff --git a/features/fixtures/skipped-first-scenario.ndjson b/features/fixtures/skipped-first-scenario.ndjson index 6af0d968..9944c6d2 100644 --- a/features/fixtures/skipped-first-scenario.ndjson +++ b/features/fixtures/skipped-first-scenario.ndjson @@ -5,7 +5,7 @@ {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"first scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"second scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"third scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} diff --git a/features/fixtures/skipped-second-scenario.ndjson b/features/fixtures/skipped-second-scenario.ndjson index 5f9fdc01..017e866f 100644 --- a/features/fixtures/skipped-second-scenario.ndjson +++ b/features/fixtures/skipped-second-scenario.ndjson @@ -5,7 +5,7 @@ {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"first scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"second scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"third scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} diff --git a/features/fixtures/skipped-steps.ndjson b/features/fixtures/skipped-steps.ndjson index 64620cb0..60060300 100644 --- a/features/fixtures/skipped-steps.ndjson +++ b/features/fixtures/skipped-steps.ndjson @@ -3,9 +3,9 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a preceding step\n And a skipped step\n And a succeeding step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a preceding step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a skipped step"},{"id":"id","location":{"line":5,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a succeeding step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a preceding step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a skipped step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a succeeding step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a skipped step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a skipped step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/skipped-third-scenario.ndjson b/features/fixtures/skipped-third-scenario.ndjson index d29bf06e..d18c0280 100644 --- a/features/fixtures/skipped-third-scenario.ndjson +++ b/features/fixtures/skipped-third-scenario.ndjson @@ -5,7 +5,7 @@ {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"first scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"second scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[{"name":"@skip","astNodeId":"id"}],"name":"third scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} diff --git a/features/fixtures/undefined-steps.ndjson b/features/fixtures/undefined-steps.ndjson index 0ded8231..61d318dd 100644 --- a/features/fixtures/undefined-steps.ndjson +++ b/features/fixtures/undefined-steps.ndjson @@ -3,8 +3,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a preceding step\n And an undefined step\n And a succeeding step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a preceding step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an undefined step"},{"id":"id","location":{"line":5,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a succeeding step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a preceding step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"an undefined step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"a succeeding step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a preceding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a succeeding step"},"sourceReference":{"uri":"not available","location":{"line":0,"column":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":[]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/issues/736.feature b/features/issues/736.feature index 0afb485d..b0bf3a0d 100644 --- a/features/issues/736.feature +++ b/features/issues/736.feature @@ -16,6 +16,10 @@ Feature: create output directories "html": { "enabled": true, "output": "baz/cucumber-report.html" + }, + "usage": { + "enabled": true, + "output": "qux/usage-report.html" } } """ diff --git a/features/reporters/usage.feature b/features/reporters/usage.feature new file mode 100644 index 00000000..0b129e0a --- /dev/null +++ b/features/reporters/usage.feature @@ -0,0 +1,196 @@ +@no-default-plugin +Feature: usage report + + Background: + Given additional preprocessor configuration + """ + { + "usage": { + "enabled": true + } + } + """ + And a file named "setupNodeEvents.js" with: + """ + const { addCucumberPreprocessorPlugin } = require("@badeball/cypress-cucumber-preprocessor"); + const { createEsbuildPlugin } = require("@badeball/cypress-cucumber-preprocessor/esbuild"); + const createBundler = require("@bahmutov/cypress-esbuild-preprocessor"); + + module.exports = async (on, config) => { + await addCucumberPreprocessorPlugin(on, config); + + on( + "file:preprocessor", + createBundler({ + plugins: [createEsbuildPlugin(config)], + sourcemap: "inline" + }) + ); + + return config; + } + """ + + Rule: it is outputted to stdout by default + + Scenario: default + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + When I run cypress + Then the output should contain a usage report + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ + + Scenario: custom location + Given additional preprocessor configuration + """ + { + "usage": { + "enabled": true, + "output": "usage-report.txt" + } + } + """ + And a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + When I run cypress + Then there should be a usage report named "usage-report.txt" containing + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ + + Rule: usage should be grouped by step definition + Scenario: one definition + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + When I run cypress + Then the output should contain a usage report + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ + + Scenario: one definition, repeated + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + And a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + When I run cypress + Then the output should contain a usage report + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:4 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ + + Scenario: two definitions + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + And another step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + Given("another step", function() {}); + """ + When I run cypress + Then the output should contain a usage report + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ another step │ 0.00ms │ cypress/support/step_definitions/steps.js:3 │ + │ another step │ 0.00ms │ cypress/e2e/a.feature:4 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ + + Scenario: two features + Given a file named "cypress/e2e/a.feature" with: + """ + Feature: a feature name + Scenario: a scenario name + Given a step + """ + Given a file named "cypress/e2e/b.feature" with: + """ + Feature: another feature name + Scenario: another scenario name + Given a step + """ + And a file named "cypress/support/step_definitions/steps.js" with: + """ + const { Given } = require("@badeball/cypress-cucumber-preprocessor"); + Given("a step", function() {}); + """ + When I run cypress + Then the output should contain a usage report + """ + ┌────────────────┬──────────┬─────────────────────────────────────────────┐ + │ Pattern / Text │ Duration │ Location │ + ├────────────────┼──────────┼─────────────────────────────────────────────┤ + │ a step │ 0.00ms │ cypress/support/step_definitions/steps.js:2 │ + │ a step │ 0.00ms │ cypress/e2e/a.feature:3 │ + │ a step │ 0.00ms │ cypress/e2e/b.feature:3 │ + └────────────────┴──────────┴─────────────────────────────────────────────┘ + """ diff --git a/features/step_definitions/cli_steps.ts b/features/step_definitions/cli_steps.ts index dd22ac00..dd6a90e9 100644 --- a/features/step_definitions/cli_steps.ts +++ b/features/step_definitions/cli_steps.ts @@ -7,7 +7,7 @@ import childProcess from "child_process"; import stripAnsi from "strip-ansi"; import * as glob from "glob"; import ICustomWorld from "../support/ICustomWorld"; -import { assertAndReturn } from "../support/helpers"; +import { expectLastRun, rescape } from "../support/helpers"; const isCI = process.env.CI === "true"; @@ -89,14 +89,6 @@ When( }, ); -When( - "I run diagnostics", - { timeout: 60 * 1000 }, - async function (this: ICustomWorld) { - await this.runDiagnostics(); - }, -); - When( "I merge the messages reports", { timeout: 60 * 1000 }, @@ -116,9 +108,6 @@ When( }, ); -const expectLastRun = (world: ICustomWorld) => - assertAndReturn(world.lastRun, "Expected to find information about last run"); - Then("it passes", function (this: ICustomWorld) { assert.equal(expectLastRun(this).exitCode, 0, "Expected a zero exit code"); }); @@ -194,11 +183,6 @@ Then( }, ); -/** - * Shamelessly copied from the RegExp.escape proposal. - */ -const rescape = (s: string) => String(s).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&"); - const runScenarioExpr = (scenarioName: string) => new RegExp(`(?:✓|√) ${rescape(scenarioName)}( \\(\\d+ms\\))?\\n`); diff --git a/features/step_definitions/usage_steps.ts b/features/step_definitions/usage_steps.ts new file mode 100644 index 00000000..6996bf50 --- /dev/null +++ b/features/step_definitions/usage_steps.ts @@ -0,0 +1,33 @@ +import { Then } from "@cucumber/cucumber"; +import path from "path"; +import { promises as fs } from "fs"; +import assert from "assert"; +import { expectLastRun, rescape } from "../support/helpers"; +import ICustomWorld from "../support/ICustomWorld"; + +const normalizeUsageOutput = (content: string) => + content.replaceAll(/\d+\.\d+ms/g, (match: string) => { + const replaceWith = "0.00ms"; + return replaceWith + " ".repeat(match.length - replaceWith.length); + }); + +Then( + "there should be a usage report named {string} containing", + async function (file, expectedContent) { + const absoluteFilePath = path.join(this.tmpDir, file); + + const actualContent = (await fs.readFile(absoluteFilePath)).toString(); + + assert.equal(normalizeUsageOutput(actualContent), expectedContent + "\n"); + }, +); + +Then( + "the output should contain a usage report", + function (this: ICustomWorld, expectedContent) { + assert.match( + normalizeUsageOutput(expectLastRun(this).stdout), + new RegExp(rescape(expectedContent)), + ); + }, +); diff --git a/features/support/ICustomWorld.ts b/features/support/ICustomWorld.ts index 9252e8a1..0c92daa7 100644 --- a/features/support/ICustomWorld.ts +++ b/features/support/ICustomWorld.ts @@ -18,7 +18,5 @@ export default interface ICustomWorld { runCypress(options?: ExtraOptions): Promise; - runDiagnostics(options?: ExtraOptions): Promise; - runMergeMessages(options?: ExtraOptions): Promise; } diff --git a/features/support/helpers.ts b/features/support/helpers.ts index 16031048..4d018a29 100644 --- a/features/support/helpers.ts +++ b/features/support/helpers.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { version as cypressVersion } from "cypress/package.json"; import { promises as fs } from "fs"; import path from "path"; +import ICustomWorld from "./ICustomWorld"; export async function writeFile(filePath: string, fileContent: string) { await fs.mkdir(path.dirname(filePath), { recursive: true }); @@ -114,3 +115,12 @@ export function isPost12() { export function isPre12() { return !isPost12(); } + +/** + * Shamelessly copied from the RegExp.escape proposal. + */ +export const rescape = (s: string) => + String(s).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&"); + +export const expectLastRun = (world: ICustomWorld) => + assertAndReturn(world.lastRun, "Expected to find information about last run"); diff --git a/features/support/world.ts b/features/support/world.ts index 38b73e8d..93142abd 100644 --- a/features/support/world.ts +++ b/features/support/world.ts @@ -54,22 +54,6 @@ export default class CustomWorld implements ICustomWorld { }); } - runDiagnostics({ - extraArgs = [], - extraEnv = {}, - expectedExitCode, - }: ExtraOptions = {}) { - return this.runCommand({ - cmd: "node", - args: [ - path.join(projectPath, bin["cypress-cucumber-diagnostics"]), - ...extraArgs, - ], - extraEnv, - expectedExitCode, - }); - } - runMergeMessages({ extraArgs = [], extraEnv = {}, diff --git a/lib/add-cucumber-preprocessor-plugin.ts b/lib/add-cucumber-preprocessor-plugin.ts index 406cb18f..5b7b19b9 100644 --- a/lib/add-cucumber-preprocessor-plugin.ts +++ b/lib/add-cucumber-preprocessor-plugin.ts @@ -195,5 +195,9 @@ export async function addCucumberPreprocessorPlugin( ); } + if (preprocessor.dryRun) { + config.supportFile = false; + } + return config; } diff --git a/lib/bin/diagnostics.ts b/lib/bin/diagnostics.ts deleted file mode 100644 index 970f617a..00000000 --- a/lib/bin/diagnostics.ts +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node - -import { execute } from "../diagnostics"; - -execute({ argv: process.argv, env: process.env, cwd: process.cwd() }).catch( - (err) => { - console.error(err.stack); - process.exitCode = 1; - }, -); diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 609e0e32..7380792a 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -70,6 +70,7 @@ import { isNotExclusivelySuiteConfiguration, tagsToOptions, } from "./helpers/options"; +import { Position } from "./helpers/source-map"; type Node = ReturnType; @@ -92,12 +93,24 @@ interface CompositionContext { stepDefinitionPatterns: string[]; stepDefinitionPaths: string[]; }; + dryRun: boolean; } -const sourceReference: messages.SourceReference = { - uri: "not available", - location: { line: 0 }, -}; +function getSourceReferenceFromPosition( + position?: Position, +): messages.SourceReference { + if (position) { + return { + uri: position.source, + location: { line: position.line, column: position.column }, + }; + } else { + return { + uri: "not available", + location: { line: 0, column: 0 }, + }; + } +} interface IStep { hook?: ICaseHook; @@ -107,6 +120,8 @@ interface IStep { const internalPropertiesReplacementText = "Internal properties of cypress-cucumber-preprocessor omitted from report."; +const noopFn = () => {}; + export interface InternalSpecProperties { pickle: messages.Pickle; testCaseStartedId: string; @@ -356,20 +371,24 @@ function createFeature(context: CompositionContext, feature: messages.Feature) { tagsToOptions(feature.tags).filter(isExclusivelySuiteConfiguration), ) as Cypress.TestConfigOverrides; + const mochaGlobals = + globalThis["__cypress_cucumber_preprocessor_mocha_dont_use_this"] ?? + globalThis; + describe(feature.name || "", suiteOptions, () => { - before(function () { + mochaGlobals.before(function () { beforeHandler.call(this, context); }); - beforeEach(function () { + mochaGlobals.beforeEach(function () { beforeEachHandler.call(this, context); }); - after(function () { + mochaGlobals.after(function () { afterHandler.call(this, context); }); - afterEach(function () { + mochaGlobals.afterEach(function () { afterEachHandler.call(this, context); }); @@ -458,7 +477,7 @@ function createScenario( } function createPickle(context: CompositionContext, pickle: messages.Pickle) { - const { registry, gherkinDocument, pickles, testFilter } = context; + const { registry, gherkinDocument, pickles, testFilter, dryRun } = context; const testCaseId = pickle.id; const pickleSteps = pickle.steps ?? []; const scenarioName = pickle.name || ""; @@ -681,7 +700,9 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { }; return runStepWithLogGroup({ - fn: () => registry.runCaseHook(this, hook, options), + fn: dryRun + ? noopFn + : () => registry.runCaseHook(this, hook, options), keyword: hook.keyword, text: createStepDescription(hook), }).then((result) => { @@ -755,8 +776,10 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { runStepWithLogGroup({ keyword: "BeforeStep", text: createStepDescription(beforeStepHook), - fn: () => - registry.runStepHook(this, beforeStepHook, options), + fn: dryRun + ? noopFn + : () => + registry.runStepHook(this, beforeStepHook, options), }), ); }, @@ -772,7 +795,8 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { ), argument, text, - fn: () => registry.runStepDefininition(this, text, argument), + fn: () => + registry.runStepDefininition(this, text, dryRun, argument), }).then((result) => { return afterStepHooks .reduce( @@ -781,12 +805,14 @@ function createPickle(context: CompositionContext, pickle: messages.Pickle) { runStepWithLogGroup({ keyword: "AfterStep", text: createStepDescription(afterStepHook), - fn: () => - registry.runStepHook( - this, - afterStepHook, - options, - ), + fn: dryRun + ? noopFn + : () => + registry.runStepHook( + this, + afterStepHook, + options, + ), }), ); }, @@ -865,7 +891,7 @@ function beforeHandler(this: Mocha.Context, context: CompositionContext) { for (const hook of registry.resolveBeforeAllHooks()) { runStepWithLogGroup({ - fn: () => registry.runRunHook(this, hook), + fn: context.dryRun ? noopFn : () => registry.runRunHook(this, hook), keyword: "BeforeAll", }); } @@ -1127,7 +1153,7 @@ function afterHandler(this: Mocha.Context, context: CompositionContext) { for (const hook of registry.resolveAfterAllHooks()) { runStepWithLogGroup({ - fn: () => registry.runRunHook(this, hook), + fn: context.dryRun ? noopFn : () => registry.runRunHook(this, hook), keyword: "AfterAll", }); } @@ -1146,6 +1172,9 @@ export default function createTests( stepDefinitionPatterns: string[]; stepDefinitionPaths: string[]; }, + projectRoot: string, + sourcesRelativeTo: string, + dryRun: boolean, ) { const prng = random(seed.toString()); @@ -1154,7 +1183,7 @@ export default function createTests( random: Array.from({ length: 16 }, () => Math.floor(prng() * 256)), }); - registry.finalize(newId); + registry.finalize(newId, projectRoot, sourcesRelativeTo); const testFilter = createTestFilter(gherkinDocument, Cypress.env()); @@ -1171,7 +1200,9 @@ export default function createTests( type, source: stepDefinition.expression.source, }, - sourceReference, + sourceReference: getSourceReferenceFromPosition( + stepDefinition.position, + ), }; }); @@ -1256,7 +1287,7 @@ export default function createTests( hook: { id: hook.id, name: hook.name, - sourceReference, + sourceReference: getSourceReferenceFromPosition(hook.position), }, }); } @@ -1288,6 +1319,7 @@ export default function createTests( omitFiltered, isTrackingState, stepDefinitionHints, + dryRun, }; if (gherkinDocument.feature) { diff --git a/lib/diagnostics/diagnose.ts b/lib/diagnostics/diagnose.ts deleted file mode 100644 index 503a63cc..00000000 --- a/lib/diagnostics/diagnose.ts +++ /dev/null @@ -1,372 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; -import util from "util"; -import { getSpecs } from "find-cypress-specs"; -import { - Expression, - ParameterTypeRegistry, - RegularExpression, -} from "@cucumber/cucumber-expressions"; -import { generateMessages } from "@cucumber/gherkin"; -import { - IdGenerator, - SourceMediaType, - PickleStepType, -} from "@cucumber/messages"; -import * as esbuild from "esbuild"; -import sourceMap from "source-map"; -import { assert, assertAndReturn } from "../helpers/assertions"; -import { createAstIdMap } from "../helpers/ast"; -import { ensureIsRelative } from "../helpers/paths"; -import { - ICypressRuntimeConfiguration, - IPreprocessorConfiguration, -} from "../preprocessor-configuration"; -import { IStepDefinition, Registry, withRegistry } from "../registry"; -import { Position } from "../helpers/source-map"; -import { - getStepDefinitionPatterns, - getStepDefinitionPaths, -} from "../step-definitions"; -import { notNull } from "../helpers/type-guards"; - -export interface DiagnosticStep { - source: string; - line: number; - text: string; -} - -export interface UnmatchedStep { - step: DiagnosticStep; - type: PickleStepType; - argument: "docString" | "dataTable" | null; - parameterTypeRegistry: ParameterTypeRegistry; - stepDefinitionHints: { - stepDefinitions: string[]; - stepDefinitionPatterns: string[]; - stepDefinitionPaths: string[]; - }; -} - -export interface AmbiguousStep { - step: DiagnosticStep; - definitions: IStepDefinition[]; -} - -export interface DiagnosticResult { - definitionsUsage: { - definition: IStepDefinition; - steps: DiagnosticStep[]; - }[]; - unmatchedSteps: UnmatchedStep[]; - ambiguousSteps: AmbiguousStep[]; -} - -export function expressionToString(expression: Expression) { - return expression instanceof RegularExpression - ? String(expression.regexp) - : expression.source; -} - -export function strictCompare(a: T, b: T) { - return a === b; -} - -export function comparePosition(a: Position, b: Position) { - return a.source === b.source && a.column === b.column && a.line === b.line; -} - -export function compareStepDefinition( - a: IStepDefinition, - b: IStepDefinition, -) { - return ( - expressionToString(a.expression) === expressionToString(b.expression) && - comparePosition(position(a), position(b)) - ); -} - -export function position( - definition: IStepDefinition, -): Position { - return assertAndReturn(definition.position, "Expected to find a position"); -} - -export async function diagnose(configuration: { - cypress: ICypressRuntimeConfiguration; - preprocessor: IPreprocessorConfiguration; -}): Promise { - const result: DiagnosticResult = { - definitionsUsage: [], - unmatchedSteps: [], - ambiguousSteps: [], - }; - - const testFiles = getSpecs(configuration.cypress as any, "e2e"); - - for (const testFile of testFiles) { - if (!testFile.endsWith(".feature")) { - continue; - } - - const stepDefinitionPatterns = getStepDefinitionPatterns( - configuration, - testFile, - ); - - const stepDefinitions = await getStepDefinitionPaths( - configuration.cypress.projectRoot, - stepDefinitionPatterns, - ); - - const randomPart = Math.random().toString(16).slice(2, 8); - - const inputFileName = path.join( - configuration.cypress.projectRoot, - ".input-" + randomPart + ".js", - ); - - const outputFileName = path.join( - configuration.cypress.projectRoot, - ".output-" + randomPart + ".cjs", - ); - - let registry: Registry; - - const newId = IdGenerator.uuid(); - - try { - await fs.writeFile( - inputFileName, - stepDefinitions - .map( - (stepDefinition) => `require(${JSON.stringify(stepDefinition)});`, - ) - .join("\n"), - ); - - const esbuildResult = await esbuild.build({ - entryPoints: [inputFileName], - bundle: true, - sourcemap: "external", - outfile: outputFileName, - }); - - if (esbuildResult.errors.length > 0) { - for (const error of esbuildResult.errors) { - console.error(JSON.stringify(error)); - } - - throw new Error( - `Failed to compile step definitions of ${testFile}, with errors shown above...`, - ); - } - - const cypressMockGlobals = { - Cypress: { - env() {}, - on() {}, - config() {}, - }, - }; - - Object.assign(globalThis, cypressMockGlobals); - - registry = withRegistry(true, () => { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - require(outputFileName); - } catch (e: unknown) { - console.log(util.inspect(e)); - - throw new Error( - "Failed to evaluate step definitions, with errors shown above...", - ); - } - }); - - registry.finalize(newId); - - const consumer = await new sourceMap.SourceMapConsumer( - (await fs.readFile(outputFileName + ".map")).toString(), - ); - - for (const stepDefinition of registry.stepDefinitions) { - const originalPosition = position(stepDefinition); - - const newPosition = consumer.originalPositionFor(originalPosition); - - stepDefinition.position = { - line: assertAndReturn( - newPosition.line, - "Expected to find a line number", - ), - column: assertAndReturn( - newPosition.column, - "Expected to find a column number", - ), - source: assertAndReturn( - newPosition.source, - "Expected to find a source", - ), - }; - } - - consumer.destroy(); - } finally { - /** - * Delete without regard for errors. - */ - await fs.rm(inputFileName).catch(() => true); - await fs.rm(outputFileName).catch(() => true); - await fs.rm(outputFileName + ".map").catch(() => true); - } - - const options = { - includeSource: false, - includeGherkinDocument: true, - includePickles: true, - newId, - }; - - const relativeUri = ensureIsRelative( - configuration.cypress.projectRoot, - testFile, - ); - - const envelopes = generateMessages( - (await fs.readFile(testFile)).toString(), - relativeUri, - SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, - options, - ); - - const gherkinDocument = assertAndReturn( - envelopes - .map((envelope) => envelope.gherkinDocument) - .find((document) => document), - "Expected to find a gherkin document", - ); - - for (const stepDefinition of registry.stepDefinitions) { - const usage = result.definitionsUsage.find((usage) => - compareStepDefinition(usage.definition, stepDefinition), - ); - - if (!usage) { - result.definitionsUsage.push({ - definition: stepDefinition, - steps: [], - }); - } - } - - const astIdMap = createAstIdMap(gherkinDocument); - - const pickles = envelopes - .map((envelope) => envelope.pickle) - .filter(notNull); - - for (const pickle of pickles) { - if (pickle.steps) { - for (const step of pickle.steps) { - const text = assertAndReturn( - step.text, - "Expected pickle step to have a text", - ); - - const matchingStepDefinitions = - registry.getMatchingStepDefinitions(text); - - const astNodeId = assertAndReturn( - step.astNodeIds?.[0], - "Expected to find at least one astNodeId", - ); - - const astNode = assertAndReturn( - astIdMap.get(astNodeId), - `Expected to find scenario step associated with id = ${astNodeId}`, - ); - - assert("location" in astNode, "Expected ast node to have a location"); - - if (matchingStepDefinitions.length === 0) { - let argument: "docString" | "dataTable" | null = null; - - if (step.argument?.dataTable) { - argument = "dataTable"; - } else if (step.argument?.docString) { - argument = "docString"; - } - - result.unmatchedSteps.push({ - step: { - source: testFile, - line: astNode.location.line, - text: step.text!, - }, - type: assertAndReturn( - step.type, - "Expected pickleStep to have a type", - ), - argument, - parameterTypeRegistry: registry.parameterTypeRegistry, - stepDefinitionHints: { - stepDefinitions: [ - configuration.preprocessor.stepDefinitions, - ].flat(), - stepDefinitionPatterns, - stepDefinitionPaths: stepDefinitions, - }, - }); - } else if (matchingStepDefinitions.length === 1) { - const usage = assertAndReturn( - result.definitionsUsage.find((usage) => - compareStepDefinition( - usage.definition, - matchingStepDefinitions[0], - ), - ), - "Expected to find usage", - ); - - usage.steps.push({ - source: testFile, - line: astNode.location?.line, - text: step.text!, - }); - } else { - for (const matchingStepDefinition of matchingStepDefinitions) { - const usage = assertAndReturn( - result.definitionsUsage.find((usage) => - compareStepDefinition( - usage.definition, - matchingStepDefinition, - ), - ), - "Expected to find usage", - ); - - usage.steps.push({ - source: testFile, - line: astNode.location.line, - text: step.text!, - }); - } - - result.ambiguousSteps.push({ - step: { - source: testFile, - line: astNode.location.line, - text: step.text!, - }, - definitions: matchingStepDefinitions, - }); - } - } - } - } - } - - return result; -} diff --git a/lib/diagnostics/index.ts b/lib/diagnostics/index.ts deleted file mode 100755 index d1b3965e..00000000 --- a/lib/diagnostics/index.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { inspect } from "util"; -import path from "path"; -import { - CucumberExpressionGenerator, - Expression, - RegularExpression, -} from "@cucumber/cucumber-expressions"; -import { getConfig, getSpecs } from "find-cypress-specs"; -import Table from "cli-table"; -import ancestor from "common-ancestor-path"; -import { - ICypressRuntimeConfiguration, - resolve as resolvePreprocessorConfiguration, -} from "../preprocessor-configuration"; -import { Position } from "../helpers/source-map"; -import { IStepDefinition } from "../registry"; -import { ensureIsRelative } from "../helpers/paths"; -import { indent } from "../helpers/strings"; -import { - AmbiguousStep, - diagnose, - DiagnosticResult, - UnmatchedStep, -} from "./diagnose"; -import { assertAndReturn } from "../helpers/assertions"; -import { generateSnippet } from "../helpers/snippets"; - -export function log(...lines: string[]) { - console.log(lines.join("\n")); -} - -export function red(message: string): string { - return `\x1b[31m${message}\x1b[0m`; -} - -export function yellow(message: string): string { - return `\x1b[33m${message}\x1b[0m`; -} - -export function expressionToString(expression: Expression) { - return expression instanceof RegularExpression - ? String(expression.regexp) - : expression.source; -} - -export function strictCompare(a: T, b: T) { - return a === b; -} - -export function comparePosition(a: Position, b: Position) { - return a.source === b.source && a.column === b.column && a.line === b.line; -} - -export function compareStepDefinition( - a: IStepDefinition, - b: IStepDefinition, -) { - return ( - expressionToString(a.expression) === expressionToString(b.expression) && - comparePosition(position(a), position(b)) - ); -} - -export function position( - definition: IStepDefinition, -): Position { - return assertAndReturn(definition.position, "Expected to find a position"); -} - -export function groupToMap( - collection: T[], - getKeyFn: (el: T) => K, - compareKeyFn: (a: K, b: K) => boolean, -): Map { - const map = new Map(); - - el: for (const el of collection) { - const key = getKeyFn(el); - - for (const existingKey of map.keys()) { - if (compareKeyFn(key, existingKey)) { - map.get(existingKey)!.push(el); - continue el; - } - } - - map.set(key, [el]); - } - - return map; -} - -export function mapValues( - map: Map, - fn: (el: A) => B, -): Map { - const mapped = new Map(); - - for (const [key, value] of map.entries()) { - mapped.set(key, fn(value)); - } - - return mapped; -} - -export function createLineBuffer( - fn: (append: (string: string) => void) => void, -): string[] { - const buffer: string[] = []; - const append = (line: string) => buffer.push(line); - fn(append); - return buffer; -} - -export function createDefinitionsUsage( - projectRoot: string, - result: DiagnosticResult, -): string { - const groups = mapValues( - groupToMap( - result.definitionsUsage, - (definitionsUsage) => definitionsUsage.definition.position!.source, - strictCompare, - ), - (definitionsUsages) => - mapValues( - groupToMap( - definitionsUsages, - (definitionsUsage) => definitionsUsage.definition, - compareStepDefinition, - ), - (definitionsUsages) => - definitionsUsages.flatMap( - (definitionsUsage) => definitionsUsage.steps, - ), - ), - ); - - const entries: [string, string][] = Array.from(groups.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .flatMap(([, matches]) => { - return Array.from(matches.entries()) - .sort((a, b) => position(a[0]).line - position(b[0]).line) - .map<[string, string]>(([stepDefinition, steps]) => { - const { expression } = stepDefinition; - - const right = [ - inspect( - expression instanceof RegularExpression - ? expression.regexp - : expression.source, - ) + (steps.length === 0 ? ` (${yellow("unused")})` : ""), - ...steps.map((step) => { - return " " + step.text; - }), - ].join("\n"); - - const left = [ - ensureIsRelative(projectRoot, position(stepDefinition).source) + - ":" + - position(stepDefinition).line, - ...steps.map((step) => { - return ( - ensureIsRelative(projectRoot, step.source) + ":" + step.line - ); - }), - ].join("\n"); - - return [right, left]; - }); - }); - - const table = new Table({ - head: ["Pattern / Text", "Location"], - style: { - head: [], // Disable colors in header cells. - border: [], // Disable colors for the border. - }, - }); - - table.push(...entries); - - return table.toString(); -} - -export function createAmbiguousStep( - projectRoot: string, - ambiguousStep: AmbiguousStep, -): string[] { - const relativeToProjectRoot = (path: string) => - ensureIsRelative(projectRoot, path); - - return createLineBuffer((append) => { - append( - `${red( - "Error", - )}: Multiple matching step definitions at ${relativeToProjectRoot( - ambiguousStep.step.source, - )}:${ambiguousStep.step.line} for`, - ); - append(""); - append(" " + ambiguousStep.step.text); - append(""); - append("Step matched the following definitions:"); - append(""); - - ambiguousStep.definitions - .map( - (definition) => - ` - ${inspect( - definition.expression instanceof RegularExpression - ? definition.expression.regexp - : definition.expression.source, - )} (${relativeToProjectRoot(position(definition).source)}:${ - position(definition).line - })`, - ) - .forEach(append); - }); -} - -export function createUnmatchedStep( - projectRoot: string, - unmatch: UnmatchedStep, -): string[] { - const relativeToProjectRoot = (path: string) => - ensureIsRelative(projectRoot, path); - - return createLineBuffer((append) => { - append( - `${red("Error")}: Step implementation missing at ${relativeToProjectRoot( - unmatch.step.source, - )}:${unmatch.step.line}`, - ); - append(""); - append(" " + unmatch.step.text); - append(""); - append( - "We tried searching for files containing step definitions using the following search pattern template(s):", - ); - append(""); - unmatch.stepDefinitionHints.stepDefinitions - .map((stepDefinition) => " - " + stepDefinition) - .forEach(append); - append(""); - append("These templates resolved to the following search pattern(s):"); - append(""); - unmatch.stepDefinitionHints.stepDefinitionPatterns - .map( - (stepDefinitionPattern) => - " - " + relativeToProjectRoot(stepDefinitionPattern), - ) - .forEach(append); - append(""); - - if (unmatch.stepDefinitionHints.stepDefinitionPaths.length === 0) { - append( - "These patterns matched *no files* containing step definitions. This almost certainly means that you have misconfigured `stepDefinitions`. Alternatively, you can implement it using the suggestion(s) below.", - ); - } else { - append("These patterns matched the following file(s):"); - append(""); - unmatch.stepDefinitionHints.stepDefinitionPaths - .map( - (stepDefinitionPath) => - " - " + relativeToProjectRoot(stepDefinitionPath), - ) - .forEach(append); - append(""); - append( - "However, none of these files contained a matching step definition. You can implement it using the suggestion(s) below.", - ); - } - - const cucumberExpressionGenerator = new CucumberExpressionGenerator( - () => unmatch.parameterTypeRegistry.parameterTypes, - ); - - const generatedExpressions = - cucumberExpressionGenerator.generateExpressions(unmatch.step.text); - - for (const generatedExpression of generatedExpressions) { - append(""); - - append( - indent( - generateSnippet( - generatedExpression, - "Context" as any, - unmatch.argument, - ), - { - count: 2, - }, - ), - ); - } - }); -} - -export async function execute(options: { - argv: string[]; - env: NodeJS.ProcessEnv; - cwd: string; -}): Promise { - const cypress: ICypressRuntimeConfiguration = Object.assign( - { - projectRoot: options.cwd, - testingType: "e2e" as const, - env: {}, - reporter: "spec", - specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", - excludeSpecPattern: "*.hot-update.js", - }, - getConfig().e2e ?? {}, - ); - - const implicitIntegrationFolder = assertAndReturn( - ancestor(...getSpecs(cypress, "e2e").map(path.dirname).map(path.normalize)), - "Expected to find a common ancestor path", - ); - - const preprocessor = await resolvePreprocessorConfiguration( - cypress, - options.env, - implicitIntegrationFolder, - ); - - const result = await diagnose({ - cypress, - preprocessor, - }); - - log( - ...createLineBuffer((append) => { - append(createDefinitionsUsage(options.cwd, result)); - - append(""); - - const problems = [ - ...result.ambiguousSteps.map((ambiguousStep) => { - return { ambiguousStep }; - }), - ...result.unmatchedSteps.map((unmatchedStep) => { - return { unmatchedStep }; - }), - ]; - - if (problems.length > 0) { - append(`Found ${problems.length} problem(s):`); - append(""); - - for (let i = 0; i < problems.length; i++) { - const problem = problems[i]; - - const lines = - "ambiguousStep" in problem - ? createAmbiguousStep(options.cwd, problem.ambiguousStep) - : createUnmatchedStep(options.cwd, problem.unmatchedStep); - - const title = `${i + 1}) `; - - const [first, ...rest] = lines; - - append(title + first); - - rest - .map((line) => - line === "" ? "" : indent(line, { count: title.length }), - ) - .forEach(append); - - if (i !== problems.length - 1) { - append(""); - } - } - - process.exitCode = 1; - } else { - append("No problems found."); - } - }), - ); -} diff --git a/lib/entrypoint-browser.ts b/lib/entrypoint-browser.ts index e2f9bf66..c1611ea4 100644 --- a/lib/entrypoint-browser.ts +++ b/lib/entrypoint-browser.ts @@ -58,7 +58,7 @@ function runStepDefininition( runStepWithLogGroup({ keyword: "Step", text, - fn: () => getRegistry().runStepDefininition(world, text, argument), + fn: () => getRegistry().runStepDefininition(world, text, false, argument), }); }); } diff --git a/lib/helpers/dry-run.ts b/lib/helpers/dry-run.ts new file mode 100644 index 00000000..2729506a --- /dev/null +++ b/lib/helpers/dry-run.ts @@ -0,0 +1,14 @@ +const globalPropertyName = + "__cypress_cucumber_preprocessor_mocha_dont_use_this"; + +globalThis[globalPropertyName] = { + before: globalThis.before, + beforeEach: globalThis.beforeEach, + after: globalThis.after, + afterEach: globalThis.afterEach, +}; + +window.before = () => {}; +window.beforeEach = () => {}; +window.after = () => {}; +window.afterEach = () => {}; diff --git a/lib/helpers/formatters.ts b/lib/helpers/formatters.ts index 765b96f1..37b65653 100644 --- a/lib/helpers/formatters.ts +++ b/lib/helpers/formatters.ts @@ -8,6 +8,7 @@ import { formatterHelpers, IFormatterOptions, JsonFormatter, + UsageFormatter, } from "@cucumber/cucumber"; import messages from "@cucumber/messages"; @@ -45,8 +46,8 @@ export function createJsonFormatter( .map((s) => { return { id: s.id, - uri: "not available", - line: 0, + uri: s.sourceReference.uri, + line: s.sourceReference.location?.line, }; }); @@ -74,6 +75,58 @@ export function createJsonFormatter( return eventBroadcaster; } +export function createUsageFormatter( + envelopes: messages.Envelope[], + log: (chunk: string) => void, +): EventEmitter { + const eventBroadcaster = new EventEmitter(); + + const eventDataCollector = new formatterHelpers.EventDataCollector( + eventBroadcaster, + ); + + const stepDefinitions = envelopes + .map((m) => m.stepDefinition) + .filter(notNull) + .map((s) => { + return { + id: s.id, + uri: s.sourceReference.uri, + line: s.sourceReference.location?.line, + unwrappedCode: "", + expression: { + source: s.pattern.source, + constructor: { + name: "foo", + }, + }, + }; + }); + + new UsageFormatter({ + eventBroadcaster, + eventDataCollector, + log(chunk) { + assertIsString( + chunk, + "Expected a JSON output of string, but got " + typeof chunk, + ); + log(chunk); + }, + supportCodeLibrary: { + stepDefinitions, + } as any, + colorFns: null as any, + cwd: null as any, + parsedArgvOptions: {}, + snippetBuilder: null as any, + stream: null as any, + cleanup: null as any, + }); + + return eventBroadcaster; +} + export function createPrettyFormatter( useColors: boolean, log: (chunk: string) => void, diff --git a/lib/helpers/messages.ts b/lib/helpers/messages.ts index 19ec0069..fb2bd790 100644 --- a/lib/helpers/messages.ts +++ b/lib/helpers/messages.ts @@ -1,3 +1,5 @@ +import * as messages from "@cucumber/messages"; + export type StrictTimestamp = { seconds: number; nanos: number; @@ -29,3 +31,67 @@ export function duration( export function durationToNanoseconds(duration: StrictTimestamp): number { return Math.floor(duration.seconds * 1_000_000_000 + duration.nanos); } + +export function removeDuplicatedStepDefinitions( + envelopes: messages.Envelope[], +) { + const seenDefinitions: { + id: string; + uri: string; + line: number; + column: number; + }[] = []; + + const findSeenStepDefinition = (stepDefinition: messages.StepDefinition) => + seenDefinitions.find((seenDefinition) => { + return ( + seenDefinition.uri === stepDefinition.sourceReference.uri && + seenDefinition.line === stepDefinition.sourceReference.location?.line && + seenDefinition.column === + stepDefinition.sourceReference.location?.column + ); + }); + + for (let i = 0; i < envelopes.length; i++) { + const { stepDefinition } = envelopes[i]; + + if ( + stepDefinition && + stepDefinition.sourceReference.uri !== "not available" + ) { + const seenDefinition = findSeenStepDefinition(stepDefinition); + + if (seenDefinition) { + // Remove this from the stack. + envelopes.splice(i, 1); + // Make sure we iterate over the "next". + i--; + + // Find TestCase's in which this is used. + for (let x = i; x < envelopes.length; x++) { + const { testCase } = envelopes[x]; + + if (testCase) { + for (const testStep of testCase.testSteps) { + // Replace ID's of spliced definition with ID of the prevously seen definition. + testStep.stepDefinitionIds = testStep.stepDefinitionIds?.map( + (stepDefinitionId) => + stepDefinitionId.replace( + stepDefinition.id, + seenDefinition.id, + ), + ); + } + } + } + } else { + seenDefinitions.push({ + id: stepDefinition.id, + uri: stepDefinition.sourceReference.uri!, + line: stepDefinition.sourceReference.location!.line, + column: stepDefinition.sourceReference.location!.column!, + }); + } + } + } +} diff --git a/lib/helpers/prepare-registry.ts b/lib/helpers/prepare-registry.ts index e5752769..2972772c 100644 --- a/lib/helpers/prepare-registry.ts +++ b/lib/helpers/prepare-registry.ts @@ -1,6 +1,6 @@ import { Registry, assignRegistry, freeRegistry } from "../registry"; -const registry = new Registry(false); +const registry = new Registry(); assignRegistry(registry); diff --git a/lib/helpers/source-map.ts b/lib/helpers/source-map.ts index ce8c6e20..39337971 100644 --- a/lib/helpers/source-map.ts +++ b/lib/helpers/source-map.ts @@ -1,5 +1,8 @@ +import { toByteArray } from "base64-js"; + import ErrorStackParser from "error-stack-parser"; -import { assertAndReturn } from "./assertions"; + +import { SourceMapConsumer } from "source-map"; export interface Position { line: number; @@ -7,31 +10,89 @@ export interface Position { source: string; } -export function retrievePositionFromSourceMap(): Position { +let isSourceMapWarned = false; + +function sourceMapWarn(message: string) { + if (isSourceMapWarned) { + return; + } + + console.warn("cypress-cucumber-preprocessor: " + message); + isSourceMapWarned = true; +} + +/** + * Taken from https://github.com/evanw/node-source-map-support/blob/v0.5.21/source-map-support.js#L148-L177. + */ +export function retrieveSourceMapURL(source: string) { + let fileData: string; + + const xhr = new XMLHttpRequest(); + xhr.open("GET", source, /** async */ false); + xhr.send(null); + + const { readyState, status } = xhr; + + if (readyState === 4 && status === 200) { + fileData = xhr.responseText; + } else { + sourceMapWarn( + `Unable to retrieve source map (readyState = ${readyState}, status = ${status})`, + ); + return; + } + + const re = + /(?:\/\/[@#][\s]*sourceMappingURL=([^\s'"]+)[\s]*$)|(?:\/\*[@#][\s]*sourceMappingURL=([^\s*'"]+)[\s]*(?:\*\/)[\s]*$)/gm; + + // Keep executing the search to find the *last* sourceMappingURL to avoid + // picking up sourceMappingURLs from comments, strings, etc. + let lastMatch, match; + + while ((match = re.exec(fileData))) lastMatch = match; + + if (!lastMatch) { + sourceMapWarn( + "Unable to find source mapping URL within the response. Are you bundling with source maps enabled?", + ); + return; + } + + return lastMatch[1]; +} + +export function maybeRetrievePositionFromSourceMap(): Position | undefined { const stack = ErrorStackParser.parse(new Error()); - const relevantFrame = stack[4]; + if (stack[0].fileName == null) { + return; + } - return { - line: assertAndReturn( - relevantFrame.getLineNumber(), - "Expected to find a line number", - ), - column: assertAndReturn( - relevantFrame.getColumnNumber(), - "Expected to find a column number", - ), - source: assertAndReturn( - relevantFrame.fileName, - "Expected to find a filename", + const sourceMappingURL = retrieveSourceMapURL(stack[0].fileName); + + if (!sourceMappingURL) { + return; + } + + const rawSourceMap = JSON.parse( + new TextDecoder().decode( + toByteArray(sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1)), ), - }; -} + ); -export function maybeRetrievePositionFromSourceMap( - experimentalSourceMap: boolean, -): Position | undefined { - if (experimentalSourceMap) { - return retrievePositionFromSourceMap(); + // Why? Because of Vite. Vite fails building the source-map module properly and this errors with "x is not a constructor". + if (typeof SourceMapConsumer !== "function") { + return; } + + const sourceMap = new SourceMapConsumer(rawSourceMap); + + const relevantFrame = stack[3]; + + const position = sourceMap.originalPositionFor({ + line: relevantFrame.getLineNumber()!, + column: relevantFrame.getColumnNumber()!, + }); + + return position; } diff --git a/lib/plugin-event-handlers.ts b/lib/plugin-event-handlers.ts index 6db3d6e3..a79a8dd8 100644 --- a/lib/plugin-event-handlers.ts +++ b/lib/plugin-event-handlers.ts @@ -33,7 +33,10 @@ import { resolve as origResolve } from "./preprocessor-configuration"; import { ensureIsAbsolute } from "./helpers/paths"; -import { createTimestamp } from "./helpers/messages"; +import { + createTimestamp, + removeDuplicatedStepDefinitions, +} from "./helpers/messages"; import { memoize } from "./helpers/memoize"; @@ -47,12 +50,15 @@ import { createHtmlStream, createJsonFormatter, createPrettyFormatter, + createUsageFormatter, } from "./helpers/formatters"; import { useColors } from "./helpers/colors"; import { notNull } from "./helpers/type-guards"; +import { indent } from "./helpers/strings"; + import { version as packageVersion } from "./version"; import { IStepHookParameter } from "./public-member-types"; @@ -342,6 +348,8 @@ export async function afterRunHandler(config: Cypress.PluginConfigOptions) { }, }; + removeDuplicatedStepDefinitions(state.messages.accumulation); + if (preprocessor.messages.enabled) { const messagesPath = ensureIsAbsolute( config.projectRoot, @@ -430,6 +438,39 @@ export async function afterRunHandler(config: Cypress.PluginConfigOptions) { output, ); } + + if (preprocessor.usage.enabled) { + let usageOutput: string | undefined; + + const eventBroadcaster = createUsageFormatter( + state.messages.accumulation, + (chunk) => { + usageOutput = chunk; + }, + ); + + for (const message of state.messages.accumulation) { + eventBroadcaster.emit("envelope", message); + } + + assertIsString( + usageOutput, + "Expected usage formatter to have finished, but it never returned", + ); + + if (preprocessor.usage.output === "stdout") { + console.log(indent(usageOutput, { count: 2 })); + } else { + const usagePath = ensureIsAbsolute( + config.projectRoot, + preprocessor.usage.output, + ); + + await fs.mkdir(path.dirname(usagePath), { recursive: true }); + + await fs.writeFile(usagePath, usageOutput); + } + } } export async function beforeSpecHandler( diff --git a/lib/preprocessor-configuration.test.ts b/lib/preprocessor-configuration.test.ts index c98bce9c..6d58fd4c 100644 --- a/lib/preprocessor-configuration.test.ts +++ b/lib/preprocessor-configuration.test.ts @@ -751,6 +751,25 @@ describe("resolve()", () => { }); }); + describe("dryRun", () => { + const getValueFn = ( + configuration: IPreprocessorConfiguration, + ): boolean => configuration.dryRun; + + const setValueFn = ( + configuration: IBaseUserConfiguration, + value: boolean, + ) => (configuration.dryRun = value); + + basicBooleanExample({ + testingType, + default: false, + environmentKey: "dryRun", + getValueFn, + setValueFn, + }); + }); + describe("isTrackingState", () => { const getValueFn = ( configuration: IPreprocessorConfiguration, diff --git a/lib/preprocessor-configuration.ts b/lib/preprocessor-configuration.ts index 2d111e5e..0111454b 100644 --- a/lib/preprocessor-configuration.ts +++ b/lib/preprocessor-configuration.ts @@ -145,6 +145,40 @@ function validateUserConfigurationEntry( }; return { [key]: messagesConfig }; } + case "usage": { + if (typeof value !== "object" || value == null) { + throw new Error( + `Expected an object (usage), but got ${util.inspect(value)}`, + ); + } + if ( + !hasOwnProperty(value, "enabled") || + typeof value.enabled !== "boolean" + ) { + throw new Error( + `Expected a boolean (usage.enabled), but got ${util.inspect( + value.enabled, + )}`, + ); + } + let output: string | undefined; + if (hasOwnProperty(value, "output")) { + if (isString(value.output)) { + output = value.output; + } else { + throw new Error( + `Expected a string (usage.output), but got ${util.inspect( + value.output, + )}`, + ); + } + } + const messagesConfig = { + enabled: value.enabled, + output, + }; + return { [key]: messagesConfig }; + } case "pretty": { if (typeof value !== "object" || value == null) { throw new Error( @@ -192,6 +226,14 @@ function validateUserConfigurationEntry( } return { [key]: value }; } + case "dryRun": { + if (!isBoolean(value)) { + throw new Error( + `Expected a boolean (dryRun), but got ${util.inspect(value)}`, + ); + } + return { [key]: value }; + } case "e2e": return { [key]: validateUserConfiguration(value) }; case "component": @@ -379,6 +421,20 @@ function validateEnvironmentOverrides( } } + if (hasOwnProperty(environment, "dryRun")) { + const { dryRun } = environment; + + if (isBoolean(dryRun)) { + overrides.dryRun = dryRun; + } else if (isString(dryRun)) { + overrides.dryRun = stringToMaybeBoolean(dryRun); + } else { + throw new Error( + `Expected a boolean (dryRun), but got ${util.inspect(dryRun)}`, + ); + } + } + return overrides; } @@ -417,10 +473,13 @@ interface IEnvironmentOverrides { jsonOutput?: string; htmlEnabled?: boolean; htmlOutput?: string; + usageEnabled?: boolean; + usageOutput?: string; prettyEnabled?: boolean; filterSpecsMixedMode?: FilterSpecsMixedMode; filterSpecs?: boolean; omitFiltered?: boolean; + dryRun?: boolean; } export interface IBaseUserConfiguration { @@ -437,12 +496,17 @@ export interface IBaseUserConfiguration { enabled: boolean; output?: string; }; + usage?: { + enabled: boolean; + output?: string; + }; pretty?: { enabled: boolean; }; filterSpecsMixedMode?: FilterSpecsMixedMode; filterSpecs?: boolean; omitFiltered?: boolean; + dryRun?: boolean; } export interface IUserConfiguration extends IBaseUserConfiguration { @@ -464,6 +528,10 @@ export interface IPreprocessorConfiguration { enabled: boolean; output: string; }; + readonly usage: { + enabled: boolean; + output: string; + }; readonly pretty: { enabled: boolean; }; @@ -472,6 +540,7 @@ export interface IPreprocessorConfiguration { readonly omitFiltered: boolean; readonly implicitIntegrationFolder: string; readonly isTrackingState: boolean; + readonly dryRun: boolean; } const DEFAULT_STEP_DEFINITIONS = [ @@ -544,6 +613,19 @@ export function combineIntoConfiguration( "cucumber-messages.ndjson", }; + const usage: IPreprocessorConfiguration["usage"] = { + enabled: + overrides.usageEnabled ?? + specific?.usage?.enabled ?? + unspecific.usage?.enabled ?? + false, + output: + overrides.usageOutput ?? + specific?.usage?.output ?? + unspecific.usage?.output ?? + "stdout", + }; + const usingPrettyReporter = cypress.reporter.endsWith( COMPILED_REPORTER_ENTRYPOINT, ); @@ -580,12 +662,16 @@ export function combineIntoConfiguration( unspecific.omitFiltered ?? false; + const dryRun: IPreprocessorConfiguration["dryRun"] = + overrides.dryRun ?? specific?.dryRun ?? unspecific.dryRun ?? false; + const isTrackingState = (cypress.isTextTerminal ?? false) && (messages.enabled || json.enabled || html.enabled || pretty.enabled || + usage.enabled || usingPrettyReporter); return { @@ -594,11 +680,13 @@ export function combineIntoConfiguration( json, html, pretty, + usage, filterSpecsMixedMode, filterSpecs, omitFiltered, implicitIntegrationFolder, isTrackingState, + dryRun, }; } diff --git a/lib/registry.ts b/lib/registry.ts index b3b928dc..18e495ee 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -10,6 +10,8 @@ import parse from "@cucumber/tag-expressions"; import { IdGenerator } from "@cucumber/messages"; +import path from "path-browserify"; + import { assertAndReturn } from "./helpers/assertions"; import DataTable from "./data_table"; @@ -107,7 +109,7 @@ export class Registry { public stepHooks: IStepHook[] = []; - constructor(private experimentalSourceMap: boolean) { + constructor(private experimentalSourceMap: boolean = true) { this.defineStep = this.defineStep.bind(this); this.runStepDefininition = this.runStepDefininition.bind(this); this.defineParameterType = this.defineParameterType.bind(this); @@ -117,7 +119,27 @@ export class Registry { this.parameterTypeRegistry = new ParameterTypeRegistry(); } - public finalize(newId: IdGenerator.NewId) { + public finalize( + newId: IdGenerator.NewId, + projectRoot: string, + sourcesRelativeTo: string, + ) { + const finalizePosition = (position?: Position) => { + if (position != null) { + console.log("Original source", position.source); + + position.source = path.relative( + projectRoot, + path.join( + sourcesRelativeTo, + // Why does Webpack do this? I have no idea. + position.source.replace(/^webpack:\/\//, ""), + ), + ); + } + return position; + }; + for (const { description, implementation, position } of this .preliminaryStepDefinitions) { if (typeof description === "string") { @@ -128,7 +150,7 @@ export class Registry { this.parameterTypeRegistry, ), implementation, - position, + position: finalizePosition(position), }); } else { this.stepDefinitions.push({ @@ -138,14 +160,15 @@ export class Registry { this.parameterTypeRegistry, ), implementation, - position, + position: finalizePosition(position), }); } } - for (const preliminaryHook of this.preliminaryHooks) { + for (const { position, ...preliminaryHook } of this.preliminaryHooks) { this.caseHooks.push({ id: newId(), + position: finalizePosition(position), ...preliminaryHook, }); } @@ -159,7 +182,7 @@ export class Registry { this.preliminaryStepDefinitions.push({ description, implementation, - position: maybeRetrievePositionFromSourceMap(this.experimentalSourceMap), + position: maybeRetrievePositionFromSourceMap(), }); } @@ -183,7 +206,7 @@ export class Registry { node: parseMaybeTags(options.tags), implementation: fn, keyword: keyword, - position: maybeRetrievePositionFromSourceMap(this.experimentalSourceMap), + position: maybeRetrievePositionFromSourceMap(), order: order ?? DEFAULT_HOOK_ORDER, ...remainingOptions, }); @@ -207,7 +230,7 @@ export class Registry { node: parseMaybeTags(options.tags), implementation: fn, keyword: keyword, - position: maybeRetrievePositionFromSourceMap(this.experimentalSourceMap), + position: maybeRetrievePositionFromSourceMap(), order: order ?? DEFAULT_HOOK_ORDER, ...remainingOptions, }); @@ -229,7 +252,7 @@ export class Registry { this.runHooks.push({ implementation: fn, keyword: keyword, - position: maybeRetrievePositionFromSourceMap(this.experimentalSourceMap), + position: maybeRetrievePositionFromSourceMap(), order: options.order ?? DEFAULT_HOOK_ORDER, }); } @@ -283,6 +306,7 @@ export class Registry { public runStepDefininition( world: Mocha.Context, text: string, + dryRun: boolean, argument?: DataTable | string, ): unknown { const stepDefinition = this.resolveStepDefintion(text); @@ -295,6 +319,10 @@ export class Registry { args.push(argument); } + if (dryRun) { + return; + } + return stepDefinition.implementation.apply(world, args); } diff --git a/lib/subpath-entrypoints/browserify.ts b/lib/subpath-entrypoints/browserify.ts index 56240a9c..f3c06c8e 100644 --- a/lib/subpath-entrypoints/browserify.ts +++ b/lib/subpath-entrypoints/browserify.ts @@ -25,7 +25,12 @@ export default function transform( try { done( null, - await compile(configuration, buffer.toString("utf8"), filepath), + await compile( + configuration, + buffer.toString("utf8"), + filepath, + configuration.projectRoot, + ), ); debug(`compiled ${filepath}`); diff --git a/lib/subpath-entrypoints/esbuild.ts b/lib/subpath-entrypoints/esbuild.ts index a62dc5bc..7103c846 100644 --- a/lib/subpath-entrypoints/esbuild.ts +++ b/lib/subpath-entrypoints/esbuild.ts @@ -1,9 +1,13 @@ import fs from "node:fs/promises"; +import path from "node:path"; + import type esbuild from "esbuild"; import { compile } from "../template"; +import { assertAndReturn } from "../helpers/assertions"; + export function createEsbuildPlugin( configuration: Cypress.PluginConfigOptions, ): esbuild.Plugin { @@ -14,7 +18,17 @@ export function createEsbuildPlugin( const content = await fs.readFile(args.path, "utf8"); return { - contents: await compile(configuration, content, args.path), + contents: await compile( + configuration, + content, + args.path, + path.dirname( + assertAndReturn( + build.initialOptions.outfile, + "Expected to find 'outfile'", + ), + ), + ), loader: "js", }; }); diff --git a/lib/subpath-entrypoints/rollup.ts b/lib/subpath-entrypoints/rollup.ts index c6cd6253..91e4c720 100644 --- a/lib/subpath-entrypoints/rollup.ts +++ b/lib/subpath-entrypoints/rollup.ts @@ -10,7 +10,7 @@ export function createRollupPlugin( async transform(src: string, id: string) { if (/\.feature$/.test(id)) { return { - code: await compile(config, src, id), + code: await compile(config, src, id, config.projectRoot), map: null, }; } diff --git a/lib/subpath-entrypoints/webpack.ts b/lib/subpath-entrypoints/webpack.ts index e378a8fe..5a520b65 100644 --- a/lib/subpath-entrypoints/webpack.ts +++ b/lib/subpath-entrypoints/webpack.ts @@ -5,7 +5,9 @@ import { compile } from "../template"; const loader: LoaderDefinition = function (data) { const callback = this.async(); - compile(this.query as any, data, this.resourcePath).then( + const config: Cypress.PluginConfigOptions = this.query as any; + + compile(config, data, this.resourcePath, config.projectRoot).then( (result) => callback(null, result), (error) => callback(error), ); diff --git a/lib/template.ts b/lib/template.ts index 654925da..f322d31a 100644 --- a/lib/template.ts +++ b/lib/template.ts @@ -35,6 +35,7 @@ export async function compile( configuration: Cypress.PluginConfigOptions, data: string, uri: string, + sourcesRelativeTo: string, ) { configuration = rebuildOriginalConfigObject(configuration); @@ -125,6 +126,8 @@ export async function compile( const prepareRegistryPath = prepareLibPath("helpers", "prepare-registry"); + const dryRun = prepareLibPath("helpers", "dry-run"); + const ensureRelativeToProjectRoot = (path: string) => ensureIsRelative(configuration.projectRoot, path); @@ -142,9 +145,13 @@ export async function compile( ), stepDefinitionPaths: stepDefinitionPaths.map(ensureRelativeToProjectRoot), }, + configuration.projectRoot, + sourcesRelativeTo, + preprocessor.dryRun, ]; return ` + ${preprocessor.dryRun ? `require(${dryRun})` : ""} const { getAndFreeRegistry } = require(${prepareRegistryPath}); const { default: createTests } = require(${createTestsPath}); ${stepDefinitionPaths diff --git a/package.json b/package.json index 596a1b3a..f6710e02 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "cypress-preprocessor" ], "bin": { - "cypress-cucumber-diagnostics": "dist/bin/diagnostics.js", "cucumber-html-formatter": "dist/bin/cucumber-html-formatter.js", "cucumber-json-formatter": "dist/bin/cucumber-json-formatter.js", "cucumber-merge-messages": "dist/bin/cucumber-merge-messages.js" @@ -41,6 +40,7 @@ "dist/**/*.d.ts" ], "scripts": { + "postinstall": "[ \"$PWD\" != \"$INIT_CWD\" ] || patch-package", "clear-dist": "rm -rf dist", "clean-install": "rm -rf node_modules && npm install", "genversion": "genversion --semi --double --es6 lib/version.ts", @@ -78,8 +78,9 @@ "glob": "^10.4.5", "is-path-inside": "^3.0.3", "mocha": "^10.7.0", + "path-browserify": "^1.0.1", "seedrandom": "^3.0.5", - "source-map": "^0.7.4", + "source-map": "^0.6.1", "split": "^1.0.1", "uuid": "^10.0.0" }, @@ -98,6 +99,7 @@ "@types/glob": "^8.1.0", "@types/jsdom": "^21.1.7", "@types/mocha": "^10.0.7", + "@types/path-browserify": "^1.0.3", "@types/pngjs": "^6.0.5", "@types/prettier": "^2.7.3", "@types/seedrandom": "^3.0.8", @@ -111,6 +113,7 @@ "eslint": "^9.8.0", "genversion": "^3.2.0", "jsdom": "^24.1.1", + "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.3.3", "recast": "^0.23.9", @@ -125,13 +128,7 @@ "webpack": "^5.93.0" }, "peerDependencies": { - "cypress": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", - "esbuild": "*" - }, - "peerDependenciesMeta": { - "esbuild": { - "optional": true - } + "cypress": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/patches/@cucumber+cucumber+10.9.0.patch b/patches/@cucumber+cucumber+10.9.0.patch new file mode 100644 index 00000000..c16bc325 --- /dev/null +++ b/patches/@cucumber+cucumber+10.9.0.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/@cucumber/cucumber/lib/formatter/helpers/usage_helpers/index.js b/node_modules/@cucumber/cucumber/lib/formatter/helpers/usage_helpers/index.js +index 35339d7..c6001c0 100644 +--- a/node_modules/@cucumber/cucumber/lib/formatter/helpers/usage_helpers/index.js ++++ b/node_modules/@cucumber/cucumber/lib/formatter/helpers/usage_helpers/index.js +@@ -86,10 +86,7 @@ function buildResult(mapping) { + .map((stepDefinitionId) => { + const { matches, ...rest } = mapping[stepDefinitionId]; + const sortedMatches = matches.sort((a, b) => { +- if (a.duration === b.duration) { +- return a.text < b.text ? -1 : 1; +- } +- return normalizeDuration(b.duration) - normalizeDuration(a.duration); ++ return a.text.localeCompare(b.text); + }); + const result = { matches: sortedMatches, ...rest }; + const durations = matches +@@ -101,7 +98,7 @@ function buildResult(mapping) { + } + return result; + }) +- .sort((a, b) => normalizeDuration(b.meanDuration) - normalizeDuration(a.meanDuration)); ++ .sort((a, b) => a.uri.localeCompare(b.uri)); + } + function getUsage({ stepDefinitions, eventDataCollector, }) { + const mapping = buildMapping({ stepDefinitions, eventDataCollector });