├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yaml │ ├── release-github.yaml │ ├── release-npm-latest.yaml │ ├── release-npm-next.yaml │ ├── site.yaml │ └── stale.yaml ├── .gitignore ├── .mocharc.yml ├── .nycrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── UPGRADING.md ├── api └── index.d.ts ├── bin ├── cucumber-js └── cucumber.js ├── compatibility ├── cck_spec.ts └── features │ ├── attachments │ └── attachments.ts │ ├── cdata │ └── cdata.ts │ ├── data-tables │ └── data-tables.ts │ ├── examples-tables-attachment │ └── examples-tables-attachment.ts │ ├── examples-tables │ └── examples-tables.ts │ ├── hooks-attachment │ └── hooks-attachment.ts │ ├── hooks-conditional │ └── hooks-conditional.ts │ ├── hooks-named │ └── hooks-named.ts │ ├── hooks │ └── hooks.ts │ ├── markdown │ └── markdown.ts │ ├── minimal │ └── minimal.ts │ ├── parameter-types │ └── parameter-types.ts │ ├── pending │ └── pending.ts │ ├── retry │ └── retry.ts │ ├── rules │ └── rules.ts │ ├── skipped │ └── skipped.ts │ ├── stack-traces │ └── stack-traces.ts │ ├── undefined │ └── undefined.ts │ └── unknown-parameter-type │ └── unknown-parameter-type.ts ├── cucumber.json ├── dependency-lint.yml ├── docs ├── cli.md ├── configuration.md ├── custom_formatters.md ├── custom_snippet_syntaxes.md ├── debugging.md ├── deprecations.md ├── dry_run.md ├── esm.md ├── fail_fast.md ├── faq.md ├── filtering.md ├── first-time-contributor-tutorial.md ├── formatters.md ├── images │ ├── html_formatter.png │ ├── logo.svg │ ├── progress.gif │ ├── progress_bar_green.gif │ ├── readme-output.png │ └── summary_green.gif ├── installation.md ├── javascript_api.md ├── older_versions.md ├── parallel.md ├── plugins.md ├── profiles.md ├── rerun.md ├── retro │ ├── 2021 │ │ ├── 07 │ │ │ └── 17.md │ │ └── 08 │ │ │ └── 06.md │ ├── 2022 │ │ └── 01 │ │ │ ├── 14.md │ │ │ └── 21.md │ └── README.md ├── retry.md ├── snippets.md ├── support_files │ ├── api_reference.md │ ├── attachments.md │ ├── data_table_interface.md │ ├── hooks.md │ ├── step_definitions.md │ ├── timeouts.md │ └── world.md └── transpiling.md ├── exports ├── api │ ├── api-extractor.json │ └── report.api.md └── root │ ├── api-extractor.json │ └── report.api.md ├── features ├── ambiguous_step.feature ├── attachments.feature ├── background.feature ├── before_after_all_hook_interfaces.feature ├── before_after_all_hook_timeouts.feature ├── before_after_all_hooks.feature ├── before_after_all_hooks_context.feature ├── before_after_step_hooks.feature ├── cli.feature.md ├── colors.feature ├── core.feature ├── custom_formatter.feature ├── data_tables.feature ├── debug.feature ├── direct_imports.feature ├── doc_string.feature ├── dryrun_mode.feature ├── error_formatting.feature ├── esm.feature ├── exit.feature ├── fail_fast.feature ├── failing_steps.feature ├── fake_time.feature ├── fixtures │ └── formatters │ │ ├── failed.json.ts │ │ ├── failed.message.json.ts │ │ ├── passed-rule.json.ts │ │ ├── passed-rule.message.json.ts │ │ ├── passed-scenario-outline.json.ts │ │ ├── passed-scenario-outline.message.json.ts │ │ ├── passed-scenario.json.ts │ │ ├── passed-scenario.message.json.ts │ │ ├── rejected-pickle.json.ts │ │ ├── rejected-pickle.message.json.ts │ │ ├── retried.json.ts │ │ └── retried.message.json.ts ├── formatter_paths.feature ├── formatters.feature ├── gherkin_parse_failure.feature ├── handling_step_errors.feature ├── hook_interface.feature ├── hook_parameter.feature ├── hook_timeouts.feature ├── hooks.feature ├── html_formatter.feature ├── i18n.feature ├── invalid_installation.feature ├── language.feature ├── loaders.feature ├── multiple_formatters.feature ├── multiple_hooks.feature ├── named_hooks.feature ├── nested_features.feature ├── order.feature ├── parallel.feature ├── parallel_custom_assign.feature ├── parameter_types.feature ├── passing_steps.feature ├── pending_steps.feature ├── profiles.feature ├── publish.feature ├── require_module.feature ├── rerun_formatter.feature ├── rerun_formatter_subfolder.feature ├── retry.feature ├── rule.feature ├── scenario_outlines.feature ├── scope_proxies.feature ├── skipped_steps.feature ├── snippets_formatter.feature ├── stack_traces.feature ├── step_definition_snippets.feature ├── step_definition_snippets_custom_syntax.feature ├── step_definition_snippets_i18n.feature ├── step_definition_snippets_interfaces.feature ├── step_definition_timeouts.feature ├── step_definitions │ ├── cli_steps.ts │ ├── file_steps.ts │ ├── formatter_steps.ts │ ├── install_steps.ts │ ├── message_steps.ts │ ├── parallel_steps.ts │ ├── report_server_steps.ts │ └── usage_json_steps.ts ├── step_wrapper_with_options.feature ├── strict_mode.feature ├── summary_formatter.feature ├── support │ ├── formatter_output_helpers.ts │ ├── helpers.ts │ ├── hooks.ts │ ├── message_helpers.ts │ ├── warn_user_about_enabling_developer_mode.ts │ └── world.ts ├── tagged_hooks.feature ├── target_specific_scenarios_by_line.feature ├── target_specific_scenarios_by_name.feature ├── target_specific_scenarios_by_tag.feature ├── usage_formatter.feature ├── usage_json_formatter.feature ├── world_in_hooks.feature └── world_parameters.feature ├── package-lock.json ├── package.json ├── renovate.json ├── scripts ├── remove-empty-sections-changelog.awk └── update-changelog.sh ├── src ├── api │ ├── convert_configuration.ts │ ├── convert_configuration_spec.ts │ ├── formatters.ts │ ├── gherkin.ts │ ├── index.ts │ ├── load_configuration.ts │ ├── load_configuration_spec.ts │ ├── load_sources.ts │ ├── load_sources_spec.ts │ ├── load_support.ts │ ├── load_support_spec.ts │ ├── plugins.ts │ ├── run_cucumber.ts │ ├── run_cucumber_spec.ts │ ├── support.ts │ ├── test_helpers.ts │ ├── types.ts │ └── wrapper.mjs ├── assemble │ ├── assemble_test_cases.ts │ ├── assemble_test_cases_spec.ts │ ├── index.ts │ └── types.ts ├── cli │ ├── helpers.ts │ ├── helpers_spec.ts │ ├── i18n.ts │ ├── index.ts │ ├── install_validator.ts │ ├── run.ts │ ├── validate_node_engine_version.ts │ └── validate_node_engine_version_spec.ts ├── configuration │ ├── argv_parser.ts │ ├── argv_parser_spec.ts │ ├── check_schema.ts │ ├── default_configuration.ts │ ├── from_file.ts │ ├── from_file_spec.ts │ ├── helpers.ts │ ├── index.ts │ ├── locate_file.ts │ ├── merge_configurations.ts │ ├── merge_configurations_spec.ts │ ├── parse_configuration.ts │ ├── split_format_descriptor.ts │ ├── split_format_descriptor_spec.ts │ ├── types.ts │ └── validate_configuration.ts ├── environment │ ├── console_logger.ts │ ├── index.ts │ ├── make_environment.ts │ └── types.ts ├── filter │ ├── filter_plugin.ts │ ├── index.ts │ └── types.ts ├── filter_stack_trace.ts ├── formatter │ ├── builder.ts │ ├── builder_spec.ts │ ├── builtin │ │ ├── html.ts │ │ ├── index.ts │ │ └── message.ts │ ├── create_stream.ts │ ├── find_class_or_plugin.ts │ ├── fixtures │ │ ├── legacy_esm.mjs │ │ ├── legacy_exports_dot_default.cjs │ │ ├── legacy_module_dot_exports.cjs │ │ ├── plugin_esm.mjs │ │ ├── plugin_exports_dot_default.cjs │ │ └── plugin_module_dot_exports.cjs │ ├── get_color_fns.ts │ ├── helpers │ │ ├── duration_helpers.ts │ │ ├── duration_helpers_spec.ts │ │ ├── event_data_collector.ts │ │ ├── formatters.ts │ │ ├── gherkin_document_parser.ts │ │ ├── gherkin_document_parser_spec.ts │ │ ├── index.ts │ │ ├── issue_helpers.ts │ │ ├── issue_helpers_spec.ts │ │ ├── keyword_type.ts │ │ ├── keyword_type_spec.ts │ │ ├── location_helpers.ts │ │ ├── pickle_parser.ts │ │ ├── step_argument_formatter.ts │ │ ├── summary_helpers.ts │ │ ├── summary_helpers_spec.ts │ │ ├── test_case_attempt_formatter.ts │ │ ├── test_case_attempt_parser.ts │ │ ├── test_case_attempt_parser_spec.ts │ │ └── usage_helpers │ │ │ ├── index.ts │ │ │ └── index_spec.ts │ ├── import_code.ts │ ├── index.ts │ ├── json_formatter.ts │ ├── json_formatter_spec.ts │ ├── progress_bar_formatter.ts │ ├── progress_bar_formatter_spec.ts │ ├── progress_formatter.ts │ ├── progress_formatter_spec.ts │ ├── rerun_formatter.ts │ ├── rerun_formatter_spec.ts │ ├── resolve_implementation.ts │ ├── resolve_implementation_spec.ts │ ├── snippets_formatter.ts │ ├── step_definition_snippet_builder │ │ ├── index.ts │ │ ├── index_spec.ts │ │ ├── javascript_snippet_syntax.ts │ │ ├── javascript_snippet_syntax_spec.ts │ │ └── snippet_syntax.ts │ ├── summary_formatter.ts │ ├── summary_formatter_spec.ts │ ├── usage_formatter.ts │ ├── usage_formatter_spec.ts │ ├── usage_json_formatter.ts │ └── usage_json_formatter_spec.ts ├── index.ts ├── models │ ├── data_table.ts │ ├── data_table_spec.ts │ ├── definition.ts │ ├── gherkin_step_keyword.ts │ ├── step_definition.ts │ ├── test_case_hook_definition.ts │ ├── test_case_hook_definition_spec.ts │ ├── test_run_hook_definition.ts │ ├── test_step_hook_definition.ts │ └── test_step_hook_definition_spec.ts ├── paths │ ├── index.ts │ ├── paths.ts │ ├── paths_spec.ts │ └── types.ts ├── pickle_filter.ts ├── pickle_filter_spec.ts ├── plugin │ ├── events.ts │ ├── index.ts │ ├── plugin_manager.ts │ ├── plugin_manager_spec.ts │ └── types.ts ├── publish │ ├── index.ts │ ├── publish_plugin.ts │ └── types.ts ├── runtime │ ├── attachment_manager │ │ ├── index.ts │ │ └── index_spec.ts │ ├── coordinator.ts │ ├── format_error.ts │ ├── format_error_spec.ts │ ├── helpers.ts │ ├── helpers_spec.ts │ ├── index.ts │ ├── make_runtime.ts │ ├── parallel │ │ ├── README.md │ │ ├── adapter.ts │ │ ├── run_worker.ts │ │ ├── types.ts │ │ └── worker.ts │ ├── run_test_run_hooks.ts │ ├── scope │ │ ├── index.ts │ │ ├── make_proxy.ts │ │ ├── test_case_scope.ts │ │ ├── test_case_scope_spec.ts │ │ ├── test_run_scope.ts │ │ └── test_run_scope_spec.ts │ ├── serial │ │ └── adapter.ts │ ├── step_runner.ts │ ├── stopwatch.ts │ ├── stopwatch_spec.ts │ ├── test_case_runner.ts │ ├── test_case_runner_spec.ts │ ├── types.ts │ └── worker.ts ├── step_arguments.ts ├── support_code_library_builder │ ├── build_parameter_type.ts │ ├── context.ts │ ├── get_definition_line_and_uri.ts │ ├── get_definition_line_and_uri_spec.ts │ ├── index.ts │ ├── index_spec.ts │ ├── parallel_can_assign_helpers.ts │ ├── parallel_can_assign_helpers_spec.ts │ ├── sourced_parameter_type_registry.ts │ ├── types.ts │ ├── validate_arguments.ts │ └── world.ts ├── time.ts ├── time_spec.ts ├── try_require.ts ├── types │ ├── assertion-error-formatter │ │ └── index.d.ts │ ├── index.ts │ ├── is-generator │ │ └── index.d.ts │ ├── knuth-shuffle-seeded │ │ └── index.d.ts │ ├── stack-chain │ │ └── index.d.ts │ └── supports-color │ │ └── index.d.ts ├── uncaught_exception_manager.ts ├── user_code_runner.ts ├── user_code_runner_spec.ts ├── value_checker.ts └── wrapper.mjs ├── test-d ├── api.ts ├── attachments.ts ├── hooks.ts ├── parallel.ts ├── steps.ts └── world.ts ├── test ├── fake_logger.ts ├── fake_report_server.ts ├── fixtures │ ├── json_formatter_steps.ts │ ├── steps.ts │ └── usage_steps.ts ├── formatter_helpers.ts ├── gherkin_helpers.ts ├── runtime_helpers.ts └── test_helper.ts ├── tsconfig.json ├── tsconfig.node.json └── typedoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # 2 space indentation 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - 'eslint:recommended' 3 | - 'plugin:import/typescript' 4 | - 'plugin:@typescript-eslint/eslint-recommended' 5 | - 'plugin:@typescript-eslint/recommended' 6 | - prettier 7 | parser: '@typescript-eslint/parser' 8 | parserOptions: 9 | project: './tsconfig.json' 10 | plugins: 11 | - import 12 | - n 13 | - unicorn 14 | - '@typescript-eslint' 15 | rules: 16 | 'no-console': 'error' 17 | 'import/order': 'error' 18 | 'unicorn/prefer-node-protocol': 'error' 19 | # requires strictNullChecks compiler option, produces many errors with messages objects 20 | '@typescript-eslint/strict-boolean-expressions': off 21 | '@typescript-eslint/no-explicit-any': off 22 | '@typescript-eslint/no-inferrable-types': off 23 | '@typescript-eslint/no-empty-function': off 24 | '@typescript-eslint/ban-types': off 25 | '@typescript-eslint/no-unused-vars': [error, { 'argsIgnorePattern': '^_' }] 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.gif binary 3 | *.png binary 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: cucumber 2 | github: cucumber 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | - windows-latest 22 | # these versions must be kept in sync with enginesTested.node in package.json 23 | node-version: [18.x, 20.x, 22.x, 23.x] 24 | fail-fast: false 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: with Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | - run: npm install-test 33 | 34 | coverage: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: 22.x 41 | - run: npm install 42 | - run: npm run test-coverage 43 | - uses: coverallsapp/github-action@main 44 | with: 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | audit-dependencies: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/setup-node@v4 52 | with: 53 | node-version: 22.x 54 | - run: npm audit --groups dependencies --audit-level high 55 | -------------------------------------------------------------------------------- /.github/workflows/release-github.yaml: -------------------------------------------------------------------------------- 1 | name: Release GitHub 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | create-github-release: 9 | name: Create GitHub Release and Git tag 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cucumber/action-create-github-release@v1.1.1 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release-npm-latest.yaml: -------------------------------------------------------------------------------- 1 | name: Release NPM (latest) 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'release/*' 7 | - '!release/*-rc*' 8 | 9 | jobs: 10 | publish-npm: 11 | name: Publish NPM module 12 | runs-on: ubuntu-latest 13 | environment: Release 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '22' 19 | cache: 'npm' 20 | cache-dependency-path: package-lock.json 21 | - run: npm install-test 22 | - uses: cucumber/action-publish-npm@v1.1.1 23 | with: 24 | npm-token: ${{ secrets.NPM_TOKEN }} 25 | npm-tag: 'latest' 26 | -------------------------------------------------------------------------------- /.github/workflows/release-npm-next.yaml: -------------------------------------------------------------------------------- 1 | name: Release NPM (next) 2 | 3 | on: 4 | push: 5 | branches: [release/*-rc*] 6 | 7 | jobs: 8 | publish-npm: 9 | name: Publish NPM module 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '22' 17 | cache: 'npm' 18 | cache-dependency-path: package-lock.json 19 | - run: npm install-test 20 | - uses: cucumber/action-publish-npm@v1.1.1 21 | with: 22 | npm-token: ${{ secrets.NPM_TOKEN }} 23 | npm-tag: 'next' 24 | -------------------------------------------------------------------------------- /.github/workflows/site.yaml: -------------------------------------------------------------------------------- 1 | name: Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | upload-artifact: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 22.6 16 | - run: npm install 17 | - run: npm run exports-generate-docs 18 | - run: | 19 | chmod -c -R +rX "_site/" | while read line; do 20 | echo "::warning title=Invalid file permissions automatically fixed::$line" 21 | done 22 | - uses: actions/upload-pages-artifact@v3 23 | 24 | deploy: 25 | needs: upload-artifact 26 | permissions: 27 | pages: write 28 | id-token: write 29 | environment: 30 | name: github-pages 31 | url: ${{ steps.deployment.outputs.page_url }} 32 | runs-on: ubuntu-latest 33 | steps: 34 | - id: deployment 35 | uses: actions/deploy-pages@v4 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | 3 | permissions: 4 | issues: write 5 | pull-requests: write 6 | 7 | on: 8 | schedule: 9 | - cron: '30 1 * * *' 10 | 11 | jobs: 12 | incomplete-issues: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | stale-issue-label: ':hourglass: stale' 18 | stale-issue-message: 'This issue is stale because it has been open for 3 weeks with no activity. Remove the stale label or comment or this will be closed in another 5 days.' 19 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 20 | only-issue-labels: ':baby_bottle: incomplete' 21 | days-before-stale: 21 22 | days-before-close: 5 23 | days-before-pr-stale: -1 24 | days-before-pr-close: -1 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nyc_output_feature/ 3 | .nyc_output_unit/ 4 | .nyc_output/ 5 | _site/ 6 | @rerun.txt 7 | coverage/ 8 | lib/ 9 | node_modules 10 | tmp/ 11 | reports/ 12 | yarn-error.log 13 | .vscode 14 | .DS_Store 15 | src/version.ts 16 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | colors: true 2 | fail-zero: true 3 | file: 4 | - test/test_helper.ts 5 | full-trace: true 6 | forbid-only: true 7 | forbid-pending: true 8 | recursive: true 9 | reporter: dot 10 | require: 'ts-node/register' 11 | timeout: 3000 12 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMap": true, 3 | "instrument": true, 4 | "extension": [".ts"], 5 | "exclude": [ 6 | "**/*_spec.ts", 7 | "compatibility/**/*.ts", 8 | "cucumber.js", 9 | "features/**/*.ts", 10 | "test/**/*.ts", 11 | "tmp/**/*.js" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | bin 2 | docs 3 | exports 4 | lib 5 | node_modules 6 | reports 7 | tmp 8 | 9 | CHANGELOG.md 10 | CONTRIBUTING.md 11 | README.md 12 | UPGRADING.md 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Julien Biezemans and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | See [.github/RELEASING](https://github.com/cucumber/.github/blob/main/RELEASING.md). 2 | -------------------------------------------------------------------------------- /api/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | allows TypeScript to see `@cucumber/cucumber/api` where it doesn't yet support 3 | subpath exports, see 4 | */ 5 | 6 | export * from '../lib/api' 7 | -------------------------------------------------------------------------------- /bin/cucumber-js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/run.js').default(); 4 | -------------------------------------------------------------------------------- /bin/cucumber.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/run.js').default(); 4 | -------------------------------------------------------------------------------- /compatibility/features/cdata/cdata.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given } from '../../../src' 3 | 4 | Given( 5 | 'I have {int} in my belly', 6 | function (cukeCount: number) { 7 | assert(cukeCount) 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /compatibility/features/data-tables/data-tables.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { When, Then, DataTable } from '../../../src' 3 | 4 | type World = { 5 | transposed: DataTable 6 | } 7 | 8 | When( 9 | 'the following table is transposed:', 10 | function (this: World, table: DataTable) { 11 | this.transposed = table.transpose() 12 | } 13 | ) 14 | 15 | Then('it should be:', function (this: World, expected: DataTable) { 16 | expect(this.transposed.raw()).to.deep.eq(expected.raw()) 17 | }) 18 | -------------------------------------------------------------------------------- /compatibility/features/examples-tables-attachment/examples-tables-attachment.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { When } from '../../../src' 4 | 5 | When('a JPEG image is attached', async function () { 6 | await this.attach( 7 | fs.createReadStream( 8 | path.join( 9 | process.cwd(), 10 | 'node_modules', 11 | '@cucumber', 12 | 'compatibility-kit', 13 | 'features', 14 | 'attachments', 15 | 'cucumber.jpeg' 16 | ) 17 | ), 18 | 'image/jpeg' 19 | ) 20 | }) 21 | 22 | When('a PNG image is attached', async function () { 23 | await this.attach( 24 | fs.createReadStream( 25 | path.join( 26 | process.cwd(), 27 | 'node_modules', 28 | '@cucumber', 29 | 'compatibility-kit', 30 | 'features', 31 | 'attachments', 32 | 'cucumber.png' 33 | ) 34 | ), 35 | 'image/png' 36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /compatibility/features/examples-tables/examples-tables.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given, When, Then } from '../../../src' 3 | 4 | Given('there are {int} cucumbers', function (initialCount: number) { 5 | this.count = initialCount 6 | }) 7 | 8 | Given('there are {int} friends', function (initialFriends: number) { 9 | this.friends = initialFriends 10 | }) 11 | 12 | When('I eat {int} cucumbers', function (eatCount: number) { 13 | this.count -= eatCount 14 | }) 15 | 16 | Then('I should have {int} cucumbers', function (expectedCount: number) { 17 | assert.strictEqual(this.count, expectedCount) 18 | }) 19 | 20 | Then('each person can eat {int} cucumbers', function (expectedShare) { 21 | const share = Math.floor(this.count / (1 + this.friends)) 22 | assert.strictEqual(share, expectedShare) 23 | }) 24 | -------------------------------------------------------------------------------- /compatibility/features/hooks-attachment/hooks-attachment.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { Before, When, After } from '../../../src' 4 | 5 | Before(async function () { 6 | await this.attach( 7 | fs.createReadStream( 8 | path.join( 9 | process.cwd(), 10 | 'node_modules', 11 | '@cucumber', 12 | 'compatibility-kit', 13 | 'features', 14 | 'hooks-attachment', 15 | 'cucumber.svg' 16 | ) 17 | ), 18 | 'image/svg+xml' 19 | ) 20 | }) 21 | 22 | When('a step passes', function () { 23 | // no-op 24 | }) 25 | 26 | After(async function () { 27 | await this.attach( 28 | fs.createReadStream( 29 | path.join( 30 | process.cwd(), 31 | 'node_modules', 32 | '@cucumber', 33 | 'compatibility-kit', 34 | 'features', 35 | 'hooks-attachment', 36 | 'cucumber.svg' 37 | ) 38 | ), 39 | 'image/svg+xml' 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /compatibility/features/hooks-conditional/hooks-conditional.ts: -------------------------------------------------------------------------------- 1 | import { Before, When, After } from '../../../src' 2 | 3 | Before('@passing-hook', async function () { 4 | // no-op 5 | }) 6 | 7 | Before('@fail-before', function () { 8 | throw new Error('Exception in conditional hook') 9 | }) 10 | 11 | When('a step passes', function () { 12 | // no-op 13 | }) 14 | 15 | After('@fail-after', function () { 16 | throw new Error('Exception in conditional hook') 17 | }) 18 | 19 | After('@passing-hook', async function () { 20 | // no-op 21 | }) 22 | -------------------------------------------------------------------------------- /compatibility/features/hooks-named/hooks-named.ts: -------------------------------------------------------------------------------- 1 | import { Before, When, After } from '../../../src' 2 | 3 | Before({ name: 'A named before hook' }, function () { 4 | // no-op 5 | }) 6 | 7 | When('a step passes', function () { 8 | // no-op 9 | }) 10 | 11 | After({ name: 'A named after hook' }, function () { 12 | // no-op 13 | }) 14 | -------------------------------------------------------------------------------- /compatibility/features/hooks/hooks.ts: -------------------------------------------------------------------------------- 1 | import { When, Before, After } from '../../../src' 2 | 3 | Before(function () { 4 | // no-op 5 | }) 6 | 7 | When('a step passes', function () { 8 | // no-op 9 | }) 10 | 11 | When('a step fails', function () { 12 | throw new Error('Exception in step') 13 | }) 14 | 15 | After(function () { 16 | // no-op 17 | }) 18 | -------------------------------------------------------------------------------- /compatibility/features/markdown/markdown.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given, DataTable, Then, When, World } from '../../../src' 3 | 4 | Given('some TypeScript code:', function (dataTable: DataTable) { 5 | assert(dataTable) 6 | }) 7 | 8 | Given('some classic Gherkin:', function (gherkin: string) { 9 | assert(gherkin) 10 | }) 11 | 12 | When( 13 | 'we use a data table and attach something and then {word}', 14 | async function (this: World, word: string, dataTable: DataTable) { 15 | assert(dataTable) 16 | await this.log(`We are logging some plain text (${word})`) 17 | if (word === 'fail') { 18 | throw new Error('You asked me to fail') 19 | } 20 | } 21 | ) 22 | 23 | Then('this might or might not run', function () { 24 | // no-op 25 | }) 26 | -------------------------------------------------------------------------------- /compatibility/features/minimal/minimal.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given } from '../../../src' 3 | 4 | Given('I have {int} cukes in my belly', function (cukeCount: number) { 5 | assert(cukeCount) 6 | }) 7 | -------------------------------------------------------------------------------- /compatibility/features/parameter-types/parameter-types.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given, defineParameterType } from '../../../src' 3 | 4 | class Flight { 5 | constructor( 6 | public readonly from: string, 7 | public readonly to: string 8 | ) {} 9 | } 10 | 11 | defineParameterType({ 12 | name: 'flight', 13 | regexp: /([A-Z]{3})-([A-Z]{3})/, 14 | transformer(from: string, to: string) { 15 | return new Flight(from, to) 16 | }, 17 | }) 18 | 19 | Given('{flight} has been delayed', function (flight: Flight) { 20 | assert.strictEqual(flight.from, 'LHR') 21 | assert.strictEqual(flight.to, 'CDG') 22 | }) 23 | -------------------------------------------------------------------------------- /compatibility/features/pending/pending.ts: -------------------------------------------------------------------------------- 1 | import { Given } from '../../../src' 2 | 3 | Given('an implemented non-pending step', function () { 4 | // no-op 5 | }) 6 | 7 | Given('an implemented step that is skipped', function () { 8 | // no-op 9 | }) 10 | 11 | Given('an unimplemented pending step', function () { 12 | return 'pending' 13 | }) 14 | -------------------------------------------------------------------------------- /compatibility/features/retry/retry.ts: -------------------------------------------------------------------------------- 1 | import { Given } from '../../../src' 2 | 3 | Given('a step that always passes', function () { 4 | // no-op 5 | }) 6 | 7 | let secondTimePass = 0 8 | Given('a step that passes the second time', function () { 9 | secondTimePass++ 10 | if (secondTimePass < 2) { 11 | throw new Error('Exception in step') 12 | } 13 | }) 14 | 15 | let thirdTimePass = 0 16 | Given('a step that passes the third time', function () { 17 | thirdTimePass++ 18 | if (thirdTimePass < 3) { 19 | throw new Error('Exception in step') 20 | } 21 | }) 22 | 23 | Given('a step that always fails', function () { 24 | throw new Error('Exception in step') 25 | }) 26 | -------------------------------------------------------------------------------- /compatibility/features/rules/rules.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { Given, When, Then } from '../../../src' 3 | 4 | Given('the customer has {int} cents', function (money) { 5 | this.money = money 6 | }) 7 | 8 | Given('there are chocolate bars in stock', function () { 9 | this.stock = ['Mars'] 10 | }) 11 | 12 | Given('there are no chocolate bars in stock', function () { 13 | this.stock = [] 14 | }) 15 | 16 | When('the customer tries to buy a {int} cent chocolate bar', function (price) { 17 | if (this.money >= price) { 18 | this.chocolate = this.stock.pop() 19 | } 20 | }) 21 | 22 | Then('the sale should not happen', function () { 23 | assert.strictEqual(this.chocolate, undefined) 24 | }) 25 | 26 | Then('the sale should happen', function () { 27 | assert.ok(this.chocolate) 28 | }) 29 | -------------------------------------------------------------------------------- /compatibility/features/skipped/skipped.ts: -------------------------------------------------------------------------------- 1 | import { Before, Given } from '../../../src' 2 | 3 | Before('@skip', function () { 4 | return 'skipped' 5 | }) 6 | 7 | Given('a step that does not skip', function () { 8 | // no-op 9 | }) 10 | 11 | Given('a step that is skipped', function () { 12 | // no-op 13 | }) 14 | 15 | Given('I skip a step', function () { 16 | return 'skipped' 17 | }) 18 | -------------------------------------------------------------------------------- /compatibility/features/stack-traces/stack-traces.ts: -------------------------------------------------------------------------------- 1 | import { When } from '../../../src' 2 | 3 | When('a step throws an exception', function () { 4 | throw new Error('BOOM') 5 | }) 6 | -------------------------------------------------------------------------------- /compatibility/features/undefined/undefined.ts: -------------------------------------------------------------------------------- 1 | import { Given } from '../../../src' 2 | 3 | Given('an implemented step', function () { 4 | // no-op 5 | }) 6 | 7 | Given('a step that will be skipped', function () { 8 | // no-op 9 | }) 10 | -------------------------------------------------------------------------------- /compatibility/features/unknown-parameter-type/unknown-parameter-type.ts: -------------------------------------------------------------------------------- 1 | import { Given } from '../../../src' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | Given('{airport} is closed because of a strike', function (airport: unknown) { 5 | throw new Error('Should not be called because airport type not defined') 6 | }) 7 | -------------------------------------------------------------------------------- /cucumber.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "requireModule": ["ts-node/register"], 4 | "require": ["features/**/*.ts"], 5 | "format": [ 6 | "progress-bar", 7 | ["rerun", "@rerun.txt"], 8 | ["usage", "reports/usage.txt"], 9 | ["message", "reports/messages.ndjson"], 10 | ["junit", "reports/junit.xml"], 11 | ["html", "reports/html-formatter.html"] 12 | ], 13 | "retry": 2, 14 | "retryTagFilter": "@flaky" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/custom_snippet_syntaxes.md: -------------------------------------------------------------------------------- 1 | # Custom Snippet Syntaxes 2 | 3 | * See the [JavaScript syntax](/src/formatter/step_definition_snippet_builder/javascript_snippet_syntax.ts) and the [custom snippet syntax](/features/step_definition_snippets_custom_syntax.feature) for examples. 4 | * Arguments passed to the constructor: 5 | * `snippetInterface` - string equal to one of the following: 'async-await', 'callback', 'generator', 'promise', or 'synchronous' 6 | * Arguments passed to `build` method: 7 | * An object with the following keys: 8 | * `comment`: a comment to be placed at the top of the function 9 | * `functionName`: the function name to use for the snippet 10 | * `generatedExpressions`: from [cucumber-expressions](https://github.com/cucumber/cucumber-expressions#readme). In most cases will be an array of length 1. But there may be multiple. If multiple, please follow the behavior of the javascript syntax in presenting each of them. See the "multiple patterns" test in this [file](/src/formatter/step_definition_snippet_builder/javascript_snippet_syntax_spec.ts). 11 | * `stepParameterNames`: names for the doc string or data table parameter when applicable. Theses should be appended to the parameter names of each generated expressions. 12 | * Please add the keywords `cucumber` and `snippets` to your package, so it can easily be found by searching [npm](https://www.npmjs.com/search?q=cucumber+snippets). 13 | * Please open an issue if you would like more information. 14 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | If things aren't working the way you expect with Cucumber, you can enable debug logging. When this is enabled, Cucumber will emit logging to `stderr` relating to configuration and other things that can help you pin down a problem with your project. 4 | 5 | Cucumber uses the [popular `debug` library](https://www.npmjs.com/package/debug) to detect when debug logging should be enabled, under the `cucumber` scope. To enable debug logging, set the `DEBUG` environment variable, like: 6 | 7 | ```shell 8 | DEBUG=cucumber 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/dry_run.md: -------------------------------------------------------------------------------- 1 | # Dry run 2 | 3 | You can run cucumber-js in "Dry Run" mode: 4 | 5 | - In a configuration file `{ dryRun: true }` 6 | - On the CLI `cucumber-js --dry-run` 7 | 8 | The effect is that Cucumber will still do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests. Specifically: 9 | 10 | - No [hooks](./support_files/hooks.md) are executed 11 | - Steps are reported as "skipped" instead of being executed 12 | - Undefined and ambiguous steps are reported, but don't cause the process to fail 13 | 14 | A few examples where this is useful: 15 | 16 | - Finding unused step definitions with the [usage formatter](./formatters.md#usage) 17 | - Generating [snippets](./snippets.md) for all undefined steps with the [snippets formatter](./formatters.md#snippets) 18 | - Checking if your path, tag expression etc matches the scenarios you expect it to 19 | 20 | -------------------------------------------------------------------------------- /docs/esm.md: -------------------------------------------------------------------------------- 1 | # ES Modules (experimental) 2 | 3 | You can optionally write your support code (steps, hooks, etc) with native ES modules syntax - i.e. using `import` and `export` statements without transpiling to CommonJS. 4 | 5 | If your support code is written as ESM, you'll need to use the `import` configuration option to specify your files, rather than the `require` option, although we do automatically detect and import any `.mjs` files found within your features directory. 6 | 7 | Example: 8 | 9 | ```javascript 10 | // features/support/steps.mjs 11 | import { Given, When, Then } from '@cucumber/cucumber' 12 | import { strict as assert } from 'assert' 13 | 14 | Given('a variable set to {int}', function (number) { 15 | this.setTo(number) 16 | }) 17 | 18 | When('I increment the variable by {int}', function (number) { 19 | this.incrementBy(number) 20 | }) 21 | 22 | Then('the variable should contain {int}', function (number) { 23 | assert.equal(this.variable, number) 24 | }) 25 | ``` 26 | 27 | As well as support code, these things can also be in ES modules syntax: 28 | 29 | - [Configuration files](./configuration.md#files) 30 | - [Custom formatters](./custom_formatters.md) 31 | - [Custom snippets](./custom_snippet_syntaxes.md) 32 | 33 | You can use ES modules selectively/incrementally - so you can have a mixture of CommonJS and ESM in the same project. 34 | 35 | ## Transpiling 36 | 37 | See [Transpiling](./transpiling.md#esm) for how to do just-in-time compilation that outputs ESM. -------------------------------------------------------------------------------- /docs/fail_fast.md: -------------------------------------------------------------------------------- 1 | # Failing Fast 2 | 3 | - In a configuration file `{ failFast: true }` 4 | - On the CLI `cucumber-js --fail-fast` 5 | 6 | By default, Cucumber runs the entire suite and reports all the failures. `failFast` allows a developer workflow where you work on one failure at a time. Combining this feature with rerun files allows you to work through all failures in an efficient manner. 7 | 8 | A note on using in conjunction with [Retry](./retry.md): we consider a test case to have failed if it exhausts retries and still fails, but passed if it passes on a retry having failed previous attempts, so `failFast` does still allow retries to happen. 9 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## The world instance isn’t available in my hooks or step definitions. 4 | 5 | If you are referencing the world instance (which is bound to `this`) in a step definition or hook, then you cannot use ES6 arrow functions. 6 | 7 | Cucumber uses [apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) internally to call your [step definition](./support_files/step_definitions.md) and 8 | [hook](./support_files/hooks.md) functions using the world object as `this`. 9 | 10 | Using `apply` [does not work with arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#call_apply_and_bind), so if you need to reference the world, use a regular `function`. 11 | 12 | ## Why do my definition patterns need to be globally unique instead of unique only within `Given`, `When`, `Then`? 13 | 14 | To encourage a ubiquitous, non-ambiguous domain language. 15 | Using the same language to mean different things is basically the definition of ambiguous. 16 | If you have similar `Given` and `Then` patterns, try adding the word “should” to `Then` patterns. 17 | 18 | ## Why aren't there `BeforeFeature` and `AfterFeature` hooks? 19 | 20 | When Cucumber runs your specifications, scenarios are collected and turned into test cases. The features that those scenarios sit within are not considered in the test run - it's all about scenarios, which should be standalone and not depend on any other scenarios. This is why `Before` and `After` hooks are available at the global and scenario levels but not the feature level. 21 | 22 | If you find yourself wanting to do some setup work at the feature level, consider whether you can move it to the scenario level and make it idempotent. You can target a hook at all scenarios in a feature [with tags](https://cucumber.io/docs/cucumber/api/?lang=javascript#tags). 23 | 24 | ## Why am I seeing `The "from" argument must be of type string. Received type undefined`? 25 | 26 | See [Invalid installations](./installation.md#invalid-installations) 27 | -------------------------------------------------------------------------------- /docs/images/html_formatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js/8cb6578285d907a05f72bdd2a40db67d11277010/docs/images/html_formatter.png -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js/8cb6578285d907a05f72bdd2a40db67d11277010/docs/images/progress.gif -------------------------------------------------------------------------------- /docs/images/progress_bar_green.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js/8cb6578285d907a05f72bdd2a40db67d11277010/docs/images/progress_bar_green.gif -------------------------------------------------------------------------------- /docs/images/readme-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js/8cb6578285d907a05f72bdd2a40db67d11277010/docs/images/readme-output.png -------------------------------------------------------------------------------- /docs/images/summary_green.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-js/8cb6578285d907a05f72bdd2a40db67d11277010/docs/images/summary_green.gif -------------------------------------------------------------------------------- /docs/older_versions.md: -------------------------------------------------------------------------------- 1 | # Older versions 2 | 3 | Here are links back to the documentation for previous major versions of Cucumber: 4 | 5 | * [`9.x`](https://github.com/cucumber/cucumber-js/tree/9.x/docs) 6 | * [`8.x`](https://github.com/cucumber/cucumber-js/tree/8.x/docs) 7 | * [`7.x`](https://github.com/cucumber/cucumber-js/tree/7.x/docs) 8 | * [`6.x`](https://github.com/cucumber/cucumber-js/tree/6.x/docs) 9 | * [`5.x`](https://github.com/cucumber/cucumber-js/tree/5.x/docs) 10 | * [`4.x`](https://github.com/cucumber/cucumber-js/tree/4.x/docs) 11 | * [`3.x`](https://github.com/cucumber/cucumber-js/tree/3.x/docs) 12 | * [`2.x`](https://github.com/cucumber/cucumber-js/tree/2.x/docs) 13 | * [`1.x`](https://github.com/cucumber/cucumber-js/tree/1.x/docs) 14 | -------------------------------------------------------------------------------- /docs/retro/2021/08/06.md: -------------------------------------------------------------------------------- 1 | # 2021-08-06 2 | 3 | This session was streamed live and recorded. Watch the video [here](https://youtu.be/EiqLzBBpjxM). 4 | 5 | ## Who 6 | 7 | * [@artismarti] 8 | * [@16sheep] 9 | * [@mattwynne] (taking these notes) 10 | 11 | ## What happened 12 | 13 | * Discussed what to do. 14 | * Worked from Marju's fork, using our own local machines and VSCode each time we switched driver. 15 | * Re-did the work we did last time on the mobbing machine, using Marju's machine to submit a new PR ([#1764]) cleanly. We learned about `git cherry-pick`. 16 | * Started working through the pre-prepared issue [#1136] in a [branch on Marju's fork](https://github.com/16sheep/cucumber-js/tree/opt-out-print-attachments-1136) 17 | * Arti had to leave a bit early 18 | 19 | ## Insights 20 | 21 | * Preferred working on own machine, not using the live share / mobbing machine. 22 | * Good to prepare. But, wonder if you over-prepare, it doesn't leave space for people to explore and learn. 23 | * Good to have an issue to focus on, and perhaps a failing test (but we could also write this ourselves). 24 | * It would help to share what we're goin g to work on beforehand so people watching can also know ahead of time. Decide now for next session? 25 | * Matt didn't like being in charge of the timer. 26 | * Could use different mechanism for changing turns, like when we make a commit, or when a test is passing? 27 | * More people would be nice. Should we make it easier for people to join? How? How often can we run it? 28 | 29 | ### Actions / decisions 30 | 31 | * Next time, continue with fixing [#1136] 32 | 33 | [@artismarti]: https://github.com/artismarti 34 | [@mattwynne]: https://github.com/mattwynne 35 | [@16sheep]: https://github.com/16sheep 36 | [#1764]: https://github.com/cucumber/cucumber-js/pull/1764 37 | [#1136]: https://github.com/cucumber/cucumber-js/issues/1136 38 | 39 | -------------------------------------------------------------------------------- /docs/retro/2022/01/14.md: -------------------------------------------------------------------------------- 1 | # What we liked: 2 | 3 | * small pieces / small steps 4 | * screensharing - made it easy to switch over, like when Demi left 5 | * liked talking about cars! - personal connection is really important. 6 | * liked the explanation of the World. Hugely valuable for Blaise! Cucumber ecosystem struggles defining these concepts for newcomers. 7 | * A clear objective with many similar, small examples of it. Easy to shift context from one problem to the next, even though they were all slightly different. 8 | * we just kept rolling when Blaise arrived and Demi left. 9 | * Kate coming along even though she didn't feel she had a lot to contribute 10 | 11 | # Anything we would do differently? Puzzles? 12 | 13 | * Took half an hour to get started - setting up the environment, etc. A script to get everything set up beforehand? GitPod? 15 minutes was just spent chatting but we value that! 14 | * Deciding what to work on took some time too - could we do that in Slack beforehand? 15 | * We could log how time is spent. Blaise has acted as "the scribe" beforehand. 16 | * VS Code linting was broken on Matt's machine at least. 17 | * Matt's GPG keychain 18 | * Tests run slowly 19 | 20 | # Actions 21 | 22 | * Kate: be the scribe next week, and spot how we spend our time 23 | * Matt: Fix GPG key and linting thing 24 | * Blaise: Come along next week! -------------------------------------------------------------------------------- /docs/retro/2022/01/21.md: -------------------------------------------------------------------------------- 1 | # 2022-01-21 2 | ## Who 3 | 4 | * Kate Dames 5 | * Blaise Pabon 6 | * Matt Wynne 7 | 8 | ## What happened 9 | 10 | * Kate and Blaise showed up 2 weeks in a row! 11 | * We talked about the docs, and their shortcomings 12 | * We decided we wanted a real example of Cucumber used in practice 13 | * We looked at https://github.com/gothinkster/realworld 14 | * We decided to start work on the thingy to retire inactive contributors (https://github.com/cucumber/commitbit/issues/3) 15 | * We made an example map: https://miro.com/app/board/uXjVOVYhVEw=/?moveToWidget=3458764517125398282&cot=14 16 | * We had this retro 17 | 18 | ## Insights 19 | 20 | * Kate liked that we're using a real-world example. 21 | * Kate liked going through the example mapping looking at actual data in the examples - e.g. Greg, Julien etc. 22 | * Kate liked that we used Miro for collaboration to pin-down our thoughts - park some things for later etc. 23 | * Blaise liked doing example mapping in action, so concretely. Learning the distinctions between a rule / example and a story. 24 | * Blaise: Materials used to teach cucumber don't emphasise the end result (e.g. the intermediate progress towards a goal). Also don't emphasis how you can use a single test to run against multiple implementations. 25 | * Matt: Our docs don't give people enough context as they arrive - straight into the guts of installation without any signposts 26 | * Matt: How much we discovered about a problem I thought was simple. I love example mapping! 27 | * Kate: I wanted to be more actively participating in the miro board. More hands on. Was it a lack of familiarity? Should we have mad more formal mob roles? What are the rules of the example mapping game? 28 | * Kate: It was fun, thank you! 29 | 30 | ## Actions 31 | 32 | * Matt: figure out where we should implement this - in the existing commitbit repo or somewhere else? 33 | * Kate: think about the process and formalising example mapping 34 | -------------------------------------------------------------------------------- /docs/retro/README.md: -------------------------------------------------------------------------------- 1 | # Retrospective notes 2 | 3 | From time to time, we run a mobbing/ensemble session on this codebase. At the end of each session., we 4 | run a brief [retrospective] conversation in an effort to "turn up the good" – Woody Zuill. 5 | 6 | Each retro we run is recorded in this folder as a dated Markdown file. 7 | 8 | e.g. To find the retrospective for 23rd May 2020 you would look in the file `2020/05/23.md` 9 | 10 | We use [retro-tools] to help manage these files. 11 | 12 | [retrospective]: https://www.agilealliance.org/glossary/heartbeatretro/ 13 | [retro-tools]: https://github.com/tooky/retro-tools 14 | -------------------------------------------------------------------------------- /docs/retry.md: -------------------------------------------------------------------------------- 1 | # Retry 2 | 3 | *Note: if you want a mechanism to rerun just the failed scenarios when doing TDD, take a look at [Rerun](./rerun.md) instead.* 4 | 5 | If you have a flaky scenario (e.g. failing 10% of the time for some reason), you can use *Retry* to have Cucumber attempt it multiple times until either it passes or the maximum number of attempts is reached. You enable this via the `retry` configuration option, like this: 6 | 7 | - In a configuration file `{ retry: 1 }` 8 | - On the CLI `cucumber-js --retry 1` 9 | 10 | The number you provide is the number of retries that will be allowed after an initial failure. 11 | 12 | *Note:* Retry isn't recommended for routine use, but can be a good trade-off in some situations where you have a scenario that's too valuable to remove, but it's either not possible or not worth the effort to fix the flakiness. 13 | 14 | Some notes on how Retry works: 15 | 16 | - Only relevant failing scenarios are retried, not the whole test run. 17 | - When a scenario is retried, it runs all hooks and steps again from the start with a fresh [World](./support_files/world.md) - nothing is retained from the failed attempt. 18 | - When a scenario passes on a retry, it's treated as a pass overall in the results, although the details of each attempt are emitted so formatters can access them. 19 | 20 | ## Targeting scenarios 21 | 22 | Using the `retry` option alone would mean every scenario would be allowed multiple attempts - this almost certainly isn't what you want, assuming you have a small set of flaky scenarios. To target just the relevant scenarios, you can provide a [tag expression](https://cucumber.io/docs/cucumber/api/#tag-expressions) via the `retryTagFilter` configuration option, like this: 23 | 24 | - In a configuration file `{ retry: 1, retryTagFilter: '@flaky' }` 25 | - On the CLI `cucumber-js --retry 1 --retry-tag-filter @flaky` 26 | -------------------------------------------------------------------------------- /docs/support_files/data_table_interface.md: -------------------------------------------------------------------------------- 1 | # Data tables 2 | 3 | When steps have a data table, they are passed an object with methods that can be used to access the data. 4 | 5 | - with column headers 6 | - `hashes`: returns an array of objects where each row is converted to an object (column header is the key) 7 | - `rows`: returns the table as a 2-D array, without the first row 8 | - without column headers 9 | - `raw`: returns the table as a 2-D array 10 | - `rowsHash`: returns an object where each row corresponds to an entry (first column is the key, second column is the value) 11 | - `transpose`: returns a new instance with the data transposed 12 | 13 | See this [feature](/features/data_tables.feature) for examples 14 | -------------------------------------------------------------------------------- /docs/support_files/timeouts.md: -------------------------------------------------------------------------------- 1 | # Timeouts 2 | 3 | By default, asynchronous hooks and steps timeout after 5000 milliseconds. 4 | This can be modified globally with: 5 | 6 | ```javascript 7 | var {setDefaultTimeout} = require('@cucumber/cucumber'); 8 | 9 | setDefaultTimeout(60 * 1000); 10 | ``` 11 | 12 | A specific hook's or step's timeout can be set with: 13 | 14 | ```javascript 15 | var {Before, Given} = require('@cucumber/cucumber'); 16 | 17 | Before({timeout: 60 * 1000}, function() { 18 | // Does some slow browser/filesystem/network actions 19 | }); 20 | 21 | Given(/^a slow step$/, {timeout: 60 * 1000}, function() { 22 | // Does some slow browser/filesystem/network actions 23 | }); 24 | ``` 25 | 26 | *Note that you should not call `setDefaultTimeout` from within other support code e.g. a step, hook or your World class; it should be called globally.* 27 | 28 | ## Disable Timeouts 29 | 30 | **DO NOT USE THIS UNLESS ABSOLUTELY NECESSARY** 31 | 32 | Disable timeouts by setting it to -1. 33 | If you use this, you need to implement your own timeout protection. 34 | Otherwise the test suite may end prematurely or hang indefinitely. 35 | The helper `wrapPromiseWithTimeout`, which cucumber-js itself uses to enforce timeouts is available if needed. 36 | 37 | ```javascript 38 | var {Before, Given, wrapPromiseWithTimeout} = require('@cucumber/cucumber'); 39 | 40 | Given('the operation completes within {n} minutes', {timeout: -1}, function(minutes) { 41 | const milliseconds = (minutes + 1) * 60 * 1000 42 | return wrapPromiseWithTimeout(this.verifyOperationComplete(), milliseconds); 43 | }); 44 | ``` 45 | -------------------------------------------------------------------------------- /exports/api/api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/lib/api/index.d.ts", 4 | "compiler": {}, 5 | "apiReport": { 6 | "enabled": true, 7 | "reportFolder": "/exports/api/", 8 | "reportTempFolder": "/exports/api/tmp", 9 | "reportFileName": "report.api.md" 10 | }, 11 | "docModel": { 12 | "enabled": false 13 | }, 14 | "dtsRollup": { 15 | "enabled": false 16 | }, 17 | "messages": { 18 | "extractorMessageReporting": { 19 | "ae-forgotten-export": { 20 | "logLevel": "warning", 21 | "addToApiReportFile": false 22 | } 23 | }, 24 | "tsdocMessageReporting": { 25 | "tsdoc-undefined-tag": { 26 | "logLevel": "none" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /exports/root/api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/lib/index.d.ts", 4 | "compiler": {}, 5 | "apiReport": { 6 | "enabled": true, 7 | "reportFolder": "/exports/root/", 8 | "reportTempFolder": "/exports/root/tmp", 9 | "reportFileName": "report.api.md" 10 | }, 11 | "docModel": { 12 | "enabled": false 13 | }, 14 | "dtsRollup": { 15 | "enabled": false 16 | }, 17 | "messages": { 18 | "extractorMessageReporting": { 19 | "ae-forgotten-export": { 20 | "logLevel": "none", 21 | "addToApiReportFile": false 22 | }, 23 | "ae-missing-release-tag": { 24 | "logLevel": "none" 25 | } 26 | }, 27 | "tsdocMessageReporting": { 28 | "tsdoc-undefined-tag": { 29 | "logLevel": "none" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /features/ambiguous_step.feature: -------------------------------------------------------------------------------- 1 | Feature: Ambiguous Steps 2 | 3 | Scenario: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: a feature name 7 | Scenario: a scenario name 8 | Given a ambiguous step 9 | """ 10 | Given a file named "features/step_definitions/cucumber_steps.js" with: 11 | """ 12 | const {When} = require('@cucumber/cucumber') 13 | 14 | When(/^a ambiguous step$/, function() {}); 15 | When(/^a (.*) step$/, function(status) {}); 16 | """ 17 | When I run cucumber-js with `-f progress` 18 | Then it outputs the text: 19 | """ 20 | A 21 | 22 | Failures: 23 | 24 | 1) Scenario: a scenario name # features/a.feature:2 25 | ✖ Given a ambiguous step 26 | Multiple step definitions match: 27 | /^a ambiguous step$/ - features/step_definitions/cucumber_steps.js:3 28 | /^a (.*) step$/ - features/step_definitions/cucumber_steps.js:4 29 | 30 | 1 scenario (1 ambiguous) 31 | 1 step (1 ambiguous) 32 | 33 | """ 34 | And it fails 35 | -------------------------------------------------------------------------------- /features/before_after_all_hook_timeouts.feature: -------------------------------------------------------------------------------- 1 | Feature: before / after all hook timeouts 2 | 3 | Background: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: 7 | Scenario: 8 | Given a passing step 9 | """ 10 | And a file named "features/step_definitions/steps.js" with: 11 | """ 12 | const {Given} = require('@cucumber/cucumber') 13 | 14 | Given(/^a passing step$/, function() {}); 15 | """ 16 | 17 | Scenario Outline: slow handler timeout 18 | Given a file named "features/support/handlers.js" with: 19 | """ 20 | const {, setDefaultTimeout} = require('@cucumber/cucumber') 21 | 22 | setDefaultTimeout(500) 23 | 24 | (function(callback) { 25 | setTimeout(callback, 1000) 26 | }) 27 | """ 28 | When I run cucumber-js 29 | Then it fails 30 | And the error output contains the text snippets: 31 | | a handler errored, process exiting | 32 | | function timed out, ensure the callback is executed within 500 milliseconds | 33 | | features/support/handlers.js:5 | 34 | 35 | Examples: 36 | | TYPE | 37 | | BeforeAll | 38 | | AfterAll | 39 | 40 | Scenario Outline: slow handlers can increase their timeout 41 | Given a file named "features/supports/handlers.js" with: 42 | """ 43 | const {, setDefaultTimeout} = require('@cucumber/cucumber') 44 | 45 | setDefaultTimeout(500) 46 | 47 | ({timeout: 1500}, function(callback) { 48 | setTimeout(callback, 1000) 49 | }) 50 | """ 51 | When I run cucumber-js 52 | Then it passes 53 | 54 | Examples: 55 | | TYPE | 56 | | BeforeAll | 57 | | AfterAll | 58 | -------------------------------------------------------------------------------- /features/colors.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | Feature: Colors 3 | 4 | As a developer 5 | I want to control when/whether the output includes colors 6 | 7 | Background: 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: 11 | Scenario: 12 | Given a step 13 | """ 14 | And a file named "features/step_definitions/steps.js" with: 15 | """ 16 | const {Given} = require('@cucumber/cucumber') 17 | Given('a step', function() {}) 18 | """ 19 | And a file named "cucumber.json" with: 20 | """ 21 | { "default": { "format": ["summary:summary.out"] } } 22 | """ 23 | 24 | Scenario: no colored output by default for a file stream 25 | When I run cucumber-js 26 | Then the file "summary.out" doesn't contain colors 27 | 28 | Scenario: colored output can be activated with the format option 29 | When I run cucumber-js with `--format-options '{"colorsEnabled":true}'` 30 | Then the file "summary.out" contains colors 31 | 32 | Scenario: colored output can be activated with FORCE_COLOR 33 | When I run cucumber-js with env `FORCE_COLOR=1` 34 | Then the file "summary.out" contains colors 35 | 36 | Scenario: FORCE_COLOR takes precedence over the format option 37 | When I run cucumber-js with arguments `--format-options '{"colorsEnabled":false}'` and env `FORCE_COLOR=1` 38 | Then the file "summary.out" contains colors 39 | -------------------------------------------------------------------------------- /features/debug.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | Feature: debug 3 | 4 | As a Cucumber user 5 | I want to enable debug logging 6 | So that I can troubleshoot issues with my project 7 | 8 | Background: 9 | Given a file named "features/a.feature" with: 10 | """ 11 | Feature: some feature 12 | Scenario: 13 | Given a step passes 14 | When a step passes 15 | Then a step passes 16 | """ 17 | And a file named "features/step_definitions/cucumber_steps.js" with: 18 | """ 19 | const {Given} = require('@cucumber/cucumber') 20 | 21 | Given(/^a step passes$/, function() {}); 22 | """ 23 | 24 | Scenario: 25 | Given my env includes "DEBUG=cucumber" 26 | When I run cucumber-js 27 | Then the error output contains the text: 28 | """ 29 | No configuration file found 30 | """ 31 | 32 | Scenario: 33 | When I run cucumber-js 34 | Then the error output does not contain the text: 35 | """ 36 | No configuration file found 37 | """ 38 | -------------------------------------------------------------------------------- /features/doc_string.feature: -------------------------------------------------------------------------------- 1 | Feature: doc string 2 | 3 | Scenario: as only step definition argument 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: a feature 7 | Scenario: a scenario 8 | Given a doc string step 9 | \"\"\" 10 | The cucumber (Cucumis sativus) is a widely cultivated plant in the gourd family Cucurbitaceae. 11 | \"\"\" 12 | """ 13 | And a file named "features/step_definitions/cucumber_steps.js" with: 14 | """ 15 | const {Given} = require('@cucumber/cucumber') 16 | const assert = require('assert') 17 | 18 | Given(/^a doc string step$/, function(docString) { 19 | assert.equal(docString, "The cucumber (Cucumis sativus) is a widely " + 20 | "cultivated plant in the gourd family Cucurbitaceae.") 21 | }) 22 | """ 23 | When I run cucumber-js 24 | Then it passes 25 | 26 | Scenario: with other step definition arguments 27 | Given a file named "features/a.feature" with: 28 | """ 29 | Feature: a feature 30 | Scenario: a scenario 31 | Given a "doc string" step 32 | \"\"\" 33 | The cucumber (Cucumis sativus) is a widely cultivated plant in the gourd family Cucurbitaceae. 34 | \"\"\" 35 | """ 36 | And a file named "features/step_definitions/cucumber_steps.js" with: 37 | """ 38 | const {Given} = require('@cucumber/cucumber') 39 | const assert = require('assert') 40 | 41 | Given(/^a "([^"]*)" step$/, function(type, docString) { 42 | assert.equal(type, "doc string") 43 | assert.equal(docString, "The cucumber (Cucumis sativus) is a widely " + 44 | "cultivated plant in the gourd family Cucurbitaceae.") 45 | }) 46 | """ 47 | When I run cucumber-js 48 | Then it passes 49 | -------------------------------------------------------------------------------- /features/exit.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | Feature: Exit 3 | 4 | Use `--exit` to exit when the test run finishes without 5 | waiting to the even loop to drain 6 | 7 | Background: 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: 11 | Scenario: 12 | Given a step 13 | """ 14 | Given a file named "features/step_definitions/cucumber_steps.js" with: 15 | """ 16 | const {Given, After} = require('@cucumber/cucumber') 17 | 18 | Given('a step', function() {}) 19 | 20 | After(() => { 21 | setTimeout(() => { 22 | console.log('external process done') 23 | }, 1000) 24 | }) 25 | """ 26 | 27 | Scenario: by default wait for the event loop to drain 28 | When I run cucumber-js 29 | Then the output contains the text: 30 | """ 31 | external process done 32 | """ 33 | 34 | Scenario Outline: exit immediately without waiting for the even loop to drain 35 | When I run cucumber-js with `` 36 | Then the output does not contain the text: 37 | """ 38 | external process done 39 | """ 40 | Examples: 41 | | FLAG | 42 | | --exit | 43 | | --force-exit | 44 | -------------------------------------------------------------------------------- /features/fail_fast.feature: -------------------------------------------------------------------------------- 1 | Feature: Fail fast 2 | 3 | Using the `--fail-fast` flag ends the suite after the first failure 4 | 5 | Scenario: --fail-fast 6 | Given a file named "features/a.feature" with: 7 | """ 8 | Feature: 9 | Scenario: Failing 10 | Given a failing step 11 | 12 | Scenario: Passing 13 | Given a passing step 14 | """ 15 | Given a file named "features/step_definitions/cucumber_steps.js" with: 16 | """ 17 | const {Given} = require('@cucumber/cucumber') 18 | 19 | Given(/^a failing step$/, function() { throw 'fail' }) 20 | Given(/^a passing step$/, function() {}) 21 | """ 22 | When I run cucumber-js with `--fail-fast` 23 | Then scenario "Passing" step "Given a passing step" has status "skipped" 24 | And it fails 25 | -------------------------------------------------------------------------------- /features/fake_time.feature: -------------------------------------------------------------------------------- 1 | Feature: Allow time to be faked by utilities such as sinon.useFakeTimers 2 | Background: Before and After hooks to enable faking time. 3 | Given a file named "features/support/hooks.js" with: 4 | """ 5 | const {After, Before} = require('@cucumber/cucumber') 6 | const sinon = require('sinon') 7 | 8 | Before(function(scenario) { 9 | this.clock = sinon.useFakeTimers() 10 | }) 11 | 12 | After(function(scenario) { 13 | this.clock.restore() 14 | }) 15 | """ 16 | 17 | Scenario: faked time doesn't trigger the test runner timeout 18 | Given a file named "features/passing_steps.feature" with: 19 | """ 20 | Feature: a feature 21 | Scenario: a scenario 22 | Given a faked time step 23 | """ 24 | 25 | Given a file named "features/step_definitions/passing_steps.js" with: 26 | """ 27 | const assert = require('assert') 28 | const {Given} = require('@cucumber/cucumber') 29 | const sinon = require('sinon') 30 | 31 | Given(/^a faked time step$/, function () { 32 | var testFunction = sinon.stub() 33 | setTimeout(testFunction, 10000) 34 | assert(!testFunction.called) 35 | this.clock.tick(10001) 36 | assert(testFunction.called) 37 | }) 38 | """ 39 | When I run cucumber-js 40 | Then it passes 41 | -------------------------------------------------------------------------------- /features/fixtures/formatters/failed.json.ts: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | description: '', 4 | elements: [ 5 | { 6 | description: '', 7 | id: 'a-feature;a-scenario', 8 | keyword: 'Scenario', 9 | line: 2, 10 | name: 'a scenario', 11 | steps: [ 12 | { 13 | arguments: [], 14 | keyword: 'Given ', 15 | line: 3, 16 | name: 'a step', 17 | match: { 18 | location: 'features/step_definitions/steps.js:3', 19 | }, 20 | result: { 21 | status: 'failed', 22 | duration: 0, 23 | error_message: 'Error: my error', 24 | }, 25 | }, 26 | ], 27 | tags: [], 28 | type: 'scenario', 29 | }, 30 | ], 31 | id: 'a-feature', 32 | line: 1, 33 | keyword: 'Feature', 34 | name: 'a feature', 35 | tags: [], 36 | uri: 'features/a.feature', 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /features/fixtures/formatters/passed-rule.json.ts: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | description: '', 4 | elements: [ 5 | { 6 | description: '', 7 | id: 'a-feature;a-rule;an-example', 8 | keyword: 'Example', 9 | line: 3, 10 | name: 'an example', 11 | steps: [ 12 | { 13 | arguments: [], 14 | keyword: 'Given ', 15 | line: 4, 16 | match: { 17 | location: 'features/step_definitions/steps.js:3', 18 | }, 19 | name: 'a step', 20 | result: { 21 | duration: 0, 22 | status: 'passed', 23 | }, 24 | }, 25 | ], 26 | tags: [], 27 | type: 'scenario', 28 | }, 29 | ], 30 | id: 'a-feature', 31 | keyword: 'Feature', 32 | line: 1, 33 | name: 'a feature', 34 | tags: [], 35 | uri: 'features/a.feature', 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /features/fixtures/formatters/passed-scenario.json.ts: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | description: '', 4 | elements: [ 5 | { 6 | description: '', 7 | id: 'a-feature;a-scenario', 8 | keyword: 'Scenario', 9 | line: 2, 10 | name: 'a scenario', 11 | steps: [ 12 | { 13 | arguments: [], 14 | keyword: 'Given ', 15 | line: 3, 16 | match: { 17 | location: 'features/step_definitions/steps.js:3', 18 | }, 19 | name: 'a step', 20 | result: { 21 | duration: 0, 22 | status: 'passed', 23 | }, 24 | }, 25 | ], 26 | tags: [], 27 | type: 'scenario', 28 | }, 29 | ], 30 | id: 'a-feature', 31 | keyword: 'Feature', 32 | line: 1, 33 | name: 'a feature', 34 | tags: [], 35 | uri: 'features/a.feature', 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /features/fixtures/formatters/rejected-pickle.json.ts: -------------------------------------------------------------------------------- 1 | module.exports = [] 2 | -------------------------------------------------------------------------------- /features/fixtures/formatters/retried.json.ts: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | description: '', 4 | elements: [ 5 | { 6 | description: '', 7 | id: 'a-feature;a-scenario', 8 | keyword: 'Scenario', 9 | line: 2, 10 | name: 'a scenario', 11 | steps: [ 12 | { 13 | arguments: [], 14 | keyword: 'Given ', 15 | line: 3, 16 | match: { 17 | location: 'features/step_definitions/steps.js:5', 18 | }, 19 | name: 'a step', 20 | result: { 21 | duration: 0, 22 | status: 'passed', 23 | }, 24 | }, 25 | ], 26 | tags: [], 27 | type: 'scenario', 28 | }, 29 | ], 30 | id: 'a-feature', 31 | keyword: 'Feature', 32 | line: 1, 33 | name: 'a feature', 34 | tags: [], 35 | uri: 'features/a.feature', 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /features/formatter_paths.feature: -------------------------------------------------------------------------------- 1 | Feature: Formatter Paths 2 | 3 | Background: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: some feature 7 | Scenario: some scenario 8 | Given a passing step 9 | """ 10 | And a file named "features/step_definitions/cucumber_steps.js" with: 11 | """ 12 | const {Given} = require('@cucumber/cucumber') 13 | 14 | Given(/^a passing step$/, function() {}) 15 | """ 16 | 17 | Scenario: Relative path 18 | When I run cucumber-js with `-f summary:summary.txt` 19 | Then the file "summary.txt" has the text: 20 | """ 21 | 1 scenario (1 passed) 22 | 1 step (1 passed) 23 | 24 | """ 25 | 26 | Scenario: Absolute path 27 | Given "{{{tmpDir}}}" is an absolute path 28 | When I run cucumber-js with `-f summary:"{{{tmpDir}}}/summary.txt"` 29 | Then the file "{{{tmpDir}}}/summary.txt" has the text: 30 | """ 31 | 1 scenario (1 passed) 32 | 1 step (1 passed) 33 | 34 | """ 35 | 36 | Scenario: Created relative path 37 | When I run cucumber-js with `-f summary:some/long/path/for/reports/summary.txt` 38 | Then the file "some/long/path/for/reports/summary.txt" has the text: 39 | """ 40 | 1 scenario (1 passed) 41 | 1 step (1 passed) 42 | 43 | """ 44 | 45 | -------------------------------------------------------------------------------- /features/gherkin_parse_failure.feature: -------------------------------------------------------------------------------- 1 | Feature: Gherkin parse failure 2 | 3 | As a developer writing features with a gherkin parse error 4 | I want an error message that points me to the file 5 | So that I can quickly fix the issue and move on 6 | 7 | Scenario: URI, line and column are called out in output 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: a feature name 11 | Scenario: a scenario name 12 | Given a step 13 | Parse Error 14 | """ 15 | When I run cucumber-js 16 | Then it fails 17 | And the error output contains the text: 18 | """ 19 | Parse error in "features/a.feature" (4:5) 20 | """ 21 | 22 | Scenario: All parseable sources and all parse errors are emitted 23 | Given a file named "features/a.feature" with: 24 | """ 25 | Parse Error 26 | """ 27 | Given a file named "features/b.feature" with: 28 | """ 29 | Feature: a feature name 30 | Scenario: a scenario name 31 | Given a step 32 | """ 33 | Given a file named "features/c.feature" with: 34 | """ 35 | Parse Error 36 | """ 37 | When I run cucumber-js with `--format message` 38 | Then it fails 39 | And the output contains these types and quantities of message: 40 | | TYPE | COUNT | 41 | | source | 3 | 42 | | gherkinDocument | 1 | 43 | | pickle | 1 | 44 | | parseError | 2 | 45 | 46 | Scenario: Formatters handle the truncated test run 47 | Given a file named "features/a.feature" with: 48 | """ 49 | Feature: a feature name 50 | Scenario: a scenario name 51 | Given a step 52 | Parse Error 53 | """ 54 | When I run cucumber-js with all formatters 55 | Then it fails 56 | -------------------------------------------------------------------------------- /features/handling_step_errors.feature: -------------------------------------------------------------------------------- 1 | Feature: Handling step errors 2 | We should be able to correctly handle arbitrary error objects from steps 3 | This includes objects that are not Errors or not json-serializable 4 | 5 | Scenario: Complex error object passed to callback 6 | Given a file named "features/a.feature" with: 7 | """ 8 | Feature: a 9 | Scenario: b 10 | Given I pass an error to the callback 11 | """ 12 | Given a file named "features/step_definitions/step_definitions.js" with: 13 | """ 14 | const {Given} = require('@cucumber/cucumber') 15 | 16 | Given('I pass an error to the callback', function (cb) { 17 | var unusualErrorObject = {} 18 | unusualErrorObject.member = unusualErrorObject 19 | cb(unusualErrorObject) 20 | }) 21 | """ 22 | When I run cucumber-js 23 | Then scenario "b" step "Given I pass an error to the callback" failed with: 24 | """ 25 | { member: [Circular] } 26 | """ 27 | And it fails 28 | -------------------------------------------------------------------------------- /features/html_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: HTML formatter 2 | 3 | Rule: Attachments except logs are externalised based on the externalAttachments option 4 | 5 | Background: 6 | Given a file named "features/a.feature" with: 7 | """ 8 | Feature: a feature 9 | Scenario: a scenario 10 | Given a step 11 | """ 12 | And a file named "features/steps.js" with: 13 | """ 14 | const {Given, world} = require('@cucumber/cucumber') 15 | 16 | Given('a step', () => { 17 | world.log('Logging some info') 18 | world.link('https://cucumber.io') 19 | world.attach(btoa('Base64 text'), 'base64:text/plain') 20 | world.attach('Plain text', 'text/plain') 21 | }) 22 | """ 23 | 24 | Scenario: Without externalAttachments option 25 | When I run cucumber-js with `--format html:html.out` 26 | Then it passes 27 | And the html formatter output is complete 28 | And the formatter has no externalised attachments 29 | 30 | Scenario: With externalAttachments option 31 | When I run cucumber-js with `--format html:html.out --format-options '{"html":{"externalAttachments":true}}'` 32 | Then it passes 33 | And the html formatter output is complete 34 | And the formatter has these external attachments: 35 | | Base64 text | 36 | | Plain text | -------------------------------------------------------------------------------- /features/i18n.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | Feature: internationalization 3 | 4 | Scenario: view available languages 5 | When I run cucumber-js with `--i18n-languages` 6 | Then the output contains the text: 7 | """ 8 | ISO 639-1 | ENGLISH NAME | NATIVE NAME 9 | af | Afrikaans | Afrikaans 10 | """ 11 | Then the output contains the text: 12 | """ 13 | ja | Japanese | 日本語 14 | """ 15 | 16 | Scenario: invalid iso code 17 | When I run cucumber-js with `--i18n-keywords XX` 18 | Then the error output contains the text: 19 | """ 20 | Unsupported ISO 639-1: XX 21 | """ 22 | And it fails 23 | 24 | Scenario: view language keywords 25 | When I run cucumber-js with `--i18n-keywords ja` 26 | Then it outputs the text: 27 | """ 28 | ENGLISH KEYWORD | NATIVE KEYWORDS 29 | Feature | "フィーチャ", "機能" 30 | Rule | "ルール" 31 | Background | "背景" 32 | Scenario | "シナリオ" 33 | Scenario Outline | "シナリオアウトライン", "シナリオテンプレート", "テンプレ", "シナリオテンプレ" 34 | Examples | "例", "サンプル" 35 | Given | "* ", "前提" 36 | When | "* ", "もし" 37 | Then | "* ", "ならば" 38 | And | "* ", "且つ", "かつ" 39 | But | "* ", "然し", "しかし", "但し", "ただし" 40 | """ 41 | -------------------------------------------------------------------------------- /features/invalid_installation.feature: -------------------------------------------------------------------------------- 1 | Feature: Invalid installations 2 | 3 | @spawn 4 | Scenario: Cucumber exits with an error when running an invalid installation 5 | Given an invalid installation 6 | Given a file named "features/a.feature" with: 7 | """ 8 | Feature: some feature 9 | Scenario: 10 | When a step is passing 11 | """ 12 | And a file named "features/step_definitions/cucumber_steps.js" with: 13 | """ 14 | const {When} = require('@cucumber/cucumber') 15 | 16 | When(/^a step is passing$/, function() {}) 17 | """ 18 | When I run cucumber-js 19 | Then it fails 20 | And the error output contains the text: 21 | """ 22 | You're calling functions (e.g. "When") on an instance of Cucumber that isn't running (status: PENDING). 23 | This means you may have an invalid installation, potentially due to: 24 | - Cucumber being installed globally 25 | - A project structure where your support code is depending on a different instance of Cucumber 26 | Either way, you'll need to address this in order for Cucumber to work. 27 | See https://github.com/cucumber/cucumber-js/blob/main/docs/installation.md#invalid-installations 28 | """ 29 | -------------------------------------------------------------------------------- /features/language.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple Formatters 2 | 3 | Background: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Fonctionnalité: Bonjour 7 | Scénario: Monde 8 | Soit une étape 9 | """ 10 | And a file named "features/step_definitions/cucumber_steps.js" with: 11 | """ 12 | const {Given} = require('@cucumber/cucumber') 13 | 14 | Given(/^une étape$/, function() {}) 15 | """ 16 | 17 | Scenario: Ability to specify multiple formatters 18 | When I run cucumber-js with `--language fr` 19 | Then it outputs the text: 20 | """ 21 | . 22 | 23 | 1 scenario (1 passed) 24 | 1 step (1 passed) 25 | 26 | """ 27 | -------------------------------------------------------------------------------- /features/loaders.feature: -------------------------------------------------------------------------------- 1 | @esm 2 | Feature: ESM loaders support 3 | 4 | I want to nominate one or more loaders to use to transpile my code 5 | So that I can write my source code in another language 6 | As a user 7 | 8 | Background: 9 | Given a file named "features/a.feature" with: 10 | """ 11 | Feature: some feature 12 | Scenario: some scenario 13 | Given a passing step 14 | """ 15 | And a file named "features/steps.ts" with: 16 | """ 17 | import { Given } from '@cucumber/cucumber' 18 | 19 | interface Something { 20 | field1: string 21 | field2: string 22 | } 23 | 24 | Given('a passing step', () => {}) 25 | """ 26 | And a file named "tsconfig.json" with: 27 | """ 28 | { 29 | "compilerOptions": { 30 | "module": "esnext", 31 | "moduleResolution": "nodenext" 32 | } 33 | } 34 | """ 35 | 36 | Scenario: serial runtime 37 | When I run cucumber-js with `--loader ts-node/esm --import features/steps.ts` 38 | Then it passes 39 | 40 | Scenario: parallel runtime 41 | When I run cucumber-js with `--loader ts-node/esm --import features/steps.ts --parallel 1` 42 | Then it passes 43 | 44 | Scenario: local loader 45 | Given a file named "custom-loader.mjs" with: 46 | """ 47 | // no-op loader hook 48 | export async function load(url, context, nextLoad) { 49 | return nextLoad(url); 50 | } 51 | """ 52 | Given a file named "features/steps.mjs" with: 53 | """ 54 | import { Given } from '@cucumber/cucumber' 55 | 56 | Given('a passing step', () => {}) 57 | """ 58 | When I run cucumber-js with `--loader ./custom-loader.mjs` 59 | Then it passes 60 | -------------------------------------------------------------------------------- /features/multiple_formatters.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple Formatters 2 | 3 | Background: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: some feature 7 | Scenario: some scenario 8 | Given a passing step 9 | """ 10 | And a file named "features/step_definitions/cucumber_steps.js" with: 11 | """ 12 | const {Given} = require('@cucumber/cucumber') 13 | 14 | Given(/^a passing step$/, function() {}) 15 | """ 16 | 17 | Scenario: Ability to specify multiple formatters 18 | When I run cucumber-js with `-f progress -f summary:summary.txt` 19 | Then it outputs the text: 20 | """ 21 | . 22 | 23 | 1 scenario (1 passed) 24 | 1 step (1 passed) 25 | 26 | """ 27 | And the file "summary.txt" has the text: 28 | """ 29 | 1 scenario (1 passed) 30 | 1 step (1 passed) 31 | 32 | """ 33 | -------------------------------------------------------------------------------- /features/multiple_hooks.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple Hooks 2 | 3 | Scenario: before hooks run in the order of definition, after hooks in reverse order of definition 4 | Given a file named "features/a.feature" with: 5 | """ 6 | @foo 7 | Feature: 8 | Scenario: 9 | Given a step 10 | 11 | Scenario: 12 | Given a step 13 | """ 14 | And a file named "features/step_definitions/world.js" with: 15 | """ 16 | const {setWorldConstructor} = require('@cucumber/cucumber') 17 | 18 | setWorldConstructor(function() { 19 | this.value = 0 20 | }) 21 | """ 22 | And a file named "features/step_definitions/my_steps.js" with: 23 | """ 24 | const assert = require('assert') 25 | const {Given} = require('@cucumber/cucumber') 26 | 27 | Given('a step', function() {}) 28 | """ 29 | And a file named "features/step_definitions/hooks.js" with: 30 | """ 31 | const {After, Before} = require('@cucumber/cucumber') 32 | const assert = require('assert') 33 | 34 | Before(function() { 35 | assert.equal(this.value, 0) 36 | this.value += 1 37 | }) 38 | 39 | After(function() { 40 | assert.equal(this.value, 3) 41 | }) 42 | 43 | Before({tags: '@foo'}, function() { 44 | assert.equal(this.value, 1) 45 | this.value += 1 46 | }) 47 | 48 | After({tags: '@foo'}, function() { 49 | assert.equal(this.value, 2) 50 | this.value += 1 51 | }) 52 | """ 53 | When I run cucumber-js 54 | Then it passes 55 | -------------------------------------------------------------------------------- /features/named_hooks.feature: -------------------------------------------------------------------------------- 1 | Feature: Named hooks 2 | 3 | As a developer 4 | I want to name a `Before` or `After` hook 5 | So that I can easily identify which hooks are run when reporting 6 | 7 | Scenario: Hook is named and then referenced by its name in formatter output 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: some feature 11 | Scenario: some scenario 12 | Given a step 13 | """ 14 | And a file named "features/step_definitions/hooks.js" with: 15 | """ 16 | const {After, Before} = require('@cucumber/cucumber') 17 | 18 | Before({name: 'hook 1'}, function() {}) 19 | Before({name: 'hook 2'}, function() {}) 20 | After({name: 'hook 3'}, function() {}) 21 | """ 22 | And a file named "features/step_definitions/steps.js" with: 23 | """ 24 | const {Given} = require('@cucumber/cucumber') 25 | 26 | Given('a step', function() { 27 | throw 'nope' 28 | }) 29 | """ 30 | When I run cucumber-js 31 | Then it fails 32 | And the output contains the text: 33 | """ 34 | Before (hook 2) # 35 | """ 36 | -------------------------------------------------------------------------------- /features/nested_features.feature: -------------------------------------------------------------------------------- 1 | Feature: Automatically required support files for nested features 2 | As a developer nesting feature files 3 | I want the default required files to include any files under the features folder 4 | So I don't have to do anything special when I start organizing my features 5 | 6 | Scenario: 7 | Given a directory named "features/nested" 8 | And a file named "features/nested/a.feature" with: 9 | """ 10 | Feature: some feature 11 | Scenario: some scenario 12 | Given a step 13 | """ 14 | And a file named "features/step_definitions/cucumber_steps.js" with: 15 | """ 16 | const {Given} = require('@cucumber/cucumber') 17 | 18 | Given(/^a step$/, function() {}) 19 | """ 20 | When I run cucumber-js 21 | Then it passes 22 | -------------------------------------------------------------------------------- /features/order.feature: -------------------------------------------------------------------------------- 1 | Feature: Set the execution order 2 | 3 | Background: 4 | Given a file named "features/a.feature" with: 5 | """ 6 | Feature: some feature 7 | @a 8 | Scenario: first scenario 9 | Given a step 10 | 11 | @b 12 | Scenario Outline: second scenario - 13 | Given a step 14 | 15 | @c 16 | Examples: 17 | | ID | 18 | | X | 19 | | Y | 20 | 21 | @d 22 | Examples: 23 | | ID | 24 | | Z | 25 | """ 26 | And a file named "features/step_definitions/cucumber_steps.js" with: 27 | """ 28 | const {Given} = require('@cucumber/cucumber') 29 | 30 | Given(/^a step$/, function() {}) 31 | """ 32 | 33 | Scenario: run in defined order scenario 34 | When I run cucumber-js with `--order defined` 35 | Then it runs the scenarios: 36 | | NAME | 37 | | first scenario | 38 | | second scenario - X | 39 | | second scenario - Y | 40 | | second scenario - Z | 41 | 42 | Scenario: run in random order with seed 43 | When I run cucumber-js with `--order random:234119` 44 | Then it runs the scenarios: 45 | | NAME | 46 | | second scenario - Z | 47 | | second scenario - X | 48 | | second scenario - Y | 49 | | first scenario | 50 | -------------------------------------------------------------------------------- /features/passing_steps.feature: -------------------------------------------------------------------------------- 1 | Feature: Passing steps 2 | 3 | Background: 4 | Given a file named "features/passing_steps.feature" with: 5 | """ 6 | Feature: a feature 7 | Scenario: a scenario 8 | Given a passing step 9 | """ 10 | 11 | Scenario: synchronous 12 | Given a file named "features/step_definitions/passing_steps.js" with: 13 | """ 14 | const {Given} = require('@cucumber/cucumber') 15 | 16 | Given(/^a passing step$/, function() {}) 17 | """ 18 | When I run cucumber-js 19 | Then scenario "a scenario" step "Given a passing step" has status "passed" 20 | 21 | Scenario: asynchronous 22 | Given a file named "features/step_definitions/passing_steps.js" with: 23 | """ 24 | const {Given} = require('@cucumber/cucumber') 25 | 26 | Given(/^a passing step$/, function(callback) { 27 | setTimeout(callback) 28 | }) 29 | """ 30 | When I run cucumber-js 31 | Then scenario "a scenario" step "Given a passing step" has status "passed" 32 | 33 | Scenario: promise 34 | Given a file named "features/step_definitions/passing_steps.js" with: 35 | """ 36 | const {Given} = require('@cucumber/cucumber') 37 | 38 | Given(/^a passing step$/, function() { 39 | return Promise.resolve() 40 | }) 41 | """ 42 | When I run cucumber-js 43 | Then scenario "a scenario" step "Given a passing step" has status "passed" 44 | -------------------------------------------------------------------------------- /features/pending_steps.feature: -------------------------------------------------------------------------------- 1 | Feature: Pending steps 2 | 3 | Background: 4 | Given a file named "features/pending.feature" with: 5 | """ 6 | Feature: a feature 7 | Scenario: a scenario 8 | Given a pending step 9 | """ 10 | 11 | Scenario: Synchronous pending step 12 | Given a file named "features/step_definitions/failing_steps.js" with: 13 | """ 14 | const {Given} = require('@cucumber/cucumber') 15 | 16 | Given(/^a pending step$/, function() { 17 | return 'pending' 18 | }) 19 | """ 20 | When I run cucumber-js 21 | Then it fails 22 | And scenario "a scenario" step "Given a pending step" has status "pending" 23 | 24 | 25 | Scenario: Callback pending step 26 | Given a file named "features/step_definitions/failing_steps.js" with: 27 | """ 28 | const {Given} = require('@cucumber/cucumber') 29 | 30 | Given(/^a pending step$/, function(callback) { 31 | callback(null, 'pending') 32 | }) 33 | """ 34 | When I run cucumber-js 35 | Then it fails 36 | And scenario "a scenario" step "Given a pending step" has status "pending" 37 | 38 | Scenario: Promise pending step 39 | Given a file named "features/step_definitions/failing_steps.js" with: 40 | """ 41 | const {Given} = require('@cucumber/cucumber') 42 | 43 | Given(/^a pending step$/, function(){ 44 | return { 45 | then: function(onResolve, onReject) { 46 | setTimeout(function() { 47 | onResolve('pending') 48 | }) 49 | } 50 | } 51 | }) 52 | """ 53 | When I run cucumber-js 54 | Then it fails 55 | And scenario "a scenario" step "Given a pending step" has status "pending" 56 | -------------------------------------------------------------------------------- /features/require_module.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | Feature: compilers 3 | In order to use the JS dialect I'm most comfortable with 4 | As a step definition implementor 5 | I want to use any compiler to write my step definitions in 6 | 7 | 8 | Scenario: CoffeeScript step definition 9 | Given a file named "features/a.feature" with: 10 | """ 11 | Feature: some feature 12 | Scenario: some scenario 13 | Given a step 14 | """ 15 | Given a file named "features/step_definitions/cucumber_steps.coffee" with: 16 | """ 17 | {Given} = require '@cucumber/cucumber' 18 | 19 | Given /^a step$/, -> 20 | """ 21 | When I run cucumber-js with `--require-module coffeescript/register --require 'features/**/*.coffee'` 22 | Then scenario "some scenario" step "Given a step" has status "passed" 23 | -------------------------------------------------------------------------------- /features/rerun_formatter_subfolder.feature: -------------------------------------------------------------------------------- 1 | Feature: Rerun Formatter 2 | 3 | As a developer 4 | I would like to be able to save my rerun results to a subfolder 5 | So that cucumber-js give me flexibility for where I store them 6 | 7 | Scenario: saving @rerun.txt in subfolder 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: A 11 | Scenario: 1 12 | Given a passing step 13 | 14 | Scenario: 2 15 | Given a failing step 16 | """ 17 | And a file named "features/step_definitions/cucumber_steps.js" with: 18 | """ 19 | const {Given} = require('@cucumber/cucumber') 20 | 21 | Given(/^a passing step$/, function() {}) 22 | Given(/^a failing step$/, function() { throw 'fail' }) 23 | """ 24 | And a directory named "test_results" 25 | When I run cucumber-js with `--format rerun:test_results/@rerun.txt` 26 | Then it fails 27 | And the file "test_results/@rerun.txt" has the text: 28 | """ 29 | features/a.feature:5 30 | """ 31 | When I run cucumber-js with `test_results/@rerun.txt` 32 | Then it fails 33 | And it runs the scenario "2" 34 | -------------------------------------------------------------------------------- /features/snippets_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: snippets formatter 2 | 3 | As a developer with undefined steps 4 | I want a formatter which just outputs the snippets 5 | So I can copy and paste all the steps I need to implement 6 | 7 | 8 | Scenario: 9 | Given a file named "features/undefined.feature" with: 10 | """ 11 | Feature: a feature 12 | Scenario: a scenario 13 | Given undefined step A 14 | When undefined step B 15 | Then undefined step C 16 | """ 17 | When I run cucumber-js with `--format snippets` 18 | Then it fails 19 | And it outputs the text: 20 | """ 21 | Given('undefined step A', function () { 22 | // Write code here that turns the phrase above into concrete actions 23 | return 'pending'; 24 | }); 25 | 26 | When('undefined step B', function () { 27 | // Write code here that turns the phrase above into concrete actions 28 | return 'pending'; 29 | }); 30 | 31 | Then('undefined step C', function () { 32 | // Write code here that turns the phrase above into concrete actions 33 | return 'pending'; 34 | }); 35 | """ 36 | -------------------------------------------------------------------------------- /features/stack_traces.feature: -------------------------------------------------------------------------------- 1 | @spawn 2 | @source-mapping 3 | Feature: Stack traces 4 | Background: 5 | Given a file named "features/a.feature" with: 6 | """ 7 | Feature: some feature 8 | Scenario: some scenario 9 | Given a passing step 10 | And a failing step 11 | """ 12 | 13 | Rule: Source maps are respected when dealing with transpiled support code 14 | 15 | Just-in-time transpilers like `@babel/register` and `ts-node` emit source maps with 16 | the transpiled code. Cucumber users expect stack traces to point to the line and column 17 | in the original source file when there is an error. 18 | 19 | Background: 20 | Given a file named "features/steps.ts" with: 21 | """ 22 | import { Given } from '@cucumber/cucumber' 23 | 24 | interface Something { 25 | field1: string 26 | field2: string 27 | } 28 | 29 | Given('a passing step', function() {}) 30 | 31 | Given('a failing step', function() { 32 | throw new Error('boom') 33 | }) 34 | """ 35 | 36 | Scenario: commonjs 37 | When I run cucumber-js with `--require-module ts-node/register --require features/steps.ts` 38 | Then the output contains the text: 39 | """ 40 | /features/steps.ts:11:9 41 | """ 42 | And it fails 43 | 44 | @esm 45 | Scenario: esm 46 | Given a file named "tsconfig.json" with: 47 | """ 48 | { 49 | "compilerOptions": { 50 | "module": "esnext", 51 | "moduleResolution": "nodenext" 52 | } 53 | } 54 | """ 55 | Given my env includes "{\"NODE_OPTIONS\":\"--loader ts-node/esm --enable-source-maps\"}" 56 | When I run cucumber-js with `--import features/steps.ts` 57 | Then the output contains the text: 58 | """ 59 | /features/steps.ts:11:9 60 | """ 61 | And it fails 62 | -------------------------------------------------------------------------------- /features/step_definition_snippets_i18n.feature: -------------------------------------------------------------------------------- 1 | Feature: step definition snippets i18n 2 | 3 | As a developer writing my features in another language 4 | I want my text snippets to reference the translation 5 | 6 | Background: 7 | Given a file named "features/undefined.feature" with: 8 | """ 9 | # language: af 10 | Funksie: a feature 11 | Situasie: a scenario 12 | Gegewe undefined step A 13 | Wanneer undefined step B 14 | Dan undefined step C 15 | """ 16 | 17 | Scenario: 18 | When I run cucumber-js 19 | Then it fails 20 | And the output contains the text: 21 | """ 22 | Given('undefined step A', function () { 23 | // Write code here that turns the phrase above into concrete actions 24 | return 'pending'; 25 | }); 26 | """ 27 | And the output contains the text: 28 | """ 29 | When('undefined step B', function () { 30 | // Write code here that turns the phrase above into concrete actions 31 | return 'pending'; 32 | }); 33 | """ 34 | And the output contains the text: 35 | """ 36 | Then('undefined step C', function () { 37 | // Write code here that turns the phrase above into concrete actions 38 | return 'pending'; 39 | }); 40 | """ 41 | -------------------------------------------------------------------------------- /features/step_definition_snippets_interfaces.feature: -------------------------------------------------------------------------------- 1 | Feature: step definition snippets custom syntax 2 | 3 | As a developer writing my step definitions in another JS dialect 4 | I want to be able to see step definition snippets in the language I prefer 5 | 6 | Background: 7 | Given a file named "features/undefined.feature" with: 8 | """ 9 | Feature: a feature 10 | Scenario: a scenario 11 | Given an undefined step 12 | """ 13 | 14 | Scenario Outline: 15 | When I run cucumber-js with `--format-options '{"snippetInterface": ""}'` 16 | Then it fails 17 | And the output contains the text: 18 | """ 19 | Given('an undefined step', { 20 | // Write code here that turns the phrase above into concrete actions 21 | ; 22 | }); 23 | """ 24 | 25 | Examples: 26 | | INTERFACE | SNIPPET_FUNCTION_KEYWORD_AND_PARAMETERS | SNIPPET_IMPLEMENTATION | 27 | | callback | function (callback) | callback(null, 'pending') | 28 | | promise | function () | return Promise.resolve('pending') | 29 | | async-await | async function () | return 'pending' | 30 | | synchronous | function () | return 'pending' | 31 | -------------------------------------------------------------------------------- /features/step_definitions/report_server_steps.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'node:url' 2 | import assert from 'node:assert' 3 | import { expect } from 'chai' 4 | import { Given, Then, DataTable } from '../..' 5 | import { World } from '../support/world' 6 | import FakeReportServer from '../../test/fake_report_server' 7 | 8 | Given( 9 | 'a report server is running on {string}', 10 | async function (this: World, url: string) { 11 | const port = parseInt(new URL(url).port) 12 | this.reportServer = new FakeReportServer(port) 13 | await this.reportServer.start() 14 | } 15 | ) 16 | 17 | Given('report publishing is not working', async function (this: World) { 18 | this.reportServer.failOnTouch = true 19 | }) 20 | 21 | Given('report uploads are not working', async function (this: World) { 22 | this.reportServer.failOnUpload = true 23 | }) 24 | 25 | Then( 26 | 'the server should receive the following message types:', 27 | async function (this: World, expectedMessageTypesTable: DataTable) { 28 | const expectedMessageTypes = expectedMessageTypesTable 29 | .raw() 30 | .map((row) => row[0]) 31 | 32 | const receivedBodies = await this.reportServer.stop() 33 | const ndjson = receivedBodies.toString('utf-8').trim() 34 | if (ndjson === '') assert.fail('Server received nothing') 35 | 36 | const receivedMessageTypes = ndjson 37 | .split(/\n/) 38 | .map((line) => JSON.parse(line)) 39 | .map((envelope) => Object.keys(envelope)[0]) 40 | 41 | expect(receivedMessageTypes).to.deep.eq(expectedMessageTypes) 42 | } 43 | ) 44 | 45 | Then( 46 | 'the server should receive a(n) {string} header with value {string}', 47 | function (this: World, name: string, value: string) { 48 | expect(this.reportServer.receivedHeaders[name.toLowerCase()]).to.eq(value) 49 | } 50 | ) 51 | -------------------------------------------------------------------------------- /features/step_definitions/usage_json_steps.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { expect } from 'chai' 3 | import { DataTable, Then } from '../../' 4 | import { World } from '../support/world' 5 | import { IUsage } from '../../src/formatter/helpers/usage_helpers' 6 | 7 | Then('it outputs the usage data:', function (this: World, table: DataTable) { 8 | const usageData: IUsage[] = JSON.parse(this.lastRun.output) 9 | table.hashes().forEach((row) => { 10 | const rowUsage = usageData.find( 11 | (datum) => 12 | datum.pattern === row.PATTERN && datum.patternType === row.PATTERN_TYPE 13 | ) 14 | expect(rowUsage).to.be.an('object') 15 | expect(rowUsage.line).to.eql(parseInt(row.LINE)) 16 | expect(rowUsage.matches).to.have.lengthOf(Number(row['NUMBER OF MATCHES'])) 17 | expect(rowUsage.uri).to.eql(path.normalize(row.URI)) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /features/step_wrapper_with_options.feature: -------------------------------------------------------------------------------- 1 | Feature: Step Wrapper with Options 2 | In order to be able to write more complex step definition wrappers 3 | As a developer 4 | I want Cucumber to provide the "options" object to the wrapping function 5 | 6 | @spawn 7 | Scenario: options passed to the step definitions wrapper 8 | Given a file named "features/a.feature" with: 9 | """ 10 | Feature: Step with an option 11 | Scenario: Steps 12 | When I run a step with options 13 | """ 14 | And a file named "features/step_definitions/cucumber_steps.js" with: 15 | """ 16 | const {When} = require('@cucumber/cucumber') 17 | 18 | When(/^I run a step with options$/, {wrapperOptions: {retry: 2}}, function () {}) 19 | """ 20 | And a file named "features/support/setup.js" with: 21 | """ 22 | const {setDefinitionFunctionWrapper} = require('@cucumber/cucumber') 23 | 24 | setDefinitionFunctionWrapper(function (fn, options = {}) { 25 | if (options.retry) { 26 | console.log("Max retries: ", options.retry); 27 | } 28 | return fn; 29 | }) 30 | """ 31 | When I run cucumber-js 32 | Then the output contains the text: 33 | """ 34 | Max retries: 2 35 | """ 36 | -------------------------------------------------------------------------------- /features/strict_mode.feature: -------------------------------------------------------------------------------- 1 | Feature: Strict mode 2 | 3 | Using the `--no-strict` flag will cause cucumber to succeed even if there are 4 | pending steps. 5 | 6 | Background: 7 | Given a file named "features/a.feature" with: 8 | """ 9 | Feature: Missing 10 | Scenario: Missing 11 | Given a step 12 | """ 13 | 14 | Scenario: Fail with pending step by default 15 | Given a file named "features/step_definitions/cucumber_steps.js" with: 16 | """ 17 | const {Given} = require('@cucumber/cucumber') 18 | 19 | Given(/^a step$/, function() { return 'pending' }) 20 | """ 21 | When I run cucumber-js 22 | Then it fails 23 | 24 | Scenario: Succeed with pending step with --no-strict 25 | Given a file named "features/step_definitions/cucumber_steps.js" with: 26 | """ 27 | const {Given} = require('@cucumber/cucumber') 28 | 29 | Given(/^a step$/, function() { return 'pending' }) 30 | """ 31 | When I run cucumber-js with `--no-strict` 32 | Then it passes 33 | -------------------------------------------------------------------------------- /features/summary_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: Summary Formatter 2 | In order to get a quick overview of Cucumber test run 3 | Developers should be able to see a high level summary of the scenarios that were executed 4 | 5 | Scenario: with no scenarios 6 | Given a file named "features/a.feature" with: 7 | """ 8 | Feature: some feature 9 | """ 10 | When I run cucumber-js with `-f summary` 11 | Then it outputs the text: 12 | """ 13 | 0 scenarios 14 | 0 steps 15 | 16 | """ 17 | 18 | Scenario: with a scenarios 19 | Given a file named "features/a.feature" with: 20 | """ 21 | Feature: some feature 22 | Scenario: some scenario 23 | Given a step 24 | And another step 25 | """ 26 | And a file named "features/step_definitions/cucumber_steps.js" with: 27 | """ 28 | const {Given} = require('@cucumber/cucumber') 29 | 30 | Given(/^a step$/, function() {}) 31 | Given(/^another step$/, function() {}) 32 | """ 33 | When I run cucumber-js with `-f summary` 34 | Then it outputs the text: 35 | """ 36 | 1 scenario (1 passed) 37 | 2 steps (2 passed) 38 | 39 | """ 40 | -------------------------------------------------------------------------------- /features/support/helpers.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import figures from 'figures' 3 | import { normalizeSummaryDuration } from '../../test/formatter_helpers' 4 | 5 | export function normalizeText(text: string): string { 6 | const normalized = figures(text) 7 | .replace(/\r\n|\r/g, '\n') 8 | .trim() 9 | .replace(/[ \t]+\n/g, '\n') 10 | .replace(/\d+(.\d+)?ms/g, 'ms') 11 | .replace(/\//g, path.sep) 12 | .replace(/ +/g, ' ') 13 | .replace(/─+/gu, '─') 14 | .split('\n') 15 | .map((line) => line.trim()) 16 | .join('\n') 17 | 18 | return normalizeSummaryDuration(normalized) 19 | } 20 | -------------------------------------------------------------------------------- /features/support/warn_user_about_enabling_developer_mode.ts: -------------------------------------------------------------------------------- 1 | import { reindent } from 'reindent-template-literals' 2 | import chalk from 'chalk' 3 | 4 | export function warnUserAboutEnablingDeveloperMode(error: any): void { 5 | if (!(error?.code === 'EPERM')) { 6 | throw error 7 | } 8 | if (!(process.platform === 'win32')) { 9 | throw error 10 | } 11 | 12 | // eslint-disable-next-line no-console 13 | console.error( 14 | chalk.red( 15 | reindent(` 16 | Error: Unable to run feature tests! 17 | 18 | You need to enable Developer Mode in Windows to run Cucumber JS's feature tests. 19 | 20 | See this link for more info: 21 | https://docs.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging 22 | `) 23 | ) 24 | ) 25 | process.exit(1) 26 | } 27 | -------------------------------------------------------------------------------- /features/tagged_hooks.feature: -------------------------------------------------------------------------------- 1 | Feature: Tagged Hooks 2 | As a developer running features 3 | I want the ability to control which scenarios my hooks run for 4 | Because not all my scenarios have the same setup and teardown 5 | 6 | Background: 7 | Given a file named "features/step_definitions/world.js" with: 8 | """ 9 | const {setWorldConstructor} = require('@cucumber/cucumber') 10 | 11 | setWorldConstructor(function() { 12 | this.foo = false 13 | this.bar = false 14 | }) 15 | """ 16 | And a file named "features/step_definitions/my_steps.js" with: 17 | """ 18 | const assert = require('assert') 19 | const {Then} = require('@cucumber/cucumber') 20 | 21 | Then('{word} is true', function(prop) { 22 | assert.equal(true, this[prop]) 23 | }) 24 | 25 | Then('{word} is false', function(prop) { 26 | assert.equal(false, this[prop]) 27 | }) 28 | """ 29 | And a file named "features/step_definitions/my_tagged_hooks.js" with: 30 | """ 31 | const {Before} = require('@cucumber/cucumber') 32 | 33 | Before({tags: '@foo'}, function() { 34 | this.foo = true 35 | }) 36 | 37 | Before({tags: '@bar'}, function() { 38 | this.bar = true 39 | }) 40 | """ 41 | 42 | Scenario: hooks filtered by tags on scenario 43 | Given a file named "features/a.feature" with: 44 | """ 45 | Feature: 46 | @foo 47 | Scenario: 48 | Then foo is true 49 | And bar is false 50 | """ 51 | When I run cucumber-js 52 | Then it passes 53 | 54 | Scenario: tags cascade from feature to scenario 55 | Given a file named "features/a.feature" with: 56 | """ 57 | @foo 58 | Feature: 59 | Scenario: 60 | Then foo is true 61 | And bar is false 62 | """ 63 | When I run cucumber-js 64 | Then it passes 65 | -------------------------------------------------------------------------------- /features/target_specific_scenarios_by_line.feature: -------------------------------------------------------------------------------- 1 | Feature: Target specific scenarios 2 | As a developer running features 3 | I want an easy way to run specific scenarios by line 4 | So that I don't waste time running my whole test suite when I don't need to 5 | 6 | Background: 7 | Given a file named "features/a.feature" with: 8 | """ 9 | Feature: some feature 10 | Scenario: first scenario 11 | Given a step 12 | 13 | Scenario Outline: second scenario - 14 | Given a step 15 | 16 | Examples: 17 | | ID | 18 | | X | 19 | | Y | 20 | """ 21 | 22 | Scenario: run a single scenario 23 | When I run cucumber-js with `features/a.feature:2` 24 | Then it fails 25 | And it runs the scenario "first scenario" 26 | 27 | Scenario: run a single scenario outline 28 | When I run cucumber-js with `features/a.feature:5` 29 | Then it fails 30 | And it runs the scenarios: 31 | | NAME | 32 | | second scenario - X | 33 | | second scenario - Y | 34 | 35 | Scenario: run a single scenario outline example 36 | When I run cucumber-js with `features/a.feature:10` 37 | Then it fails 38 | And it runs the scenario "second scenario - X" 39 | 40 | Scenario Outline: run multiple scenarios 41 | When I run cucumber-js with `` 42 | Then it fails 43 | And it runs the scenarios: 44 | | NAME | 45 | | first scenario | 46 | | second scenario - X | 47 | 48 | Examples: 49 | | args | 50 | | features/a.feature:2:10 | 51 | | features/a.feature:2 features/a.feature:10 | 52 | 53 | Scenario: using absolute paths 54 | When I run cucumber-js with `{{{tmpDir}}}/features/a.feature:2` 55 | Then it fails 56 | And it runs the scenario "first scenario" 57 | -------------------------------------------------------------------------------- /features/target_specific_scenarios_by_name.feature: -------------------------------------------------------------------------------- 1 | Feature: Target specific scenarios 2 | As a developer running features 3 | I want an easy way to run specific scenarios by name 4 | So that I don't waste time running my whole test suite when I don't need to 5 | 6 | Background: 7 | Given a file named "features/a.feature" with: 8 | """ 9 | Feature: some feature 10 | Scenario: my topic 11 | Given a step 12 | 13 | Scenario: other topic 1 14 | Given a step 15 | 16 | Scenario: other topic 2 17 | Given a step 18 | """ 19 | 20 | Scenario: run a scenario by name 21 | When I run cucumber-js with `--name my` 22 | Then it fails 23 | And it runs the scenario "my topic" 24 | 25 | Scenario: run multiple scenarios by name 26 | When I run cucumber-js with `--name other` 27 | Then it fails 28 | And it runs the scenarios "other topic 1" and "other topic 2" 29 | -------------------------------------------------------------------------------- /features/usage_json_formatter.feature: -------------------------------------------------------------------------------- 1 | Feature: usage json formatter 2 | 3 | As a developer 4 | I want a formatter which just outputs the full step definition usage in a parsable format 5 | So I can feed the usage data to other programs 6 | 7 | 8 | Scenario: 9 | Given a file named "features/a.feature" with: 10 | """ 11 | Feature: a feature 12 | Scenario: a scenario 13 | Given step A 14 | And step A 15 | When step B 16 | Then step C 17 | """ 18 | And a file named "features/step_definitions/steps.js" with: 19 | """ 20 | const {When} = require('@cucumber/cucumber') 21 | 22 | When('step A', function() {}); 23 | When('step B', function() {}); 24 | When('step C', function() {}); 25 | When(/step D/, function() {}); 26 | """ 27 | When I run cucumber-js with `--format usage-json` 28 | Then it outputs the usage data: 29 | | PATTERN | PATTERN_TYPE | URI | LINE | NUMBER OF MATCHES | 30 | | step A | CucumberExpression | features/step_definitions/steps.js | 3 | 2 | 31 | | step B | CucumberExpression | features/step_definitions/steps.js | 4 | 1 | 32 | | step C | CucumberExpression | features/step_definitions/steps.js | 5 | 1 | 33 | | step D | RegularExpression | features/step_definitions/steps.js | 6 | 0 | 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>cucumber/renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /scripts/remove-empty-sections-changelog.awk: -------------------------------------------------------------------------------- 1 | function start_buffering() { 2 | buf = $0 3 | } 4 | function store_line_in_buffer() { 5 | buf = buf ORS $0 6 | } 7 | function clear_buffer() { 8 | buf = "" 9 | } 10 | /^### (Added|Changed|Deprecated|Removed|Fixed)$/ { 11 | start_buffering() 12 | next 13 | } 14 | /^## / { 15 | clear_buffer() 16 | } 17 | /^ *$/ { 18 | if (buf != "") { 19 | store_line_in_buffer() 20 | } else { 21 | print $0 22 | } 23 | } 24 | !/^ *$/ { 25 | if (buf != "") { 26 | print buf 27 | clear_buffer() 28 | } 29 | print $0 30 | } -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript API for running and extending Cucumber 3 | * 4 | * @packageDocumentation 5 | * @module api 6 | * @remarks 7 | * These docs cover the API used for running Cucumber programmatically. The entry point is `@cucumber/cucumber/api`. 8 | */ 9 | 10 | export { IConfiguration } from '../configuration' 11 | export { IRunEnvironment } from '../environment' 12 | export { IPickleOrder } from '../filter' 13 | export { IPublishConfig } from '../publish' 14 | export * from './load_configuration' 15 | export * from './load_sources' 16 | export * from './load_support' 17 | export * from './run_cucumber' 18 | export * from './types' 19 | -------------------------------------------------------------------------------- /src/api/load_configuration.ts: -------------------------------------------------------------------------------- 1 | import { locateFile } from '../configuration/locate_file' 2 | import { 3 | DEFAULT_CONFIGURATION, 4 | fromFile, 5 | mergeConfigurations, 6 | parseConfiguration, 7 | validateConfiguration, 8 | } from '../configuration' 9 | import { IRunEnvironment, makeEnvironment } from '../environment' 10 | import { convertConfiguration } from './convert_configuration' 11 | import { IResolvedConfiguration, ILoadConfigurationOptions } from './types' 12 | 13 | /** 14 | * Load user-authored configuration to be used in a test run 15 | * 16 | * @public 17 | * @param options - Coordinates required to find configuration 18 | * @param environment - Project environment 19 | */ 20 | export async function loadConfiguration( 21 | options: ILoadConfigurationOptions = {}, 22 | environment: IRunEnvironment = {} 23 | ): Promise { 24 | const { cwd, env, logger } = makeEnvironment(environment) 25 | const configFile = options.file ?? locateFile(cwd) 26 | if (configFile) { 27 | logger.debug(`Configuration will be loaded from "${configFile}"`) 28 | } else if (configFile === false) { 29 | logger.debug('Skipping configuration file resolution') 30 | } else { 31 | logger.debug('No configuration file found') 32 | } 33 | const profileConfiguration = configFile 34 | ? await fromFile(logger, cwd, configFile, options.profiles) 35 | : {} 36 | const original = mergeConfigurations( 37 | DEFAULT_CONFIGURATION, 38 | profileConfiguration, 39 | parseConfiguration(logger, 'Provided', options.provided) 40 | ) 41 | logger.debug('Resolved configuration:', original) 42 | validateConfiguration(original, logger) 43 | const runnable = await convertConfiguration(logger, original, env) 44 | return { 45 | useConfiguration: original, 46 | runConfiguration: runnable, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/api/load_configuration_spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { IRunEnvironment } from '../environment' 3 | import { setupEnvironment, teardownEnvironment } from './test_helpers' 4 | import { loadConfiguration } from './load_configuration' 5 | 6 | describe('loadConfiguration', function () { 7 | this.timeout(10_000) 8 | 9 | let environment: IRunEnvironment 10 | beforeEach(async () => { 11 | environment = await setupEnvironment() 12 | }) 13 | afterEach(async () => teardownEnvironment(environment)) 14 | 15 | it('should handle configuration directly provided as an array of strings', async () => { 16 | const { useConfiguration } = await loadConfiguration( 17 | { provided: ['--world-parameters', '{"foo":"bar"}'] }, 18 | environment 19 | ) 20 | 21 | expect(useConfiguration.worldParameters).to.deep.eq({ foo: 'bar' }) 22 | }) 23 | 24 | it('should handle configuration directly provided as a string', async () => { 25 | const { useConfiguration } = await loadConfiguration( 26 | { provided: `--world-parameters '{"foo":"bar"}'` }, 27 | environment 28 | ) 29 | 30 | expect(useConfiguration.worldParameters).to.deep.eq({ foo: 'bar' }) 31 | }) 32 | 33 | it('should skip trying to resolve from a file if `file=false`', async () => { 34 | const { useConfiguration } = await loadConfiguration( 35 | { file: false }, 36 | environment 37 | ) 38 | 39 | // values from configuration file are not present 40 | expect(useConfiguration.paths).to.deep.eq([]) 41 | expect(useConfiguration.requireModule).to.deep.eq([]) 42 | expect(useConfiguration.require).to.deep.eq([]) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/api/load_support.ts: -------------------------------------------------------------------------------- 1 | import { IdGenerator } from '@cucumber/messages' 2 | import { resolvePaths } from '../paths' 3 | import { IRunEnvironment, makeEnvironment } from '../environment' 4 | import { ILoadSupportOptions, ISupportCodeLibrary } from './types' 5 | import { getSupportCodeLibrary } from './support' 6 | import { initializeForLoadSupport } from './plugins' 7 | 8 | /** 9 | * Load support code for use in test runs 10 | * 11 | * @public 12 | * @param options - Options required to find the support code 13 | * @param environment - Project environment 14 | */ 15 | export async function loadSupport( 16 | options: ILoadSupportOptions, 17 | environment: IRunEnvironment = {} 18 | ): Promise { 19 | const mergedEnvironment = makeEnvironment(environment) 20 | const { cwd, logger } = mergedEnvironment 21 | const newId = IdGenerator.uuid() 22 | const supportCoordinates = Object.assign( 23 | { 24 | requireModules: [], 25 | requirePaths: [], 26 | loaders: [], 27 | importPaths: [], 28 | }, 29 | options.support 30 | ) 31 | const pluginManager = await initializeForLoadSupport(mergedEnvironment) 32 | const resolvedPaths = await resolvePaths( 33 | logger, 34 | cwd, 35 | options.sources, 36 | supportCoordinates 37 | ) 38 | pluginManager.emit('paths:resolve', resolvedPaths) 39 | const { requirePaths, importPaths } = resolvedPaths 40 | const supportCodeLibrary = await getSupportCodeLibrary({ 41 | logger, 42 | cwd, 43 | newId, 44 | requireModules: supportCoordinates.requireModules, 45 | requirePaths, 46 | loaders: supportCoordinates.loaders, 47 | importPaths, 48 | }) 49 | await pluginManager.cleanup() 50 | return supportCodeLibrary 51 | } 52 | -------------------------------------------------------------------------------- /src/api/load_support_spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { expect } from 'chai' 3 | import { IRunEnvironment } from '../environment' 4 | import { loadSupport } from './load_support' 5 | import { loadConfiguration } from './load_configuration' 6 | import { setupEnvironment, teardownEnvironment } from './test_helpers' 7 | 8 | describe('loadSupport', function () { 9 | this.timeout(10_000) 10 | 11 | let environment: IRunEnvironment 12 | beforeEach(async () => { 13 | environment = await setupEnvironment() 14 | }) 15 | afterEach(async () => teardownEnvironment(environment)) 16 | 17 | it('should include original paths in the returned support code library', async () => { 18 | const { runConfiguration } = await loadConfiguration({}, environment) 19 | const support = await loadSupport(runConfiguration, environment) 20 | 21 | expect(support.originalCoordinates).to.deep.eq({ 22 | requireModules: ['ts-node/register'], 23 | requirePaths: [path.join(environment.cwd, 'features', 'steps.ts')], 24 | importPaths: [], 25 | loaders: [], 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/api/plugins.ts: -------------------------------------------------------------------------------- 1 | import { PluginManager } from '../plugin' 2 | import publishPlugin from '../publish' 3 | import filterPlugin from '../filter' 4 | import { UsableEnvironment } from '../environment' 5 | import { IRunConfiguration, ISourcesCoordinates } from './types' 6 | 7 | export async function initializeForLoadSources( 8 | coordinates: ISourcesCoordinates, 9 | environment: UsableEnvironment 10 | ): Promise { 11 | // eventually we'll load plugin packages here 12 | const pluginManager = new PluginManager(environment) 13 | await pluginManager.initCoordinator('loadSources', filterPlugin, coordinates) 14 | return pluginManager 15 | } 16 | 17 | export async function initializeForLoadSupport( 18 | environment: UsableEnvironment 19 | ): Promise { 20 | // eventually we'll load plugin packages here 21 | return new PluginManager(environment) 22 | } 23 | 24 | export async function initializeForRunCucumber( 25 | configuration: IRunConfiguration, 26 | environment: UsableEnvironment 27 | ): Promise { 28 | // eventually we'll load plugin packages here 29 | const pluginManager = new PluginManager(environment) 30 | await pluginManager.initCoordinator( 31 | 'runCucumber', 32 | publishPlugin, 33 | configuration.formats.publish 34 | ) 35 | await pluginManager.initCoordinator( 36 | 'runCucumber', 37 | filterPlugin, 38 | configuration.sources 39 | ) 40 | return pluginManager 41 | } 42 | -------------------------------------------------------------------------------- /src/api/support.ts: -------------------------------------------------------------------------------- 1 | import { register } from 'node:module' 2 | import { pathToFileURL } from 'node:url' 3 | import { IdGenerator } from '@cucumber/messages' 4 | import { SupportCodeLibrary } from '../support_code_library_builder/types' 5 | import supportCodeLibraryBuilder from '../support_code_library_builder' 6 | import tryRequire from '../try_require' 7 | import { ILogger } from '../environment' 8 | 9 | export async function getSupportCodeLibrary({ 10 | logger, 11 | cwd, 12 | newId, 13 | requireModules, 14 | requirePaths, 15 | importPaths, 16 | loaders, 17 | }: { 18 | logger: ILogger 19 | cwd: string 20 | newId: IdGenerator.NewId 21 | requireModules: string[] 22 | requirePaths: string[] 23 | importPaths: string[] 24 | loaders: string[] 25 | }): Promise { 26 | supportCodeLibraryBuilder.reset(cwd, newId, { 27 | requireModules, 28 | requirePaths, 29 | importPaths, 30 | loaders, 31 | }) 32 | 33 | requireModules.map((path) => { 34 | logger.debug(`Attempting to require code from "${path}"`) 35 | tryRequire(path) 36 | }) 37 | requirePaths.map((path) => { 38 | logger.debug(`Attempting to require code from "${path}"`) 39 | tryRequire(path) 40 | }) 41 | 42 | for (const specifier of loaders) { 43 | logger.debug(`Attempting to register loader "${specifier}"`) 44 | register(specifier, pathToFileURL('./')) 45 | } 46 | 47 | for (const path of importPaths) { 48 | logger.debug(`Attempting to import code from "${path}"`) 49 | await import(pathToFileURL(path).toString()) 50 | } 51 | 52 | return supportCodeLibraryBuilder.finalize() 53 | } 54 | -------------------------------------------------------------------------------- /src/api/test_helpers.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { PassThrough } from 'node:stream' 3 | import fs from 'mz/fs' 4 | import { reindent } from 'reindent-template-literals' 5 | import { IdGenerator } from '@cucumber/messages' 6 | import { IRunEnvironment } from '../environment' 7 | 8 | const newId = IdGenerator.uuid() 9 | 10 | export async function setupEnvironment(): Promise> { 11 | const cwd = path.join(__dirname, '..', '..', 'tmp', `api_${newId()}`) 12 | await fs.mkdir(path.join(cwd, 'features'), { recursive: true }) 13 | await fs.writeFile( 14 | path.join(cwd, 'features', 'test.feature'), 15 | reindent(`Feature: test fixture 16 | Scenario: one 17 | Given a step 18 | Then another step`) 19 | ) 20 | await fs.writeFile( 21 | path.join(cwd, 'features', 'steps.ts'), 22 | reindent(`import { Given, Then } from '../../../src' 23 | Given('a step', function () {}) 24 | Then('another step', function () {})`) 25 | ) 26 | await fs.writeFile( 27 | path.join(cwd, 'cucumber.mjs'), 28 | `export default {paths: ['features/test.feature'], requireModule: ['ts-node/register'], require: ['features/steps.ts']}` 29 | ) 30 | const stdout = new PassThrough() 31 | return { cwd, stdout } 32 | } 33 | 34 | export async function teardownEnvironment(environment: IRunEnvironment) { 35 | return new Promise((resolve) => { 36 | fs.rm(environment.cwd, { recursive: true }, resolve) 37 | }).then(() => { 38 | environment.stdout.end() 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/wrapper.mjs: -------------------------------------------------------------------------------- 1 | import api from './index.js' 2 | 3 | export const loadConfiguration = api.loadConfiguration 4 | export const loadSupport = api.loadSupport 5 | export const loadSources = api.loadSources 6 | export const runCucumber = api.runCucumber 7 | -------------------------------------------------------------------------------- /src/assemble/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assemble_test_cases' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /src/assemble/types.ts: -------------------------------------------------------------------------------- 1 | import { GherkinDocument, Pickle, TestCase } from '@cucumber/messages' 2 | 3 | export interface SourcedPickle { 4 | gherkinDocument: GherkinDocument 5 | pickle: Pickle 6 | } 7 | 8 | export interface AssembledTestCase { 9 | gherkinDocument: GherkinDocument 10 | pickle: Pickle 11 | testCase: TestCase 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/i18n.ts: -------------------------------------------------------------------------------- 1 | import { dialects } from '@cucumber/gherkin' 2 | import Table from 'cli-table3' 3 | import { capitalCase } from 'capital-case' 4 | 5 | const keywords = [ 6 | 'feature', 7 | 'rule', 8 | 'background', 9 | 'scenario', 10 | 'scenarioOutline', 11 | 'examples', 12 | 'given', 13 | 'when', 14 | 'then', 15 | 'and', 16 | 'but', 17 | ] as const 18 | 19 | function getAsTable(header: string[], rows: string[][]): string { 20 | const table = new Table({ 21 | chars: { 22 | bottom: '', 23 | 'bottom-left': '', 24 | 'bottom-mid': '', 25 | 'bottom-right': '', 26 | left: '', 27 | 'left-mid': '', 28 | mid: '', 29 | 'mid-mid': '', 30 | middle: ' | ', 31 | right: '', 32 | 'right-mid': '', 33 | top: '', 34 | 'top-left': '', 35 | 'top-mid': '', 36 | 'top-right': '', 37 | }, 38 | style: { 39 | border: [], 40 | 'padding-left': 0, 41 | 'padding-right': 0, 42 | }, 43 | }) 44 | table.push(header) 45 | table.push(...rows) 46 | return table.toString() 47 | } 48 | 49 | export function getLanguages(): string { 50 | const rows = Object.keys(dialects).map((isoCode) => [ 51 | isoCode, 52 | dialects[isoCode].name, 53 | dialects[isoCode].native, 54 | ]) 55 | return getAsTable(['ISO 639-1', 'ENGLISH NAME', 'NATIVE NAME'], rows) 56 | } 57 | 58 | export function getKeywords(isoCode: string): string { 59 | const language = dialects[isoCode] 60 | const rows = keywords.map((keyword) => { 61 | const words = language[keyword].map((s) => `"${s}"`).join(', ') 62 | return [capitalCase(keyword), words] 63 | }) 64 | return getAsTable(['ENGLISH KEYWORD', 'NATIVE KEYWORDS'], rows) 65 | } 66 | -------------------------------------------------------------------------------- /src/cli/install_validator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import isInstalledGlobally from 'is-installed-globally' 3 | 4 | export async function validateInstall(): Promise { 5 | if (isInstalledGlobally) 6 | console.warn( 7 | ` 8 | It looks like you're running Cucumber from a global installation. 9 | If so, you'll likely see issues - you need to have Cucumber installed as a local dependency in your project. 10 | See https://github.com/cucumber/cucumber-js/blob/main/docs/installation.md#invalid-installations 11 | ` 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/cli/run.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* This is one rare place where we're fine to use process/console directly, 3 | * but other code abstracts those to remain composable and testable. */ 4 | import { validateNodeEngineVersion } from './validate_node_engine_version' 5 | import Cli, { ICliRunResult } from './' 6 | 7 | function logErrorMessageAndExit(message: string): void { 8 | console.error(message) 9 | process.exit(1) 10 | } 11 | 12 | export default async function run(): Promise { 13 | validateNodeEngineVersion( 14 | process.version, 15 | (error) => { 16 | console.error(error) 17 | process.exit(1) 18 | }, 19 | console.warn 20 | ) 21 | 22 | const cli = new Cli({ 23 | argv: process.argv, 24 | cwd: process.cwd(), 25 | stdout: process.stdout, 26 | stderr: process.stderr, 27 | env: process.env, 28 | }) 29 | 30 | let result: ICliRunResult 31 | try { 32 | result = await cli.run() 33 | } catch (error) { 34 | logErrorMessageAndExit(error) 35 | } 36 | 37 | const exitCode = result.success ? 0 : 1 38 | if (result.shouldExitImmediately) { 39 | process.exit(exitCode) 40 | } else { 41 | process.exitCode = exitCode 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cli/validate_node_engine_version.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import semver from 'semver' 4 | 5 | type PackageJSON = { 6 | engines: { node: string } 7 | enginesTested: { node: string } 8 | } 9 | 10 | const readActualPackageJSON: () => PackageJSON = () => 11 | JSON.parse( 12 | fs 13 | .readFileSync(path.resolve(__dirname, '..', '..', 'package.json')) 14 | .toString() 15 | ) 16 | 17 | export function validateNodeEngineVersion( 18 | currentVersion: string, 19 | onError: (message: string) => void, 20 | onWarning: (message: string) => void, 21 | readPackageJSON: () => PackageJSON = readActualPackageJSON 22 | ): void { 23 | const requiredVersions = readPackageJSON().engines.node 24 | const testedVersions = readPackageJSON().enginesTested.node 25 | if (!semver.satisfies(currentVersion, requiredVersions)) { 26 | onError( 27 | `Cucumber can only run on Node.js versions ${requiredVersions}. This Node.js version is ${currentVersion}` 28 | ) 29 | } else if (!semver.satisfies(currentVersion, testedVersions)) { 30 | onWarning( 31 | `This Node.js version (${currentVersion}) has not been tested with this version of Cucumber; it should work normally, but please raise an issue if you see anything unexpected.` 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/configuration/argv_parser_spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import ArgvParser from './argv_parser' 3 | 4 | const baseArgv = ['/path/to/node', '/path/to/cucumber-js'] 5 | 6 | describe('ArgvParser', () => { 7 | describe('parse', () => { 8 | it('should produce an empty object when no arguments', () => { 9 | const { configuration } = ArgvParser.parse(baseArgv) 10 | expect(configuration).to.deep.eq({}) 11 | }) 12 | 13 | it('should handle repeatable arguments', () => { 14 | const { configuration } = ArgvParser.parse([ 15 | ...baseArgv, 16 | 'features/hello.feature', 17 | 'features/world.feature', 18 | '--require', 19 | 'hooks/**/*.js', 20 | '--require', 21 | 'steps/**/*.js', 22 | ]) 23 | expect(configuration).to.deep.eq({ 24 | paths: ['features/hello.feature', 'features/world.feature'], 25 | require: ['hooks/**/*.js', 'steps/**/*.js'], 26 | }) 27 | }) 28 | 29 | it('should handle mergeable tag strings', () => { 30 | const { configuration } = ArgvParser.parse([ 31 | ...baseArgv, 32 | '--tags', 33 | '@foo', 34 | '--tags', 35 | '@bar', 36 | ]) 37 | expect(configuration).to.deep.eq({ 38 | tags: '(@foo) and (@bar)', 39 | }) 40 | }) 41 | 42 | it('should handle mergeable json objects', () => { 43 | const params1 = { foo: 1, bar: { stuff: 3 } } 44 | const params2 = { foo: 2, bar: { things: 4 } } 45 | const { configuration } = ArgvParser.parse([ 46 | ...baseArgv, 47 | '--world-parameters', 48 | JSON.stringify(params1), 49 | '--world-parameters', 50 | JSON.stringify(params2), 51 | ]) 52 | expect(configuration).to.deep.eq({ 53 | worldParameters: { foo: 2, bar: { stuff: 3, things: 4 } }, 54 | }) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/configuration/check_schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup' 2 | import { dialects } from '@cucumber/gherkin' 3 | import { IConfiguration } from './types' 4 | 5 | const schema = yup.object().shape({ 6 | backtrace: yup.boolean(), 7 | dryRun: yup.boolean(), 8 | exit: yup.boolean(), 9 | failFast: yup.boolean(), 10 | format: yup 11 | .array() 12 | .of( 13 | yup.lazy((val) => 14 | Array.isArray(val) 15 | ? yup.array().of(yup.string()).min(1).max(2) 16 | : yup.string() 17 | ) 18 | ), 19 | formatOptions: yup.object(), 20 | import: yup.array().of(yup.string()), 21 | language: yup.string().oneOf(Object.keys(dialects)), 22 | name: yup.array().of(yup.string()), 23 | order: yup.string().matches(/^random:.*|random|defined$/), 24 | paths: yup.array().of(yup.string()), 25 | parallel: yup.number().integer().min(0), 26 | publish: yup.boolean(), 27 | publishQuiet: yup.boolean(), 28 | require: yup.array().of(yup.string()), 29 | requireModule: yup.array().of(yup.string()), 30 | retry: yup.number().integer().min(0), 31 | retryTagFilter: yup.string(), 32 | strict: yup.boolean(), 33 | tags: yup.string(), 34 | worldParameters: yup.object(), 35 | }) 36 | 37 | export function checkSchema(configuration: any): Partial { 38 | return schema.validateSync(configuration, { 39 | abortEarly: false, 40 | strict: true, 41 | stripUnknown: true, 42 | }) as Partial 43 | } 44 | -------------------------------------------------------------------------------- /src/configuration/default_configuration.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from './types' 2 | 3 | export const DEFAULT_CONFIGURATION: IConfiguration = { 4 | backtrace: false, 5 | dryRun: false, 6 | forceExit: false, 7 | failFast: false, 8 | format: [], 9 | formatOptions: {}, 10 | import: [], 11 | language: 'en', 12 | loader: [], 13 | name: [], 14 | order: 'defined', 15 | paths: [], 16 | parallel: 0, 17 | publish: false, 18 | publishQuiet: false, 19 | require: [], 20 | requireModule: [], 21 | retry: 0, 22 | retryTagFilter: '', 23 | strict: true, 24 | tags: '', 25 | worldParameters: {}, 26 | } 27 | -------------------------------------------------------------------------------- /src/configuration/helpers.ts: -------------------------------------------------------------------------------- 1 | export function isTruthyString(s: string | undefined): boolean { 2 | if (s === undefined) { 3 | return false 4 | } 5 | return s.match(/^(false|no|0)$/i) === null 6 | } 7 | -------------------------------------------------------------------------------- /src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ArgvParser } from './argv_parser' 2 | export * from './default_configuration' 3 | export * from './from_file' 4 | export * from './helpers' 5 | export * from './merge_configurations' 6 | export * from './parse_configuration' 7 | export * from './split_format_descriptor' 8 | export * from './types' 9 | export * from './validate_configuration' 10 | -------------------------------------------------------------------------------- /src/configuration/locate_file.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import fs from 'mz/fs' 3 | 4 | const DEFAULT_FILENAMES = [ 5 | 'cucumber.js', 6 | 'cucumber.cjs', 7 | 'cucumber.mjs', 8 | 'cucumber.json', 9 | 'cucumber.yaml', 10 | 'cucumber.yml', 11 | ] 12 | 13 | export function locateFile(cwd: string): string | undefined { 14 | return DEFAULT_FILENAMES.find((filename) => 15 | fs.existsSync(path.join(cwd, filename)) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/configuration/merge_configurations.ts: -------------------------------------------------------------------------------- 1 | import mergeWith from 'lodash.mergewith' 2 | import { IConfiguration } from './types' 3 | 4 | const ADDITIVE_ARRAYS = [ 5 | 'format', 6 | 'import', 7 | 'loader', 8 | 'name', 9 | 'paths', 10 | 'require', 11 | 'requireModule', 12 | ] 13 | const TAG_EXPRESSIONS = ['tags', 'retryTagFilter'] 14 | 15 | function mergeArrays(objValue: any[], srcValue: any[]) { 16 | if (objValue && srcValue) { 17 | return [].concat(objValue, srcValue) 18 | } 19 | return undefined 20 | } 21 | 22 | function mergeTagExpressions(objValue: string, srcValue: string) { 23 | if (objValue && srcValue) { 24 | return `${wrapTagExpression(objValue)} and ${wrapTagExpression(srcValue)}` 25 | } 26 | return undefined 27 | } 28 | 29 | function wrapTagExpression(raw: string) { 30 | if (raw.startsWith('(') && raw.endsWith(')')) { 31 | return raw 32 | } 33 | return `(${raw})` 34 | } 35 | 36 | function customizer(objValue: any, srcValue: any, key: string): any { 37 | if (ADDITIVE_ARRAYS.includes(key)) { 38 | return mergeArrays(objValue, srcValue) 39 | } 40 | if (TAG_EXPRESSIONS.includes(key)) { 41 | return mergeTagExpressions(objValue, srcValue) 42 | } 43 | return undefined 44 | } 45 | 46 | export function mergeConfigurations>( 47 | source: T, 48 | ...configurations: Partial[] 49 | ): T { 50 | return mergeWith({}, source, ...configurations, customizer) 51 | } 52 | -------------------------------------------------------------------------------- /src/configuration/parse_configuration.ts: -------------------------------------------------------------------------------- 1 | import stringArgv from 'string-argv' 2 | import { ILogger } from '../environment' 3 | import { IConfiguration } from './types' 4 | import ArgvParser from './argv_parser' 5 | import { checkSchema } from './check_schema' 6 | 7 | export function parseConfiguration( 8 | logger: ILogger, 9 | source: string, 10 | definition: Partial | string[] | string | undefined 11 | ): Partial { 12 | if (!definition) { 13 | return {} 14 | } 15 | if (Array.isArray(definition)) { 16 | logger.debug(`${source} configuration value is an array; parsing as argv`) 17 | const { configuration } = ArgvParser.parse([ 18 | 'node', 19 | 'cucumber-js', 20 | ...definition, 21 | ]) 22 | return configuration 23 | } 24 | if (typeof definition === 'string') { 25 | logger.debug(`${source} configuration value is a string; parsing as argv`) 26 | const { configuration } = ArgvParser.parse([ 27 | 'node', 28 | 'cucumber-js', 29 | ...stringArgv(definition), 30 | ]) 31 | return configuration 32 | } 33 | try { 34 | return checkSchema(definition) 35 | } catch (error) { 36 | throw new Error( 37 | `${source} configuration value failed schema validation: ${error.errors.join( 38 | ' ' 39 | )}` 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/configuration/validate_configuration.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../environment' 2 | import { IConfiguration } from './types' 3 | 4 | export function validateConfiguration( 5 | configuration: IConfiguration, 6 | logger: ILogger 7 | ): void { 8 | if (configuration.publishQuiet) { 9 | logger.warn( 10 | '`publishQuiet` option is no longer needed, you can remove it from your configuration; see https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md' 11 | ) 12 | } 13 | if (configuration.requireModule.length && !configuration.require.length) { 14 | logger.warn( 15 | 'Use of `require-module` option normally means you should specify your support code paths with `require`; see https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#finding-your-code' 16 | ) 17 | } 18 | if (configuration.loader.length && !configuration.import.length) { 19 | logger.warn( 20 | 'Use of `loader` option normally means you should specify your support code paths with `import`; see https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#finding-your-code' 21 | ) 22 | } 23 | if (configuration.retryTagFilter && !configuration.retry) { 24 | throw new Error( 25 | 'a positive `retry` count must be specified when setting `retryTagFilter`' 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/environment/console_logger.ts: -------------------------------------------------------------------------------- 1 | import { Console } from 'node:console' 2 | import { Writable } from 'node:stream' 3 | import { ILogger } from './types' 4 | 5 | export class ConsoleLogger implements ILogger { 6 | private readonly console: Console 7 | 8 | constructor( 9 | private stream: Writable, 10 | private debugEnabled: boolean 11 | ) { 12 | this.console = new Console(this.stream) 13 | } 14 | 15 | debug(message?: any, ...optionalParams: any[]): void { 16 | if (this.debugEnabled) { 17 | this.console.debug(message, ...optionalParams) 18 | } 19 | } 20 | 21 | error(message?: any, ...optionalParams: any[]): void { 22 | this.console.error(message, ...optionalParams) 23 | } 24 | 25 | warn(message?: any, ...optionalParams: any[]): void { 26 | this.console.warn(message, ...optionalParams) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/environment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './make_environment' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /src/environment/make_environment.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger } from './console_logger' 2 | import { UsableEnvironment, IRunEnvironment } from './types' 3 | 4 | export function makeEnvironment(provided: IRunEnvironment): UsableEnvironment { 5 | const fullEnvironment = Object.assign( 6 | {}, 7 | { 8 | cwd: process.cwd(), 9 | stdout: process.stdout, 10 | stderr: process.stderr, 11 | env: process.env, 12 | debug: false, 13 | }, 14 | provided 15 | ) 16 | const logger = new ConsoleLogger( 17 | fullEnvironment.stderr, 18 | fullEnvironment.debug 19 | ) 20 | logger.debug('Resolved environment:', fullEnvironment) 21 | return { 22 | ...fullEnvironment, 23 | logger: logger, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/environment/types.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from 'node:stream' 2 | 3 | export interface ILogger { 4 | debug: (message?: any, ...optionalParams: any[]) => void 5 | error: (message?: any, ...optionalParams: any[]) => void 6 | warn: (message?: any, ...optionalParams: any[]) => void 7 | } 8 | 9 | /** 10 | * Contextual data about the project environment 11 | * @public 12 | */ 13 | export interface IRunEnvironment { 14 | /** 15 | * Working directory for the project 16 | * @default process.cwd() 17 | */ 18 | cwd?: string 19 | /** 20 | * Writable stream where the test run's main formatter output is written 21 | * @default process.stdout 22 | */ 23 | stdout?: Writable 24 | /** 25 | * Writable stream where the test run's warning/error output is written 26 | * @default process.stderr 27 | */ 28 | stderr?: Writable 29 | /** 30 | * Environment variables 31 | * @default process.env 32 | */ 33 | env?: Record 34 | /** 35 | * Whether debug logging should be emitted to {@link IRunEnvironment.stderr} 36 | * @default false 37 | * @see {@link https://github.com/cucumber/cucumber-js/blob/main/docs/debugging.md} 38 | */ 39 | debug?: boolean 40 | } 41 | 42 | export type UsableEnvironment = Required & { 43 | logger: ILogger 44 | } 45 | -------------------------------------------------------------------------------- /src/filter/filter_plugin.ts: -------------------------------------------------------------------------------- 1 | import { InternalPlugin } from '../plugin' 2 | import { ISourcesCoordinates } from '../api' 3 | import PickleFilter from '../pickle_filter' 4 | import { orderPickles } from '../cli/helpers' 5 | 6 | export const filterPlugin: InternalPlugin = { 7 | type: 'plugin', 8 | coordinator: async ({ on, options, logger, environment }) => { 9 | let unexpandedSourcePaths: string[] = [] 10 | on('paths:resolve', (paths) => { 11 | unexpandedSourcePaths = paths.unexpandedSourcePaths 12 | }) 13 | 14 | on('pickles:filter', async (allPickles) => { 15 | const pickleFilter = new PickleFilter({ 16 | cwd: environment.cwd, 17 | featurePaths: unexpandedSourcePaths, 18 | names: options.names, 19 | tagExpression: options.tagExpression, 20 | }) 21 | 22 | return allPickles.filter((pickle) => pickleFilter.matches(pickle)) 23 | }) 24 | 25 | on('pickles:order', async (unorderedPickles) => { 26 | const orderedPickles = [...unorderedPickles] 27 | orderPickles(orderedPickles, options.order, logger) 28 | return orderedPickles 29 | }) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/filter/index.ts: -------------------------------------------------------------------------------- 1 | import { filterPlugin } from './filter_plugin' 2 | export * from './types' 3 | 4 | export default filterPlugin 5 | -------------------------------------------------------------------------------- /src/filter/types.ts: -------------------------------------------------------------------------------- 1 | import { GherkinDocument, Location, Pickle } from '@cucumber/messages' 2 | 3 | /** 4 | * The ordering strategy for pickles 5 | * @public 6 | * @example "defined" 7 | * @example "random" 8 | * @example "random:234119" 9 | */ 10 | export type IPickleOrder = 'defined' | 'random' | `random:${string}` 11 | 12 | export interface IFilterablePickle { 13 | pickle: Pickle 14 | gherkinDocument: GherkinDocument 15 | location: Location 16 | } 17 | -------------------------------------------------------------------------------- /src/filter_stack_trace.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { StackFrame } from 'error-stack-parser' 3 | import { valueOrDefault } from './value_checker' 4 | 5 | const projectRootPath = path.join(__dirname, '..') 6 | const projectChildDirs = ['src', 'lib', 'node_modules'] 7 | 8 | export function isFileNameInCucumber(fileName: string): boolean { 9 | return projectChildDirs.some((dir) => 10 | fileName.startsWith(path.join(projectRootPath, dir)) 11 | ) 12 | } 13 | 14 | export function filterStackTrace(frames: StackFrame[]): StackFrame[] { 15 | if (isErrorInCucumber(frames)) { 16 | return frames 17 | } 18 | const index = frames.findIndex((x) => isFrameInCucumber(x)) 19 | if (index === -1) { 20 | return frames 21 | } 22 | return frames.slice(0, index) 23 | } 24 | 25 | function isErrorInCucumber(frames: StackFrame[]): boolean { 26 | const filteredFrames = frames.filter((x) => !isFrameInNode(x)) 27 | return filteredFrames.length > 0 && isFrameInCucumber(filteredFrames[0]) 28 | } 29 | 30 | function isFrameInCucumber(frame: StackFrame): boolean { 31 | const fileName = valueOrDefault(frame.getFileName(), '') 32 | return isFileNameInCucumber(fileName) 33 | } 34 | 35 | function isFrameInNode(frame: StackFrame): boolean { 36 | const fileName = valueOrDefault(frame.getFileName(), '') 37 | return !fileName.includes(path.sep) 38 | } 39 | -------------------------------------------------------------------------------- /src/formatter/builder_spec.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url' 2 | import path from 'node:path' 3 | import { expect } from 'chai' 4 | import FormatterBuilder from './builder' 5 | 6 | describe('custom class loading', () => { 7 | const varieties = [ 8 | 'legacy_esm.mjs', 9 | 'legacy_exports_dot_default.cjs', 10 | 'legacy_module_dot_exports.cjs', 11 | ] 12 | varieties.forEach((filename) => { 13 | describe(filename, () => { 14 | it('should handle a relative path', async () => { 15 | const CustomClass = await FormatterBuilder.loadCustomClass( 16 | 'formatter', 17 | `./fixtures/${filename}`, 18 | __dirname 19 | ) 20 | 21 | expect(typeof CustomClass).to.eq('function') 22 | }) 23 | 24 | it('should handle a file:// url', async () => { 25 | const fileUrl = pathToFileURL( 26 | path.resolve(__dirname, `./fixtures/${filename}`) 27 | ).toString() 28 | const CustomClass = await FormatterBuilder.loadCustomClass( 29 | 'formatter', 30 | fileUrl, 31 | __dirname 32 | ) 33 | 34 | expect(typeof CustomClass).to.eq('function') 35 | }) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/formatter/builtin/index.ts: -------------------------------------------------------------------------------- 1 | import { FormatterImplementation } from '../index' 2 | import JsonFormatter from '../json_formatter' 3 | import ProgressFormatter from '../progress_formatter' 4 | import ProgressBarFormatter from '../progress_bar_formatter' 5 | import RerunFormatter from '../rerun_formatter' 6 | import SnippetsFormatter from '../snippets_formatter' 7 | import SummaryFormatter from '../summary_formatter' 8 | import UsageFormatter from '../usage_formatter' 9 | import UsageJsonFormatter from '../usage_json_formatter' 10 | import messageFormatter from './message' 11 | import htmlFormatter from './html' 12 | 13 | const builtin = { 14 | // new plugin-based formatters 15 | html: htmlFormatter, 16 | junit: '@cucumber/junit-xml-formatter', 17 | message: messageFormatter, 18 | // legacy class-based formatters 19 | json: JsonFormatter, 20 | progress: ProgressFormatter, 21 | 'progress-bar': ProgressBarFormatter, 22 | rerun: RerunFormatter, 23 | snippets: SnippetsFormatter, 24 | summary: SummaryFormatter, 25 | usage: UsageFormatter, 26 | 'usage-json': UsageJsonFormatter, 27 | } as const satisfies Record 28 | 29 | export default builtin as Record 30 | 31 | export const documentation = { 32 | // new plugin-based formatters 33 | html: 'Outputs a HTML report', 34 | junit: 'Produces a JUnit XML report', 35 | message: 'Emits Cucumber messages in newline-delimited JSON', 36 | // legacy class-based formatters 37 | json: JsonFormatter.documentation, 38 | progress: ProgressFormatter.documentation, 39 | 'progress-bar': ProgressBarFormatter.documentation, 40 | rerun: RerunFormatter.documentation, 41 | snippets: SnippetsFormatter.documentation, 42 | summary: SummaryFormatter.documentation, 43 | usage: UsageFormatter.documentation, 44 | 'usage-json': UsageJsonFormatter.documentation, 45 | } satisfies Record 46 | -------------------------------------------------------------------------------- /src/formatter/builtin/message.ts: -------------------------------------------------------------------------------- 1 | import { FormatterPlugin } from '../../plugin' 2 | 3 | export default { 4 | type: 'formatter', 5 | formatter({ on, write }) { 6 | on('message', (message) => write(JSON.stringify(message) + '\n')) 7 | }, 8 | } satisfies FormatterPlugin 9 | -------------------------------------------------------------------------------- /src/formatter/create_stream.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { Writable } from 'node:stream' 3 | import { mkdirp } from 'mkdirp' 4 | import fs from 'mz/fs' 5 | import { ILogger } from '../environment' 6 | 7 | export async function createStream( 8 | target: string, 9 | onStreamError: () => void, 10 | cwd: string, 11 | logger: ILogger 12 | ) { 13 | const absoluteTarget = path.resolve(cwd, target) 14 | const directory = path.dirname(absoluteTarget) 15 | 16 | try { 17 | await mkdirp(directory) 18 | } catch (error) { 19 | logger.warn('Failed to ensure directory for formatter target exists') 20 | } 21 | 22 | const stream: Writable = fs.createWriteStream(null, { 23 | fd: await fs.open(absoluteTarget, 'w'), 24 | }) 25 | 26 | stream.on('error', (error: Error) => { 27 | logger.error(error.message) 28 | onStreamError() 29 | }) 30 | 31 | return { 32 | directory, 33 | stream, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/formatter/find_class_or_plugin.ts: -------------------------------------------------------------------------------- 1 | import { doesNotHaveValue } from '../value_checker' 2 | 3 | export function findClassOrPlugin(imported: any) { 4 | return findRecursive(imported, 3) 5 | } 6 | 7 | function findRecursive(thing: any, depth: number): any { 8 | if (doesNotHaveValue(thing)) { 9 | return null 10 | } 11 | if (typeof thing === 'function') { 12 | return thing 13 | } 14 | if (typeof thing === 'object' && thing.type === 'formatter') { 15 | return thing 16 | } 17 | depth-- 18 | if (depth > 0) { 19 | return findRecursive(thing.default, depth) 20 | } 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /src/formatter/fixtures/legacy_esm.mjs: -------------------------------------------------------------------------------- 1 | export default class Formatter {} 2 | -------------------------------------------------------------------------------- /src/formatter/fixtures/legacy_exports_dot_default.cjs: -------------------------------------------------------------------------------- 1 | class Formatter {} 2 | 3 | exports.default = Formatter 4 | -------------------------------------------------------------------------------- /src/formatter/fixtures/legacy_module_dot_exports.cjs: -------------------------------------------------------------------------------- 1 | class Formatter {} 2 | 3 | module.exports = Formatter 4 | -------------------------------------------------------------------------------- /src/formatter/fixtures/plugin_esm.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'formatter', 3 | formatter() {}, 4 | } 5 | -------------------------------------------------------------------------------- /src/formatter/fixtures/plugin_exports_dot_default.cjs: -------------------------------------------------------------------------------- 1 | exports.default = { 2 | type: 'formatter', 3 | formatter() {}, 4 | } 5 | -------------------------------------------------------------------------------- /src/formatter/fixtures/plugin_module_dot_exports.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'formatter', 3 | formatter() {}, 4 | } 5 | -------------------------------------------------------------------------------- /src/formatter/helpers/duration_helpers.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from '@cucumber/messages' 2 | 3 | const NANOS_IN_SECOND = 1_000_000_000 4 | 5 | export function durationToNanoseconds(duration: Duration): number { 6 | return Math.floor(duration.seconds * NANOS_IN_SECOND + duration.nanos) 7 | } 8 | -------------------------------------------------------------------------------- /src/formatter/helpers/duration_helpers_spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { durationToNanoseconds } from './duration_helpers' 3 | 4 | describe('duration helpers', () => { 5 | describe('durationToNanoseconds', () => { 6 | it('should convert under a second', () => { 7 | expect(durationToNanoseconds({ seconds: 0, nanos: 257344166 })).to.eq( 8 | 257344166 9 | ) 10 | }) 11 | 12 | it('should convert over a second', () => { 13 | expect(durationToNanoseconds({ seconds: 2, nanos: 1043459 })).to.eq( 14 | 2001043459 15 | ) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/formatter/helpers/formatters.ts: -------------------------------------------------------------------------------- 1 | import Formatter from '../.' 2 | import JsonFormatter from '../json_formatter' 3 | import ProgressBarFormatter from '../progress_bar_formatter' 4 | import ProgressFormatter from '../progress_formatter' 5 | import RerunFormatter from '../rerun_formatter' 6 | import SnippetsFormatter from '../snippets_formatter' 7 | import SummaryFormatter from '../summary_formatter' 8 | import UsageFormatter from '../usage_formatter' 9 | import UsageJsonFormatter from '../usage_json_formatter' 10 | 11 | const Formatters = { 12 | getFormatters(): Record { 13 | return { 14 | json: JsonFormatter, 15 | progress: ProgressFormatter, 16 | 'progress-bar': ProgressBarFormatter, 17 | rerun: RerunFormatter, 18 | snippets: SnippetsFormatter, 19 | summary: SummaryFormatter, 20 | usage: UsageFormatter, 21 | 'usage-json': UsageJsonFormatter, 22 | } 23 | }, 24 | } 25 | 26 | export default Formatters 27 | -------------------------------------------------------------------------------- /src/formatter/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as GherkinDocumentParser from './gherkin_document_parser' 2 | import * as PickleParser from './pickle_parser' 3 | 4 | export { parseTestCaseAttempt } from './test_case_attempt_parser' 5 | export { default as EventDataCollector } from './event_data_collector' 6 | export { KeywordType, getStepKeywordType } from './keyword_type' 7 | export { formatIssue, isWarning, isFailure, isIssue } from './issue_helpers' 8 | export { formatLocation } from './location_helpers' 9 | export { formatSummary } from './summary_helpers' 10 | export { getUsage } from './usage_helpers' 11 | export { GherkinDocumentParser, PickleParser } 12 | -------------------------------------------------------------------------------- /src/formatter/helpers/keyword_type.ts: -------------------------------------------------------------------------------- 1 | import { Dialect, dialects } from '@cucumber/gherkin' 2 | import { doesHaveValue } from '../../value_checker' 3 | 4 | export enum KeywordType { 5 | Precondition = 'precondition', 6 | Event = 'event', 7 | Outcome = 'outcome', 8 | } 9 | 10 | export interface IGetStepKeywordTypeOptions { 11 | keyword: string 12 | language: string 13 | previousKeywordType?: KeywordType 14 | } 15 | 16 | export function getStepKeywordType({ 17 | keyword, 18 | language, 19 | previousKeywordType, 20 | }: IGetStepKeywordTypeOptions): KeywordType { 21 | const dialect: Dialect = dialects[language] 22 | const stepKeywords = ['given', 'when', 'then', 'and', 'but'] as const 23 | const type = stepKeywords.find((key) => dialect[key].includes(keyword)) 24 | switch (type) { 25 | case 'when': 26 | return KeywordType.Event 27 | case 'then': 28 | return KeywordType.Outcome 29 | case 'and': 30 | case 'but': 31 | if (doesHaveValue(previousKeywordType)) { 32 | return previousKeywordType 33 | } 34 | // fallthrough 35 | default: 36 | return KeywordType.Precondition 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/formatter/helpers/location_helpers.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { doesHaveValue } from '../../value_checker' 3 | import { ILineAndUri } from '../../types' 4 | 5 | export function formatLocation(obj: ILineAndUri, cwd?: string): string { 6 | let uri = obj.uri 7 | if (doesHaveValue(cwd)) { 8 | uri = path.relative(cwd, uri) 9 | } 10 | return `${uri}:${obj.line.toString()}` 11 | } 12 | -------------------------------------------------------------------------------- /src/formatter/helpers/pickle_parser.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { getGherkinScenarioLocationMap } from './gherkin_document_parser' 3 | 4 | export interface IGetPickleLocationRequest { 5 | gherkinDocument: messages.GherkinDocument 6 | pickle: messages.Pickle 7 | } 8 | 9 | export interface IGetStepKeywordRequest { 10 | pickleStep: messages.PickleStep 11 | gherkinStepMap: Record 12 | } 13 | 14 | export interface IGetScenarioDescriptionRequest { 15 | pickle: messages.Pickle 16 | gherkinScenarioMap: Record 17 | } 18 | 19 | export function getScenarioDescription({ 20 | pickle, 21 | gherkinScenarioMap, 22 | }: IGetScenarioDescriptionRequest): string { 23 | return pickle.astNodeIds 24 | .map((id) => gherkinScenarioMap[id]) 25 | .filter((x) => x != null)[0].description 26 | } 27 | 28 | export function getStepKeyword({ 29 | pickleStep, 30 | gherkinStepMap, 31 | }: IGetStepKeywordRequest): string { 32 | return pickleStep.astNodeIds 33 | .map((id) => gherkinStepMap[id]) 34 | .filter((x) => x != null)[0].keyword 35 | } 36 | 37 | export function getPickleStepMap( 38 | pickle: messages.Pickle 39 | ): Record { 40 | const result: Record = {} 41 | pickle.steps.forEach((pickleStep) => (result[pickleStep.id] = pickleStep)) 42 | return result 43 | } 44 | 45 | export function getPickleLocation({ 46 | gherkinDocument, 47 | pickle, 48 | }: IGetPickleLocationRequest): messages.Location { 49 | const gherkinScenarioLocationMap = 50 | getGherkinScenarioLocationMap(gherkinDocument) 51 | return gherkinScenarioLocationMap[ 52 | pickle.astNodeIds[pickle.astNodeIds.length - 1] 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/formatter/helpers/step_argument_formatter.ts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3' 2 | import * as messages from '@cucumber/messages' 3 | import { parseStepArgument } from '../../step_arguments' 4 | 5 | function formatDataTable(dataTable: messages.PickleTable): string { 6 | const table = new Table({ 7 | chars: { 8 | bottom: '', 9 | 'bottom-left': '', 10 | 'bottom-mid': '', 11 | 'bottom-right': '', 12 | left: '|', 13 | 'left-mid': '', 14 | mid: '', 15 | 'mid-mid': '', 16 | middle: '|', 17 | right: '|', 18 | 'right-mid': '', 19 | top: '', 20 | 'top-left': '', 21 | 'top-mid': '', 22 | 'top-right': '', 23 | }, 24 | style: { 25 | border: [], 26 | 'padding-left': 1, 27 | 'padding-right': 1, 28 | }, 29 | }) 30 | const rows = dataTable.rows.map((row) => 31 | row.cells.map((cell) => 32 | cell.value.replace(/\\/g, '\\\\').replace(/\n/g, '\\n') 33 | ) 34 | ) 35 | table.push(...rows) 36 | return table.toString() 37 | } 38 | 39 | function formatDocString(docString: messages.PickleDocString): string { 40 | return `"""\n${docString.content}\n"""` 41 | } 42 | 43 | export function formatStepArgument(arg: messages.PickleStepArgument): string { 44 | return parseStepArgument(arg, { 45 | dataTable: formatDataTable, 46 | docString: formatDocString, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/formatter/import_code.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url' 2 | import path from 'node:path' 3 | 4 | export async function importCode(specifier: string, cwd: string): Promise { 5 | try { 6 | let normalized: URL | string = specifier 7 | if (specifier.startsWith('.')) { 8 | normalized = pathToFileURL(path.resolve(cwd, specifier)) 9 | } else if (specifier.startsWith('file://')) { 10 | normalized = new URL(specifier) 11 | } 12 | return await import(normalized.toString()) 13 | } catch (e) { 14 | throw new Error(`Failed to import formatter ${specifier}`, { 15 | cause: e, 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/formatter/progress_formatter.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { doesHaveValue } from '../value_checker' 3 | import SummaryFormatter from './summary_formatter' 4 | import { IFormatterOptions } from './index' 5 | import IEnvelope = messages.Envelope 6 | import ITestStepFinished = messages.TestStepFinished 7 | 8 | const STATUS_CHARACTER_MAPPING: Map = 9 | new Map([ 10 | [messages.TestStepResultStatus.AMBIGUOUS, 'A'], 11 | [messages.TestStepResultStatus.FAILED, 'F'], 12 | [messages.TestStepResultStatus.PASSED, '.'], 13 | [messages.TestStepResultStatus.PENDING, 'P'], 14 | [messages.TestStepResultStatus.SKIPPED, '-'], 15 | [messages.TestStepResultStatus.UNDEFINED, 'U'], 16 | ]) 17 | 18 | export default class ProgressFormatter extends SummaryFormatter { 19 | public static readonly documentation: string = 20 | 'Prints one character per scenario.' 21 | 22 | constructor(options: IFormatterOptions) { 23 | options.eventBroadcaster.on('envelope', (envelope: IEnvelope) => { 24 | if (doesHaveValue(envelope.testRunFinished)) { 25 | this.log('\n\n') 26 | } else if (doesHaveValue(envelope.testStepFinished)) { 27 | this.logProgress(envelope.testStepFinished) 28 | } 29 | }) 30 | super(options) 31 | } 32 | 33 | logProgress({ testStepResult: { status } }: ITestStepFinished): void { 34 | const character = this.colorFns.forStatus(status)( 35 | STATUS_CHARACTER_MAPPING.get(status) 36 | ) 37 | this.log(character) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/formatter/resolve_implementation.ts: -------------------------------------------------------------------------------- 1 | import builtin from './builtin' 2 | import { importCode } from './import_code' 3 | import { findClassOrPlugin } from './find_class_or_plugin' 4 | import { FormatterImplementation } from './index' 5 | 6 | export async function resolveImplementation( 7 | specifier: string, 8 | cwd: string 9 | ): Promise { 10 | const fromBuiltin = builtin[specifier] 11 | if (fromBuiltin) { 12 | if (typeof fromBuiltin !== 'string') { 13 | return fromBuiltin 14 | } else { 15 | specifier = fromBuiltin 16 | } 17 | } 18 | const imported = await importCode(specifier, cwd) 19 | const found = findClassOrPlugin(imported) 20 | if (!found) { 21 | throw new Error(`${specifier} does not export a function/class`) 22 | } 23 | return found 24 | } 25 | -------------------------------------------------------------------------------- /src/formatter/resolve_implementation_spec.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url' 2 | import path from 'node:path' 3 | import { expect } from 'chai' 4 | import { resolveImplementation } from './resolve_implementation' 5 | 6 | describe('resolveImplementation', () => { 7 | const varieties = [ 8 | 'esm.mjs', 9 | 'exports_dot_default.cjs', 10 | 'module_dot_exports.cjs', 11 | ] 12 | 13 | describe('legacy classes', () => { 14 | varieties.forEach((filename) => { 15 | describe(filename, () => { 16 | it('should handle a relative path', async () => { 17 | const CustomClass = await resolveImplementation( 18 | `./fixtures/legacy_${filename}`, 19 | __dirname 20 | ) 21 | 22 | expect(typeof CustomClass).to.eq('function') 23 | }) 24 | 25 | it('should handle a file:// url', async () => { 26 | const fileUrl = pathToFileURL( 27 | path.resolve(__dirname, `./fixtures/legacy_${filename}`) 28 | ).toString() 29 | const CustomClass = await resolveImplementation(fileUrl, __dirname) 30 | 31 | expect(typeof CustomClass).to.eq('function') 32 | }) 33 | }) 34 | }) 35 | }) 36 | 37 | describe('plugins', () => { 38 | varieties.forEach((filename) => { 39 | describe(filename, () => { 40 | it('should handle a relative path', async () => { 41 | const plugin = await resolveImplementation( 42 | `./fixtures/plugin_${filename}`, 43 | __dirname 44 | ) 45 | 46 | expect(typeof plugin).to.eq('object') 47 | }) 48 | 49 | it('should handle a file:// url', async () => { 50 | const fileUrl = pathToFileURL( 51 | path.resolve(__dirname, `./fixtures/plugin_${filename}`) 52 | ).toString() 53 | const plugin = await resolveImplementation(fileUrl, __dirname) 54 | 55 | expect(typeof plugin).to.eq('object') 56 | }) 57 | }) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/formatter/snippets_formatter.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { doesHaveValue } from '../value_checker' 3 | import { parseTestCaseAttempt } from './helpers' 4 | import Formatter, { IFormatterOptions } from './' 5 | import IEnvelope = messages.Envelope 6 | 7 | export default class SnippetsFormatter extends Formatter { 8 | public static readonly documentation: string = 9 | "The Snippets Formatter doesn't output anything regarding the test run; it just prints snippets to implement any undefined steps" 10 | 11 | constructor(options: IFormatterOptions) { 12 | super(options) 13 | options.eventBroadcaster.on('envelope', (envelope: IEnvelope) => { 14 | if (doesHaveValue(envelope.testRunFinished)) { 15 | this.logSnippets() 16 | } 17 | }) 18 | } 19 | 20 | logSnippets(): void { 21 | const snippets: string[] = [] 22 | this.eventDataCollector.getTestCaseAttempts().forEach((testCaseAttempt) => { 23 | const parsed = parseTestCaseAttempt({ 24 | snippetBuilder: this.snippetBuilder, 25 | supportCodeLibrary: this.supportCodeLibrary, 26 | testCaseAttempt, 27 | }) 28 | parsed.testSteps.forEach((testStep) => { 29 | if ( 30 | testStep.result.status === messages.TestStepResultStatus.UNDEFINED 31 | ) { 32 | snippets.push(testStep.snippet) 33 | } 34 | }) 35 | }) 36 | this.log(snippets.join('\n\n')) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/formatter/step_definition_snippet_builder/snippet_syntax.ts: -------------------------------------------------------------------------------- 1 | import { GeneratedExpression } from '@cucumber/cucumber-expressions' 2 | 3 | export enum SnippetInterface { 4 | AsyncAwait = 'async-await', 5 | Callback = 'callback', 6 | Promise = 'promise', 7 | Synchronous = 'synchronous', 8 | } 9 | 10 | export interface ISnippetSyntaxBuildOptions { 11 | comment: string 12 | functionName: string 13 | generatedExpressions: readonly GeneratedExpression[] 14 | stepParameterNames: string[] 15 | } 16 | 17 | export interface ISnippetSnytax { 18 | build: (options: ISnippetSyntaxBuildOptions) => string 19 | } 20 | -------------------------------------------------------------------------------- /src/formatter/usage_json_formatter.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { doesHaveValue } from '../value_checker' 3 | import { getUsage } from './helpers' 4 | import Formatter, { IFormatterOptions } from './' 5 | import IEnvelope = messages.Envelope 6 | 7 | export default class UsageJsonFormatter extends Formatter { 8 | public static readonly documentation: string = 9 | 'Does what the Usage Formatter does, but outputs JSON, which can be output to a file and then consumed by other tools.' 10 | 11 | constructor(options: IFormatterOptions) { 12 | super(options) 13 | options.eventBroadcaster.on('envelope', (envelope: IEnvelope) => { 14 | if (doesHaveValue(envelope.testRunFinished)) { 15 | this.logUsage() 16 | } 17 | }) 18 | } 19 | 20 | logUsage(): void { 21 | const usage = getUsage({ 22 | stepDefinitions: this.supportCodeLibrary.stepDefinitions, 23 | eventDataCollector: this.eventDataCollector, 24 | }) 25 | this.log(JSON.stringify(usage, this.replacer, 2)) 26 | } 27 | 28 | replacer(key: string, value: any): any { 29 | if (key === 'seconds') { 30 | return parseInt(value) 31 | } 32 | return value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/models/data_table.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | 3 | export default class DataTable { 4 | private readonly rawTable: string[][] 5 | 6 | constructor(sourceTable: messages.PickleTable | string[][]) { 7 | if (sourceTable instanceof Array) { 8 | this.rawTable = sourceTable 9 | } else { 10 | this.rawTable = sourceTable.rows.map((row) => 11 | row.cells.map((cell) => cell.value) 12 | ) 13 | } 14 | } 15 | 16 | hashes(): Record[] { 17 | const copy = this.raw() 18 | const keys = copy[0] 19 | const valuesArray = copy.slice(1) 20 | return valuesArray.map((values) => { 21 | const rowObject: Record = {} 22 | keys.forEach((key, index) => (rowObject[key] = values[index])) 23 | return rowObject 24 | }) 25 | } 26 | 27 | raw(): string[][] { 28 | return this.rawTable.slice(0) 29 | } 30 | 31 | rows(): string[][] { 32 | const copy = this.raw() 33 | copy.shift() 34 | return copy 35 | } 36 | 37 | rowsHash(): Record { 38 | const rows = this.raw() 39 | const everyRowHasTwoColumns = rows.every((row) => row.length === 2) 40 | if (!everyRowHasTwoColumns) { 41 | throw new Error( 42 | 'rowsHash can only be called on a data table where all rows have exactly two columns' 43 | ) 44 | } 45 | const result: Record = {} 46 | rows.forEach((x) => (result[x[0]] = x[1])) 47 | return result 48 | } 49 | 50 | transpose(): DataTable { 51 | const transposed = this.rawTable[0].map((x, i) => 52 | this.rawTable.map((y) => y[i]) 53 | ) 54 | return new DataTable(transposed) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/models/gherkin_step_keyword.ts: -------------------------------------------------------------------------------- 1 | export type GherkinStepKeyword = 'Unknown' | 'Given' | 'When' | 'Then' 2 | -------------------------------------------------------------------------------- /src/models/step_definition.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '@cucumber/cucumber-expressions' 2 | import { parseStepArgument } from '../step_arguments' 3 | import { doesHaveValue } from '../value_checker' 4 | import DataTable from './data_table' 5 | import Definition, { 6 | IDefinition, 7 | IGetInvocationDataRequest, 8 | IGetInvocationDataResponse, 9 | IStepDefinitionParameters, 10 | } from './definition' 11 | import { GherkinStepKeyword } from './gherkin_step_keyword' 12 | 13 | export default class StepDefinition extends Definition implements IDefinition { 14 | public readonly keyword: GherkinStepKeyword 15 | public readonly pattern: string | RegExp 16 | public readonly expression: Expression 17 | 18 | constructor(data: IStepDefinitionParameters) { 19 | super(data) 20 | this.keyword = data.keyword 21 | this.pattern = data.pattern 22 | this.expression = data.expression 23 | } 24 | 25 | async getInvocationParameters({ 26 | step, 27 | world, 28 | }: IGetInvocationDataRequest): Promise { 29 | const parameters = await Promise.all( 30 | this.expression.match(step.text).map((arg) => arg.getValue(world)) 31 | ) 32 | if (doesHaveValue(step.argument)) { 33 | const argumentParameter = parseStepArgument(step.argument, { 34 | dataTable: (arg) => new DataTable(arg), 35 | docString: (arg) => arg.content, 36 | }) 37 | parameters.push(argumentParameter) 38 | } 39 | return { 40 | getInvalidCodeLengthMessage: () => 41 | this.baseGetInvalidCodeLengthMessage(parameters), 42 | parameters, 43 | validCodeLengths: [parameters.length, parameters.length + 1], 44 | } 45 | } 46 | 47 | matchesStepName(stepName: string): boolean { 48 | return doesHaveValue(this.expression.match(stepName)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/models/test_case_hook_definition.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { PickleTagFilter } from '../pickle_filter' 3 | import Definition, { 4 | IDefinition, 5 | IDefinitionParameters, 6 | IGetInvocationDataRequest, 7 | IGetInvocationDataResponse, 8 | IHookDefinitionOptions, 9 | } from './definition' 10 | 11 | export default class TestCaseHookDefinition 12 | extends Definition 13 | implements IDefinition 14 | { 15 | public readonly name: string 16 | public readonly tagExpression: string 17 | private readonly pickleTagFilter: PickleTagFilter 18 | 19 | constructor(data: IDefinitionParameters) { 20 | super(data) 21 | this.name = data.options.name 22 | this.tagExpression = data.options.tags 23 | this.pickleTagFilter = new PickleTagFilter(data.options.tags) 24 | } 25 | 26 | appliesToTestCase(pickle: messages.Pickle): boolean { 27 | return this.pickleTagFilter.matchesAllTagExpressions(pickle) 28 | } 29 | 30 | async getInvocationParameters({ 31 | hookParameter, 32 | }: IGetInvocationDataRequest): Promise { 33 | return { 34 | getInvalidCodeLengthMessage: () => 35 | this.buildInvalidCodeLengthMessage('0 or 1', '2'), 36 | parameters: [hookParameter], 37 | validCodeLengths: [0, 1, 2], 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/models/test_run_hook_definition.ts: -------------------------------------------------------------------------------- 1 | import Definition from './definition' 2 | 3 | export default class TestRunHookDefinition extends Definition {} 4 | -------------------------------------------------------------------------------- /src/models/test_step_hook_definition.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { PickleTagFilter } from '../pickle_filter' 3 | import Definition, { 4 | IDefinition, 5 | IGetInvocationDataResponse, 6 | IGetInvocationDataRequest, 7 | IDefinitionParameters, 8 | IHookDefinitionOptions, 9 | } from './definition' 10 | 11 | export default class TestStepHookDefinition 12 | extends Definition 13 | implements IDefinition 14 | { 15 | public readonly tagExpression: string 16 | private readonly pickleTagFilter: PickleTagFilter 17 | 18 | constructor(data: IDefinitionParameters) { 19 | super(data) 20 | this.tagExpression = data.options.tags 21 | this.pickleTagFilter = new PickleTagFilter(data.options.tags) 22 | } 23 | 24 | appliesToTestCase(pickle: messages.Pickle): boolean { 25 | return this.pickleTagFilter.matchesAllTagExpressions(pickle) 26 | } 27 | 28 | async getInvocationParameters({ 29 | hookParameter, 30 | }: IGetInvocationDataRequest): Promise { 31 | return { 32 | getInvalidCodeLengthMessage: () => 33 | this.buildInvalidCodeLengthMessage('0 or 1', '2'), 34 | parameters: [hookParameter], 35 | validCodeLengths: [0, 1, 2], 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/paths/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paths' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /src/paths/types.ts: -------------------------------------------------------------------------------- 1 | export interface IResolvedPaths { 2 | unexpandedSourcePaths: string[] 3 | sourcePaths: string[] 4 | requirePaths: string[] 5 | importPaths: string[] 6 | } 7 | -------------------------------------------------------------------------------- /src/plugin/events.ts: -------------------------------------------------------------------------------- 1 | export const coordinatorVoidKeys = ['message', 'paths:resolve'] as const 2 | export const coordinatorTransformKeys = [ 3 | 'pickles:filter', 4 | 'pickles:order', 5 | ] as const 6 | -------------------------------------------------------------------------------- /src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './plugin_manager' 3 | -------------------------------------------------------------------------------- /src/publish/index.ts: -------------------------------------------------------------------------------- 1 | import { publishPlugin } from './publish_plugin' 2 | 3 | export default publishPlugin 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /src/publish/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options relating to publication to https://reports.cucumber.io 3 | * @public 4 | */ 5 | export interface IPublishConfig { 6 | /** 7 | * Base URL for the Cucumber Reports service 8 | */ 9 | url: string 10 | /** 11 | * Access token for the Cucumber Reports service 12 | */ 13 | token: string 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime/coordinator.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { Envelope, IdGenerator } from '@cucumber/messages' 3 | import { assembleTestCases, SourcedPickle } from '../assemble' 4 | import { SupportCodeLibrary } from '../support_code_library_builder/types' 5 | import { RuntimeAdapter } from './types' 6 | import { timestamp } from './stopwatch' 7 | import { Runtime } from './index' 8 | 9 | export class Coordinator implements Runtime { 10 | constructor( 11 | private eventBroadcaster: EventEmitter, 12 | private newId: IdGenerator.NewId, 13 | private sourcedPickles: ReadonlyArray, 14 | private supportCodeLibrary: SupportCodeLibrary, 15 | private adapter: RuntimeAdapter 16 | ) {} 17 | 18 | async run(): Promise { 19 | const testRunStartedId = this.newId() 20 | 21 | this.eventBroadcaster.emit('envelope', { 22 | testRunStarted: { 23 | id: testRunStartedId, 24 | timestamp: timestamp(), 25 | }, 26 | } satisfies Envelope) 27 | 28 | const assembledTestCases = await assembleTestCases( 29 | testRunStartedId, 30 | this.eventBroadcaster, 31 | this.newId, 32 | this.sourcedPickles, 33 | this.supportCodeLibrary 34 | ) 35 | 36 | const success = await this.adapter.run(assembledTestCases) 37 | 38 | this.eventBroadcaster.emit('envelope', { 39 | testRunFinished: { 40 | testRunStartedId, 41 | timestamp: timestamp(), 42 | success, 43 | }, 44 | } satisfies Envelope) 45 | 46 | return success 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/runtime/format_error.ts: -------------------------------------------------------------------------------- 1 | import { TestStepResult } from '@cucumber/messages' 2 | import { format } from 'assertion-error-formatter' 3 | import errorStackParser from 'error-stack-parser' 4 | import { filterStackTrace } from '../filter_stack_trace' 5 | 6 | export function formatError( 7 | error: Error, 8 | filterStackTraces: boolean 9 | ): Pick { 10 | let processedStackTrace: string 11 | try { 12 | const parsedStack = errorStackParser.parse(error) 13 | const filteredStack = filterStackTraces 14 | ? filterStackTrace(parsedStack) 15 | : parsedStack 16 | processedStackTrace = filteredStack.map((f) => f.source).join('\n') 17 | } catch { 18 | // if we weren't able to parse and process, we'll settle for the original 19 | } 20 | const message = format(error, { 21 | colorFns: { 22 | errorStack: (stack: string) => { 23 | return processedStackTrace ? `\n${processedStackTrace}` : stack 24 | }, 25 | }, 26 | }) 27 | return { 28 | message, 29 | exception: { 30 | type: error.name || 'Error', 31 | message: typeof error === 'string' ? error : error.message, 32 | stackTrace: processedStackTrace ?? error.stack, 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | export * from './make_runtime' 2 | export { Runtime, RuntimeOptions } from './types' 3 | -------------------------------------------------------------------------------- /src/runtime/make_runtime.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { IdGenerator } from '@cucumber/messages' 3 | import { IRunOptionsRuntime } from '../api' 4 | import { ILogger } from '../environment' 5 | import { SourcedPickle } from '../assemble' 6 | import { SupportCodeLibrary } from '../support_code_library_builder/types' 7 | import { IRunEnvironment } from '../environment' 8 | import { Runtime, RuntimeAdapter } from './types' 9 | import { ChildProcessAdapter } from './parallel/adapter' 10 | import { InProcessAdapter } from './serial/adapter' 11 | import { Coordinator } from './coordinator' 12 | 13 | export async function makeRuntime({ 14 | environment, 15 | logger, 16 | eventBroadcaster, 17 | sourcedPickles, 18 | newId, 19 | supportCodeLibrary, 20 | options, 21 | }: { 22 | environment: IRunEnvironment 23 | logger: ILogger 24 | eventBroadcaster: EventEmitter 25 | newId: IdGenerator.NewId 26 | sourcedPickles: ReadonlyArray 27 | supportCodeLibrary: SupportCodeLibrary 28 | options: IRunOptionsRuntime 29 | }): Promise { 30 | const adapter: RuntimeAdapter = 31 | options.parallel > 0 32 | ? new ChildProcessAdapter( 33 | environment, 34 | logger, 35 | eventBroadcaster, 36 | options, 37 | supportCodeLibrary 38 | ) 39 | : new InProcessAdapter( 40 | eventBroadcaster, 41 | newId, 42 | options, 43 | supportCodeLibrary 44 | ) 45 | return new Coordinator( 46 | eventBroadcaster, 47 | newId, 48 | sourcedPickles, 49 | supportCodeLibrary, 50 | adapter 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/runtime/parallel/run_worker.ts: -------------------------------------------------------------------------------- 1 | import { doesHaveValue } from '../../value_checker' 2 | import { ChildProcessWorker } from './worker' 3 | 4 | function run(): void { 5 | const exit = (exitCode: number, error?: Error, message?: string): void => { 6 | if (doesHaveValue(error)) { 7 | console.error(new Error(message, { cause: error })) // eslint-disable-line no-console 8 | } 9 | process.exit(exitCode) 10 | } 11 | const worker = new ChildProcessWorker({ 12 | id: process.env.CUCUMBER_WORKER_ID, 13 | sendMessage: (message: any) => process.send(message), 14 | cwd: process.cwd(), 15 | exit, 16 | }) 17 | process.on('message', (m: any): void => { 18 | worker 19 | .receiveMessage(m) 20 | .catch((error: Error) => 21 | exit(1, error, 'Unexpected error on worker.receiveMessage') 22 | ) 23 | }) 24 | } 25 | 26 | run() 27 | -------------------------------------------------------------------------------- /src/runtime/parallel/types.ts: -------------------------------------------------------------------------------- 1 | import { Envelope } from '@cucumber/messages' 2 | import { RuntimeOptions } from '../index' 3 | import { ISupportCodeCoordinates } from '../../api' 4 | import { AssembledTestCase } from '../../assemble' 5 | import { CanonicalSupportCodeIds } from '../../support_code_library_builder/types' 6 | 7 | // Messages from Coordinator to Worker 8 | 9 | export type CoordinatorToWorkerCommand = 10 | | InitializeCommand 11 | | RunCommand 12 | | FinalizeCommand 13 | 14 | export interface InitializeCommand { 15 | type: 'INITIALIZE' 16 | supportCodeCoordinates: ISupportCodeCoordinates 17 | supportCodeIds: CanonicalSupportCodeIds 18 | options: RuntimeOptions 19 | } 20 | 21 | export interface RunCommand { 22 | type: 'RUN' 23 | assembledTestCase: AssembledTestCase 24 | failing: boolean 25 | } 26 | 27 | export interface FinalizeCommand { 28 | type: 'FINALIZE' 29 | } 30 | 31 | // Messages from Worker to Coordinator 32 | 33 | export type WorkerToCoordinatorEvent = 34 | | ReadyEvent 35 | | EnvelopeEvent 36 | | FinishedEvent 37 | 38 | export interface ReadyEvent { 39 | type: 'READY' 40 | } 41 | 42 | export interface EnvelopeEvent { 43 | type: 'ENVELOPE' 44 | envelope: Envelope 45 | } 46 | 47 | export interface FinishedEvent { 48 | type: 'FINISHED' 49 | success: boolean 50 | } 51 | -------------------------------------------------------------------------------- /src/runtime/run_test_run_hooks.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from 'type-fest' 2 | import UserCodeRunner from '../user_code_runner' 3 | import { formatLocation } from '../formatter/helpers' 4 | import { doesHaveValue, valueOrDefault } from '../value_checker' 5 | import TestRunHookDefinition from '../models/test_run_hook_definition' 6 | import { runInTestRunScope } from './scope' 7 | 8 | export type RunsTestRunHooks = ( 9 | definitions: TestRunHookDefinition[], 10 | name: string 11 | ) => Promise 12 | 13 | export const makeRunTestRunHooks = ( 14 | dryRun: boolean, 15 | defaultTimeout: number, 16 | worldParameters: JsonObject, 17 | errorMessage: (name: string, location: string) => string 18 | ): RunsTestRunHooks => 19 | dryRun 20 | ? async () => {} 21 | : async (definitions, name) => { 22 | const context = { parameters: worldParameters } 23 | for (const hookDefinition of definitions) { 24 | const { error } = await runInTestRunScope({ context }, () => 25 | UserCodeRunner.run({ 26 | argsArray: [], 27 | fn: hookDefinition.code, 28 | thisArg: context, 29 | timeoutInMilliseconds: valueOrDefault( 30 | hookDefinition.options.timeout, 31 | defaultTimeout 32 | ), 33 | }) 34 | ) 35 | if (doesHaveValue(error)) { 36 | const location = formatLocation(hookDefinition) 37 | throw new Error(errorMessage(name, location), { cause: error }) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/runtime/scope/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test_case_scope' 2 | export * from './test_run_scope' 3 | -------------------------------------------------------------------------------- /src/runtime/scope/make_proxy.ts: -------------------------------------------------------------------------------- 1 | export function makeProxy(getThing: () => any): T { 2 | return new Proxy( 3 | {}, 4 | { 5 | defineProperty(_, property, attributes) { 6 | return Reflect.defineProperty(getThing(), property, attributes) 7 | }, 8 | deleteProperty(_, property) { 9 | return Reflect.get(getThing(), property) 10 | }, 11 | get(_, property) { 12 | return Reflect.get(getThing(), property, getThing()) 13 | }, 14 | getOwnPropertyDescriptor(_, property) { 15 | return Reflect.getOwnPropertyDescriptor(getThing(), property) 16 | }, 17 | getPrototypeOf(_) { 18 | return Reflect.getPrototypeOf(getThing()) 19 | }, 20 | has(_, key) { 21 | return Reflect.has(getThing(), key) 22 | }, 23 | isExtensible(_) { 24 | return Reflect.isExtensible(getThing()) 25 | }, 26 | ownKeys(_) { 27 | return Reflect.ownKeys(getThing()) 28 | }, 29 | preventExtensions(_) { 30 | return Reflect.preventExtensions(getThing()) 31 | }, 32 | set(_, property, value) { 33 | return Reflect.set(getThing(), property, value, getThing()) 34 | }, 35 | setPrototypeOf(_, proto) { 36 | return Reflect.setPrototypeOf(getThing(), proto) 37 | }, 38 | } 39 | ) as T 40 | } 41 | -------------------------------------------------------------------------------- /src/runtime/scope/test_case_scope.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'node:async_hooks' 2 | import { IWorld } from '../../support_code_library_builder/world' 3 | import { makeProxy } from './make_proxy' 4 | 5 | interface TestCaseScopeStore { 6 | world: IWorld 7 | } 8 | 9 | const testCaseScope = new AsyncLocalStorage() 10 | 11 | export async function runInTestCaseScope( 12 | store: TestCaseScopeStore, 13 | callback: () => ResponseType 14 | ) { 15 | return testCaseScope.run(store, callback) 16 | } 17 | 18 | function getWorld(): IWorld { 19 | const store = testCaseScope.getStore() 20 | if (!store) { 21 | throw new Error( 22 | 'Attempted to access `world` from incorrect scope; only applicable to steps and case-level hooks' 23 | ) 24 | } 25 | return store.world as IWorld 26 | } 27 | 28 | /** 29 | * A proxy to the World instance for the currently-executing test case 30 | * 31 | * @beta 32 | * @remarks 33 | * Useful for getting a handle on the World when using arrow functions and thus 34 | * being unable to rely on the value of `this`. Only callable from the body of a 35 | * step or a `Before`, `After`, `BeforeStep` or `AfterStep` hook (will throw 36 | * otherwise). 37 | */ 38 | export const worldProxy = makeProxy(getWorld) 39 | -------------------------------------------------------------------------------- /src/runtime/scope/test_case_scope_spec.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import { expect } from 'chai' 3 | import World from '../../support_code_library_builder/world' 4 | import { ICreateAttachment } from '../attachment_manager' 5 | import { IFormatterLogFn } from '../../formatter' 6 | import { runInTestCaseScope, worldProxy } from './test_case_scope' 7 | 8 | describe('testCaseScope', () => { 9 | class CustomWorld extends World { 10 | firstNumber: number = 0 11 | secondNumber: number = 0 12 | 13 | get numbers() { 14 | return [this.firstNumber, this.secondNumber] 15 | } 16 | 17 | sum() { 18 | return this.firstNumber + this.secondNumber 19 | } 20 | } 21 | 22 | it('provides a proxy to the world that works when running a test case', async () => { 23 | const customWorld = new CustomWorld({ 24 | attach: sinon.stub() as unknown as ICreateAttachment, 25 | log: sinon.stub() as IFormatterLogFn, 26 | link: sinon.stub() as (url: string) => void, 27 | parameters: {}, 28 | }) 29 | const customProxy = worldProxy as CustomWorld 30 | 31 | await runInTestCaseScope({ world: customWorld }, () => { 32 | // simple property access 33 | customProxy.firstNumber = 1 34 | customProxy.secondNumber = 2 35 | expect(customProxy.firstNumber).to.eq(1) 36 | expect(customProxy.secondNumber).to.eq(2) 37 | // getters using internal state 38 | expect(customProxy.numbers).to.deep.eq([1, 2]) 39 | // instance methods using internal state 40 | expect(customProxy.sum()).to.eq(3) 41 | // enumeration 42 | expect(Object.keys(customProxy)).to.deep.eq([ 43 | 'attach', 44 | 'log', 45 | 'link', 46 | 'parameters', 47 | 'firstNumber', 48 | 'secondNumber', 49 | ]) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/runtime/scope/test_run_scope.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'node:async_hooks' 2 | import { IContext } from '../../support_code_library_builder/context' 3 | import { makeProxy } from './make_proxy' 4 | 5 | interface TestRunScopeStore { 6 | context: IContext 7 | } 8 | 9 | const testRunScope = new AsyncLocalStorage() 10 | 11 | export async function runInTestRunScope( 12 | store: TestRunScopeStore, 13 | callback: () => ResponseType 14 | ) { 15 | return testRunScope.run(store, callback) 16 | } 17 | 18 | function getContext(): IContext { 19 | const store = testRunScope.getStore() 20 | if (!store) { 21 | throw new Error( 22 | 'Attempted to access `context` from incorrect scope; only applicable to run-level hooks' 23 | ) 24 | } 25 | return store.context as IContext 26 | } 27 | 28 | /** 29 | * A proxy to the context for the currently-executing test run. 30 | * 31 | * @beta 32 | * @remarks 33 | * Useful for getting a handle on the context when using arrow functions and thus 34 | * being unable to rely on the value of `this`. Only callable from the body of a 35 | * `BeforeAll` or `AfterAll` hook (will throw otherwise). 36 | */ 37 | export const contextProxy = makeProxy(getContext) 38 | -------------------------------------------------------------------------------- /src/runtime/scope/test_run_scope_spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { contextProxy, runInTestRunScope } from './test_run_scope' 3 | 4 | describe('testRunScope', () => { 5 | it('provides a proxy to the context that works when running a test run hook', async () => { 6 | const context = { 7 | parameters: { 8 | foo: 1, 9 | bar: 2, 10 | }, 11 | } 12 | 13 | await runInTestRunScope({ context }, () => { 14 | // simple property access 15 | expect(contextProxy.parameters.foo).to.eq(1) 16 | contextProxy.parameters.foo = 'baz' 17 | expect(contextProxy.parameters.foo).to.eq('baz') 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/runtime/serial/adapter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { IdGenerator } from '@cucumber/messages' 3 | import { RuntimeAdapter } from '../types' 4 | import { AssembledTestCase } from '../../assemble' 5 | import { Worker } from '../worker' 6 | import { RuntimeOptions } from '../index' 7 | import { SupportCodeLibrary } from '../../support_code_library_builder/types' 8 | 9 | export class InProcessAdapter implements RuntimeAdapter { 10 | private readonly worker: Worker 11 | private failing: boolean = false 12 | 13 | constructor( 14 | eventBroadcaster: EventEmitter, 15 | newId: IdGenerator.NewId, 16 | options: RuntimeOptions, 17 | supportCodeLibrary: SupportCodeLibrary 18 | ) { 19 | this.worker = new Worker( 20 | undefined, 21 | eventBroadcaster, 22 | newId, 23 | options, 24 | supportCodeLibrary 25 | ) 26 | } 27 | 28 | async run( 29 | assembledTestCases: ReadonlyArray 30 | ): Promise { 31 | await this.worker.runBeforeAllHooks() 32 | for (const item of assembledTestCases) { 33 | const success = await this.worker.runTestCase(item, this.failing) 34 | if (!success) { 35 | this.failing = true 36 | } 37 | } 38 | await this.worker.runAfterAllHooks() 39 | return !this.failing 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/runtime/stopwatch.ts: -------------------------------------------------------------------------------- 1 | import { Duration, TimeConversion } from '@cucumber/messages' 2 | import methods from '../time' 3 | 4 | /** 5 | * A utility for timing test run operations and returning duration and 6 | * timestamp objects in messages-compatible formats 7 | */ 8 | export interface IStopwatch { 9 | start: () => IStopwatch 10 | stop: () => IStopwatch 11 | duration: () => Duration 12 | } 13 | 14 | class StopwatchImpl implements IStopwatch { 15 | private started: number 16 | 17 | constructor(private base: Duration = { seconds: 0, nanos: 0 }) {} 18 | 19 | start(): IStopwatch { 20 | this.started = methods.performance.now() 21 | return this 22 | } 23 | 24 | stop(): IStopwatch { 25 | this.base = this.duration() 26 | this.started = undefined 27 | return this 28 | } 29 | 30 | duration(): Duration { 31 | if (typeof this.started !== 'number') { 32 | return this.base 33 | } 34 | return TimeConversion.addDurations( 35 | this.base, 36 | TimeConversion.millisecondsToDuration( 37 | methods.performance.now() - this.started 38 | ) 39 | ) 40 | } 41 | } 42 | 43 | export const create = (base?: Duration): IStopwatch => new StopwatchImpl(base) 44 | 45 | export const timestamp = () => 46 | TimeConversion.millisecondsSinceEpochToTimestamp(methods.Date.now()) 47 | -------------------------------------------------------------------------------- /src/runtime/stopwatch_spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha' 2 | import { expect } from 'chai' 3 | import { TimeConversion } from '@cucumber/messages' 4 | import { create, timestamp } from './stopwatch' 5 | 6 | describe('stopwatch', () => { 7 | it('returns a duration between the start and stop', async () => { 8 | const stopwatch = create() 9 | stopwatch.start() 10 | await new Promise((resolve) => setTimeout(resolve, 1200)) 11 | stopwatch.stop() 12 | expect( 13 | TimeConversion.durationToMilliseconds(stopwatch.duration()) 14 | ).to.be.closeTo(1200, 50) 15 | }) 16 | 17 | it('accounts for an initial duration', async () => { 18 | const stopwatch = create(TimeConversion.millisecondsToDuration(300)) 19 | stopwatch.start() 20 | await new Promise((resolve) => setTimeout(resolve, 200)) 21 | stopwatch.stop() 22 | expect( 23 | TimeConversion.durationToMilliseconds(stopwatch.duration()) 24 | ).to.be.closeTo(500, 50) 25 | }) 26 | 27 | it('returns accurate durations ad-hoc if not stopped', async () => { 28 | const stopwatch = create() 29 | stopwatch.start() 30 | await new Promise((resolve) => setTimeout(resolve, 200)) 31 | expect( 32 | TimeConversion.durationToMilliseconds(stopwatch.duration()) 33 | ).to.be.closeTo(200, 50) 34 | await new Promise((resolve) => setTimeout(resolve, 200)) 35 | stopwatch.stop() 36 | expect( 37 | TimeConversion.durationToMilliseconds(stopwatch.duration()) 38 | ).to.be.closeTo(400, 50) 39 | }) 40 | 41 | it('returns 0 duration if never started', async () => { 42 | const stopwatch = create() 43 | await new Promise((resolve) => setTimeout(resolve, 200)) 44 | stopwatch.stop() 45 | expect(TimeConversion.durationToMilliseconds(stopwatch.duration())).to.eq(0) 46 | }) 47 | 48 | it('returns a timestamp close to now', () => { 49 | expect( 50 | TimeConversion.timestampToMillisecondsSinceEpoch(timestamp()) 51 | ).to.be.closeTo(Date.now(), 100) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/runtime/types.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from 'type-fest' 2 | import { AssembledTestCase } from '../assemble' 3 | 4 | export interface RuntimeOptions { 5 | dryRun: boolean 6 | failFast: boolean 7 | filterStacktraces: boolean 8 | retry: number 9 | retryTagFilter: string 10 | strict: boolean 11 | worldParameters: JsonObject 12 | } 13 | 14 | export interface Runtime { 15 | run: () => Promise 16 | } 17 | 18 | export interface RuntimeAdapter { 19 | run(assembledTestCases: ReadonlyArray): Promise 20 | } 21 | -------------------------------------------------------------------------------- /src/step_arguments.ts: -------------------------------------------------------------------------------- 1 | import util from 'node:util' 2 | import * as messages from '@cucumber/messages' 3 | import { doesHaveValue } from './value_checker' 4 | 5 | export interface IPickleStepArgumentFunctionMap { 6 | dataTable: (arg: messages.PickleTable) => T 7 | docString: (arg: messages.PickleDocString) => T 8 | } 9 | 10 | export function parseStepArgument( 11 | arg: messages.PickleStepArgument, 12 | mapping: IPickleStepArgumentFunctionMap 13 | ): T { 14 | if (doesHaveValue(arg.dataTable)) { 15 | return mapping.dataTable(arg.dataTable) 16 | } else if (doesHaveValue(arg.docString)) { 17 | return mapping.docString(arg.docString) 18 | } 19 | throw new Error(`Unknown step argument: ${util.inspect(arg)}`) 20 | } 21 | -------------------------------------------------------------------------------- /src/support_code_library_builder/build_parameter_type.ts: -------------------------------------------------------------------------------- 1 | import { ParameterType } from '@cucumber/cucumber-expressions' 2 | import { IParameterTypeDefinition } from './types' 3 | 4 | export function buildParameterType({ 5 | name, 6 | regexp, 7 | transformer, 8 | useForSnippets, 9 | preferForRegexpMatch, 10 | }: IParameterTypeDefinition): ParameterType { 11 | if (typeof useForSnippets !== 'boolean') useForSnippets = true 12 | if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false 13 | return new ParameterType( 14 | name, 15 | regexp, 16 | null, 17 | transformer, 18 | useForSnippets, 19 | preferForRegexpMatch 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/support_code_library_builder/context.ts: -------------------------------------------------------------------------------- 1 | export interface IContext { 2 | readonly parameters: ParametersType 3 | } 4 | -------------------------------------------------------------------------------- /src/support_code_library_builder/get_definition_line_and_uri.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import errorStackParser, { StackFrame } from 'error-stack-parser' 3 | import { isFileNameInCucumber } from '../filter_stack_trace' 4 | import { doesHaveValue, valueOrDefault } from '../value_checker' 5 | import { ILineAndUri } from '../types' 6 | 7 | export function getDefinitionLineAndUri( 8 | cwd: string, 9 | isExcluded = isFileNameInCucumber 10 | ): ILineAndUri { 11 | let line: number 12 | let uri: string 13 | const stackframes: StackFrame[] = errorStackParser.parse(new Error()) 14 | const stackframe = stackframes.find( 15 | (frame: StackFrame) => 16 | frame.fileName !== __filename && !isExcluded(frame.fileName) 17 | ) 18 | if (stackframe != null) { 19 | line = stackframe.getLineNumber() 20 | uri = stackframe.getFileName() 21 | if (doesHaveValue(uri)) { 22 | uri = path.relative(cwd, uri) 23 | } 24 | } 25 | 26 | return { 27 | line: valueOrDefault(line, 0), 28 | uri: valueOrDefault(uri, 'unknown'), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/support_code_library_builder/get_definition_line_and_uri_spec.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import path from 'node:path' 3 | import { getDefinitionLineAndUri } from './get_definition_line_and_uri' 4 | 5 | describe(getDefinitionLineAndUri.name, () => { 6 | it('correctly gets the filename of the caller', () => { 7 | const includeAnyFile = (): boolean => false 8 | const { uri, line } = getDefinitionLineAndUri('.', includeAnyFile) 9 | assert.strictEqual( 10 | path.normalize(uri), 11 | path.normalize( 12 | 'src/support_code_library_builder/get_definition_line_and_uri_spec.ts' 13 | ) 14 | ) 15 | assert.strictEqual(line, 8) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/support_code_library_builder/parallel_can_assign_helpers.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import { ParallelAssignmentValidator } from './types' 3 | 4 | function hasTag(pickle: messages.Pickle, tagName: string): boolean { 5 | return pickle.tags.some((t) => t.name == tagName) 6 | } 7 | 8 | export function atMostOnePicklePerTag( 9 | tagNames: string[] 10 | ): ParallelAssignmentValidator { 11 | return (inQuestion: messages.Pickle, inProgress: messages.Pickle[]) => { 12 | return tagNames.every((tagName) => { 13 | return ( 14 | !hasTag(inQuestion, tagName) || 15 | inProgress.every((p) => !hasTag(p, tagName)) 16 | ) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/support_code_library_builder/sourced_parameter_type_registry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ParameterType, 3 | ParameterTypeRegistry, 4 | } from '@cucumber/cucumber-expressions' 5 | import { ILineAndUri } from '../types' 6 | 7 | export class SourcedParameterTypeRegistry extends ParameterTypeRegistry { 8 | private parameterTypeToSource: WeakMap, ILineAndUri> = 9 | new WeakMap() 10 | 11 | defineSourcedParameterType( 12 | parameterType: ParameterType, 13 | source: ILineAndUri 14 | ) { 15 | this.defineParameterType(parameterType) 16 | this.parameterTypeToSource.set(parameterType, source) 17 | } 18 | 19 | lookupSource(parameterType: ParameterType) { 20 | return this.parameterTypeToSource.get(parameterType) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/support_code_library_builder/world.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICreateAttachment, 3 | ICreateLink, 4 | ICreateLog, 5 | } from '../runtime/attachment_manager' 6 | 7 | export interface IWorldOptions { 8 | attach: ICreateAttachment 9 | log: ICreateLog 10 | link: ICreateLink 11 | parameters: ParametersType 12 | } 13 | 14 | export interface IWorld { 15 | readonly attach: ICreateAttachment 16 | readonly log: ICreateLog 17 | readonly link: ICreateLink 18 | readonly parameters: ParametersType 19 | 20 | [key: string]: any 21 | } 22 | 23 | export default class World 24 | implements IWorld 25 | { 26 | public readonly attach: ICreateAttachment 27 | public readonly log: ICreateLog 28 | public readonly link: ICreateLink 29 | public readonly parameters: ParametersType 30 | 31 | constructor({ 32 | attach, 33 | log, 34 | link, 35 | parameters, 36 | }: IWorldOptions) { 37 | this.attach = attach 38 | this.log = log 39 | this.link = link 40 | this.parameters = parameters 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/time_spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha' 2 | import { expect } from 'chai' 3 | import { wrapPromiseWithTimeout } from './time' 4 | 5 | describe('wrapPromiseWithTimeout()', () => { 6 | describe('promise times out (default timeout message)', () => { 7 | it('rejects the promise', async () => { 8 | // Arrange 9 | const promise = new Promise((resolve) => { 10 | setTimeout(resolve, 50) 11 | }) 12 | 13 | // Act 14 | let error: Error = null 15 | try { 16 | await wrapPromiseWithTimeout(promise, 25) 17 | } catch (e) { 18 | error = e 19 | } 20 | 21 | // Assert 22 | expect(error).to.exist() 23 | expect(error.message).to.eql( 24 | 'Action did not complete within 25 milliseconds' 25 | ) 26 | }) 27 | }) 28 | 29 | describe('promise times out (supplied timeout message)', () => { 30 | it('rejects the promise', async () => { 31 | // Arrange 32 | const promise = new Promise((resolve) => { 33 | setTimeout(resolve, 50) 34 | }) 35 | 36 | // Act 37 | let error: Error = null 38 | try { 39 | await wrapPromiseWithTimeout(promise, 25, 'custom timeout message') 40 | } catch (e) { 41 | error = e 42 | } 43 | 44 | // Assert 45 | expect(error).to.exist() 46 | expect(error.message).to.eql('custom timeout message') 47 | }) 48 | }) 49 | 50 | describe('promise does not time out', () => { 51 | it('resolves the promise', async () => { 52 | // Arrange 53 | const promise = new Promise((resolve) => { 54 | setTimeout(() => resolve('value'), 10) 55 | }) 56 | 57 | // Act 58 | const result = await wrapPromiseWithTimeout(promise, 25) 59 | 60 | // Assert 61 | expect(result).to.eql('value') 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/try_require.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a try guarded require call that will throw a more detailed error when 3 | * the ERR_REQUIRE_ESM error code is encountered. 4 | * 5 | * @param {string} path File path to require from. 6 | */ 7 | export default function tryRequire(path: string) { 8 | try { 9 | return require(path) 10 | } catch (error) { 11 | if (error.code === 'ERR_REQUIRE_ESM') { 12 | throw Error( 13 | `Cucumber expected a CommonJS module at '${path}' but found an ES module. 14 | Either change the file to CommonJS syntax or use the --import directive instead of --require.`, 15 | { cause: error } 16 | ) 17 | } else if (error.code === 'ERR_REQUIRE_ASYNC_MODULE') { 18 | throw Error( 19 | `Cucumber expected a CommonJS module or simple ES module at '${path}' but found an async ES module. 20 | Either change the file so it can be required or use the --import directive instead of --require.`, 21 | { cause: error } 22 | ) 23 | } else { 24 | throw error 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/types/assertion-error-formatter/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'assertion-error-formatter' { 2 | export function format(error: Error, options?: any): string 3 | } 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ILineAndUri { 2 | line: number 3 | uri: string 4 | } 5 | -------------------------------------------------------------------------------- /src/types/is-generator/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'is-generator' { 2 | export function fn(code: Function): boolean 3 | } 4 | -------------------------------------------------------------------------------- /src/types/knuth-shuffle-seeded/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'knuth-shuffle-seeded' { 2 | export default function shuffle(inputArray: T[], seed?: string): T[] 3 | } 4 | -------------------------------------------------------------------------------- /src/types/stack-chain/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'stack-chain' { 2 | const _temp: any 3 | export = _temp 4 | } 5 | -------------------------------------------------------------------------------- /src/types/supports-color/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'supports-color' { 2 | import { Writable } from 'node:stream' 3 | 4 | export interface Options { 5 | readonly sniffFlags?: boolean 6 | } 7 | 8 | export type ColorSupportLevel = 0 | 1 | 2 | 3 9 | 10 | export interface ColorSupport { 11 | level: ColorSupportLevel 12 | } 13 | 14 | export type ColorInfo = ColorSupport | false 15 | 16 | export function supportsColor(stream: Writable, options?: Options): ColorInfo 17 | } 18 | -------------------------------------------------------------------------------- /src/uncaught_exception_manager.ts: -------------------------------------------------------------------------------- 1 | import UncaughtExceptionListener = NodeJS.UncaughtExceptionListener 2 | 3 | const UncaughtExceptionManager = { 4 | registerHandler(handler: UncaughtExceptionListener): void { 5 | process.addListener('uncaughtException', handler) 6 | }, 7 | 8 | unregisterHandler(handler: UncaughtExceptionListener): void { 9 | process.removeListener('uncaughtException', handler) 10 | }, 11 | } 12 | 13 | export default UncaughtExceptionManager 14 | -------------------------------------------------------------------------------- /src/value_checker.ts: -------------------------------------------------------------------------------- 1 | export function doesHaveValue(value: T): boolean { 2 | return !doesNotHaveValue(value) 3 | } 4 | 5 | export function doesNotHaveValue(value: T): boolean { 6 | return value === null || value === undefined 7 | } 8 | 9 | export function valueOrDefault(value: T, defaultValue: T): T { 10 | if (doesHaveValue(value)) { 11 | return value 12 | } 13 | return defaultValue 14 | } 15 | -------------------------------------------------------------------------------- /src/wrapper.mjs: -------------------------------------------------------------------------------- 1 | import cucumber from './index.js' 2 | 3 | export const version = cucumber.version 4 | 5 | export const supportCodeLibraryBuilder = cucumber.supportCodeLibraryBuilder 6 | export const Status = cucumber.Status 7 | export const DataTable = cucumber.DataTable 8 | export const TestCaseHookDefinition = cucumber.TestCaseHookDefinition 9 | 10 | export const Formatter = cucumber.Formatter 11 | export const FormatterBuilder = cucumber.FormatterBuilder 12 | export const JsonFormatter = cucumber.JsonFormatter 13 | export const ProgressFormatter = cucumber.ProgressFormatter 14 | export const RerunFormatter = cucumber.RerunFormatter 15 | export const SnippetsFormatter = cucumber.SnippetsFormatter 16 | export const SummaryFormatter = cucumber.SummaryFormatter 17 | export const UsageFormatter = cucumber.UsageFormatter 18 | export const UsageJsonFormatter = cucumber.UsageJsonFormatter 19 | export const formatterHelpers = cucumber.formatterHelpers 20 | 21 | export const After = cucumber.After 22 | export const AfterAll = cucumber.AfterAll 23 | export const AfterStep = cucumber.AfterStep 24 | export const Before = cucumber.Before 25 | export const BeforeAll = cucumber.BeforeAll 26 | export const BeforeStep = cucumber.BeforeStep 27 | export const defineStep = cucumber.defineStep 28 | export const defineParameterType = cucumber.defineParameterType 29 | export const Given = cucumber.Given 30 | export const setDefaultTimeout = cucumber.setDefaultTimeout 31 | export const setDefinitionFunctionWrapper = 32 | cucumber.setDefinitionFunctionWrapper 33 | export const setWorldConstructor = cucumber.setWorldConstructor 34 | export const setParallelCanAssign = cucumber.setParallelCanAssign 35 | export const Then = cucumber.Then 36 | export const When = cucumber.When 37 | export const World = cucumber.World 38 | export const world = cucumber.world 39 | export const context = cucumber.context 40 | export const parallelCanAssignHelpers = cucumber.parallelCanAssignHelpers 41 | 42 | export const wrapPromiseWithTimeout = cucumber.wrapPromiseWithTimeout 43 | 44 | // Deprecated 45 | export const Cli = cucumber.Cli 46 | -------------------------------------------------------------------------------- /test-d/api.ts: -------------------------------------------------------------------------------- 1 | import { IRunEnvironment } from '../api' 2 | 3 | // environment variables accepts an object literal 4 | const fromLiteral: IRunEnvironment = { 5 | env: { 6 | FOO: 'BAR', 7 | }, 8 | } 9 | 10 | // environment variables accepts a NodeJS.ProcessEnv 11 | const fromProcessEnv: IRunEnvironment = { 12 | env: process.env, 13 | } 14 | -------------------------------------------------------------------------------- /test-d/attachments.ts: -------------------------------------------------------------------------------- 1 | import { After } from '../' 2 | import { expectError, expectType } from 'tsd' 3 | import { PassThrough } from 'stream' 4 | 5 | After(async function () { 6 | // log 7 | expectType(this.log('things')) 8 | // string 9 | expectType(this.attach('stuff')) 10 | expectType(this.attach('{}', 'application/json')) 11 | // buffer 12 | expectType(this.attach(Buffer.from('{}'), 'application/json')) 13 | // stream 14 | expectType>(this.attach(new PassThrough(), 'application/json')) 15 | expectType( 16 | this.attach(new PassThrough(), 'application/json', () => undefined) 17 | ) 18 | // buffer and stream flavours must specify media type 19 | expectError(this.attach(Buffer.from('{}'))) 20 | expectError(this.attach(new PassThrough())) 21 | }) 22 | -------------------------------------------------------------------------------- /test-d/hooks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | After, 3 | AfterAll, 4 | AfterStep, 5 | Before, 6 | BeforeAll, 7 | BeforeStep, 8 | ITestCaseHookParameter, 9 | ITestStepHookParameter, 10 | } from '../' 11 | 12 | // should allow argument-less hooks 13 | BeforeAll(function () {}) 14 | AfterAll(function () {}) 15 | Before(function () {}) 16 | After(function () {}) 17 | BeforeStep(function () {}) 18 | AfterStep(function () {}) 19 | 20 | // should allow hook functions to be async 21 | BeforeAll(async function () {}) 22 | AfterAll(async function () {}) 23 | Before(async function () {}) 24 | After(async function () {}) 25 | BeforeStep(async function () {}) 26 | AfterStep(async function () {}) 27 | 28 | // should allow accessing world parameters in global hooks 29 | BeforeAll(function () { 30 | this.parameters.foo = 1 31 | }) 32 | AfterAll(function () { 33 | this.parameters.foo = 1 34 | }) 35 | 36 | // should allow typed arguments in hooks 37 | Before(function (param: ITestCaseHookParameter) {}) 38 | After(function (param: ITestCaseHookParameter) {}) 39 | BeforeStep(function (param: ITestStepHookParameter) {}) 40 | AfterStep(function (param: ITestStepHookParameter) {}) 41 | 42 | // should allow an object with tags and/or name in hooks 43 | Before({ tags: '@foo', name: 'before hook' }, function () {}) 44 | After({ tags: '@foo', name: 'after hook' }, function () {}) 45 | 46 | // should allow us to return 'skipped' from a test case hook 47 | Before(async function () { 48 | return 'skipped' 49 | }) 50 | After(async function () { 51 | return 'skipped' 52 | }) 53 | -------------------------------------------------------------------------------- /test-d/parallel.ts: -------------------------------------------------------------------------------- 1 | import { parallelCanAssignHelpers, setParallelCanAssign } from '../' 2 | 3 | const { atMostOnePicklePerTag } = parallelCanAssignHelpers 4 | 5 | setParallelCanAssign(atMostOnePicklePerTag(['@tag1', '@tag2'])) 6 | -------------------------------------------------------------------------------- /test-d/steps.ts: -------------------------------------------------------------------------------- 1 | import { Given, When, Then } from '../' 2 | 3 | Given('some context', async function () {}) 4 | 5 | When('an action context', async function () {}) 6 | 7 | Then('verification', async function () {}) 8 | 9 | Given('a step that will be skipped', async function () { 10 | return 'skipped' 11 | }) 12 | 13 | Given('a step that we need to implement', async function () { 14 | return 'pending' 15 | }) 16 | -------------------------------------------------------------------------------- /test/fake_logger.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import { ILogger } from '../src/environment' 3 | 4 | export class FakeLogger implements ILogger { 5 | debug = sinon.fake() 6 | error = sinon.fake() 7 | warn = sinon.fake() 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/steps.ts: -------------------------------------------------------------------------------- 1 | // Tests depend on the lines the steps are defined on 2 | 3 | import { buildSupportCodeLibrary } from '../runtime_helpers' 4 | import { SupportCodeLibrary } from '../../src/support_code_library_builder/types' 5 | import World from '../../src/support_code_library_builder/world' 6 | 7 | export function getBaseSupportCodeLibrary(): SupportCodeLibrary { 8 | return buildSupportCodeLibrary(__dirname, ({ Given }) => { 9 | Given('a failing step', function () { 10 | throw 'error' // eslint-disable-line @typescript-eslint/no-throw-literal 11 | }) 12 | 13 | Given('an ambiguous step', function () {}) // eslint-disable-line @typescript-eslint/no-empty-function 14 | Given(/an? ambiguous step/, function () {}) // eslint-disable-line @typescript-eslint/no-empty-function 15 | 16 | Given('a pending step', function () { 17 | return 'pending' 18 | }) 19 | 20 | let willPass = false 21 | Given('a flaky step', function () { 22 | if (willPass) { 23 | return 24 | } 25 | willPass = true 26 | throw 'error' // eslint-disable-line @typescript-eslint/no-throw-literal 27 | }) 28 | 29 | Given('a passing step', function () {}) // eslint-disable-line @typescript-eslint/no-empty-function 30 | 31 | Given('a skipped step', function () { 32 | return 'skipped' 33 | }) 34 | 35 | Given('attachment step1', async function (this: World) { 36 | await this.attach('Some info') 37 | await this.attach('{"name": "some JSON"}', 'application/json') 38 | await this.attach(Buffer.from([137, 80, 78, 71]), { 39 | mediaType: 'image/png', 40 | fileName: 'screenshot.png', 41 | }) 42 | }) 43 | 44 | Given('attachment step2', async function (this: World) { 45 | await this.attach('Other info') 46 | throw 'error' // eslint-disable-line @typescript-eslint/no-throw-literal 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/usage_steps.ts: -------------------------------------------------------------------------------- 1 | // Tests depend on the lines the steps are defined on 2 | 3 | import { InstalledClock } from '@sinonjs/fake-timers' 4 | import { buildSupportCodeLibrary } from '../runtime_helpers' 5 | import { SupportCodeLibrary } from '../../src/support_code_library_builder/types' 6 | 7 | export function getUsageSupportCodeLibrary( 8 | clock: InstalledClock 9 | ): SupportCodeLibrary { 10 | return buildSupportCodeLibrary(__dirname, ({ Given }) => { 11 | Given('abc', function () { 12 | clock.tick(1) 13 | }) 14 | 15 | let count = 0 16 | Given(/def?/, function () { 17 | if (count === 0) { 18 | clock.tick(2) 19 | count += 1 20 | } else { 21 | clock.tick(1) 22 | } 23 | }) 24 | 25 | Given('ghi', function () {}) // eslint-disable-line @typescript-eslint/no-empty-function 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /test/runtime_helpers.ts: -------------------------------------------------------------------------------- 1 | import { IdGenerator } from '@cucumber/messages' 2 | import { SupportCodeLibraryBuilder } from '../src/support_code_library_builder' 3 | import { RuntimeOptions } from '../src/runtime' 4 | import { 5 | IDefineSupportCodeMethods, 6 | SupportCodeLibrary, 7 | } from '../src/support_code_library_builder/types' 8 | import { doesHaveValue } from '../src/value_checker' 9 | 10 | export function buildOptions( 11 | overrides: Partial 12 | ): RuntimeOptions { 13 | return { 14 | dryRun: false, 15 | failFast: false, 16 | filterStacktraces: false, 17 | retry: 0, 18 | retryTagFilter: '', 19 | strict: true, 20 | worldParameters: {}, 21 | ...overrides, 22 | } 23 | } 24 | 25 | type DefineSupportCodeFunction = (methods: IDefineSupportCodeMethods) => void 26 | 27 | export function buildSupportCodeLibrary( 28 | cwd: string | DefineSupportCodeFunction = __dirname, 29 | fn: DefineSupportCodeFunction = null 30 | ): SupportCodeLibrary { 31 | if (typeof cwd === 'function') { 32 | fn = cwd 33 | cwd = __dirname 34 | } 35 | const supportCodeLibraryBuilder = new SupportCodeLibraryBuilder() 36 | supportCodeLibraryBuilder.reset(cwd, IdGenerator.incrementing()) 37 | if (doesHaveValue(fn)) { 38 | fn(supportCodeLibraryBuilder.methods) 39 | } 40 | return supportCodeLibraryBuilder.finalize() 41 | } 42 | -------------------------------------------------------------------------------- /test/test_helper.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import sinonChai from 'sinon-chai' 3 | import dirtyChai from 'dirty-chai' 4 | 5 | chai.use(sinonChai) 6 | chai.use(dirtyChai) 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "lib": ["es2022"], 5 | "module": "node16", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noImplicitThis": true, 9 | "resolveJsonModule": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "target": "es2022", 13 | "typeRoots": ["./node_modules/@types", "./src/types"], 14 | "paths": { 15 | "@sinonjs/fake-timers": ["./node_modules/@types/sinonjs__fake-timers"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "lib" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["**/*_spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts", "src/api/index.ts"], 3 | "favicon": "./docs/images/logo.svg", 4 | "navigationLinks": { 5 | "GitHub": "https://github.com/cucumber/cucumber-js" 6 | }, 7 | "out": "./_site", 8 | "readme": "none", 9 | "tsconfig": "tsconfig.node.json" 10 | } 11 | --------------------------------------------------------------------------------