├── .editorconfig ├── .github └── workflows │ └── test-package.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── README.md ├── calculator.dart ├── features │ ├── calculator_background_example.feature │ ├── calculator_can_add.feature │ ├── calculator_can_add_powers_of_two.feature │ ├── calculator_count_strings.feature │ ├── calculator_scenario_outline_example.feature │ └── calculator_scenario_outline_german_example.feature ├── supporting_files │ ├── hooks │ │ └── hook_example.dart │ ├── parameters │ │ └── power_of_two.parameter.dart │ ├── steps │ │ ├── data_table_example_step.dart │ │ ├── given_the_characters.step.dart │ │ ├── given_the_numbers.step.dart │ │ ├── given_the_powers_of_two.step.dart │ │ ├── multiline_string_example_step.dart │ │ ├── then_expect_numeric_result.step.dart │ │ ├── when_numbers_are_added.step.dart │ │ └── when_the_characters_are_counted.step.dart │ └── worlds │ │ └── custom_world.world.dart └── test.dart ├── lib ├── gherkin.dart └── src │ ├── configuration.dart │ ├── expect │ ├── expect_mimic.dart │ └── expect_mimic_utils.dart │ ├── feature_file_runner.dart │ ├── gherkin │ ├── ast │ │ └── feature_file_visitor.dart │ ├── attachments │ │ ├── attachment.dart │ │ └── attachment_manager.dart │ ├── exceptions │ │ ├── dialect_not_supported.dart │ │ ├── gherkin_exception.dart │ │ ├── parameter_count_mismatch_error.dart │ │ ├── step_not_defined_error.dart │ │ ├── syntax_error.dart │ │ └── test_run_failed_exception.dart │ ├── expressions │ │ ├── gherkin_expression.dart │ │ └── tag_expression.dart │ ├── feature.dart │ ├── languages │ │ ├── README.md │ │ ├── dialect.dart │ │ ├── language_service.dart │ │ └── languages.dart │ ├── models │ │ ├── table.dart │ │ └── table_row.dart │ ├── parameters │ │ ├── custom_parameter.dart │ │ ├── float_parameter.dart │ │ ├── int_parameter.dart │ │ ├── plural_parameter.dart │ │ ├── step_defined_parameter.dart │ │ ├── string_parameter.dart │ │ └── word_parameter.dart │ ├── parser.dart │ ├── runnables │ │ ├── background.dart │ │ ├── comment_line.dart │ │ ├── debug_information.dart │ │ ├── dialect_block.dart │ │ ├── empty_line.dart │ │ ├── example.dart │ │ ├── feature.dart │ │ ├── feature_file.dart │ │ ├── language.dart │ │ ├── multi_line_string.dart │ │ ├── runnable.dart │ │ ├── runnable_block.dart │ │ ├── runnable_result.dart │ │ ├── scenario.dart │ │ ├── scenario_expanded_from_outline_example.dart │ │ ├── scenario_outline.dart │ │ ├── scenario_type_enum.dart │ │ ├── step.dart │ │ ├── table.dart │ │ ├── taggable_runnable_block.dart │ │ ├── tags.dart │ │ └── text_line.dart │ ├── steps │ │ ├── and.dart │ │ ├── but.dart │ │ ├── executable_step.dart │ │ ├── given.dart │ │ ├── step_configuration.dart │ │ ├── step_definition.dart │ │ ├── step_definition_implementations.dart │ │ ├── step_run_result.dart │ │ ├── then.dart │ │ ├── when.dart │ │ └── world.dart │ └── syntax │ │ ├── background_syntax.dart │ │ ├── comment_syntax.dart │ │ ├── empty_line_syntax.dart │ │ ├── example_syntax.dart │ │ ├── feature_file_syntax.dart │ │ ├── feature_syntax.dart │ │ ├── language_syntax.dart │ │ ├── multiline_string_syntax.dart │ │ ├── regex_matched_syntax.dart │ │ ├── scenario_outline_syntax.dart │ │ ├── scenario_syntax.dart │ │ ├── step_syntax.dart │ │ ├── syntax_matcher.dart │ │ ├── table_line_syntax.dart │ │ ├── tag_syntax.dart │ │ └── text_line_syntax.dart │ ├── hooks │ ├── aggregated_hook.dart │ └── hook.dart │ ├── io │ ├── feature_file_matcher.dart │ ├── feature_file_reader.dart │ └── io_feature_file_accessor.dart │ ├── processes │ └── process_handler.dart │ ├── reporters │ ├── aggregated_reporter.dart │ ├── json │ │ ├── json_embedding.dart │ │ ├── json_feature.dart │ │ ├── json_reporter.dart │ │ ├── json_row.dart │ │ ├── json_scenario.dart │ │ ├── json_statuses.dart │ │ ├── json_step.dart │ │ └── json_tag.dart │ ├── message_level.dart │ ├── messages │ │ ├── feature │ │ │ └── feature_message.dart │ │ ├── messages.dart │ │ ├── scenario │ │ │ └── scenario_message.dart │ │ ├── step │ │ │ └── step_message.dart │ │ └── test │ │ │ └── test_message.dart │ ├── progress_reporter.dart │ ├── reporter.dart │ ├── serializable_reporter.dart │ ├── stdout_reporter.dart │ └── test_run_summary_reporter.dart │ ├── test_runner.dart │ └── utils │ └── perf.dart ├── pre-publish-checks.cmd ├── pubspec.lock ├── pubspec.yaml └── test ├── feature_file_runner_test.dart ├── gherkin ├── attachments │ └── attachment_manager_test.dart ├── expressions │ ├── gherkin_expression_test.dart │ └── tag_expression_test.dart ├── langauges │ └── language_service_test.dart ├── parameters │ ├── float_parameter_test.dart │ ├── int_parameter.dart │ ├── string_parameter_test.dart │ └── word_parameter.dart ├── parser_test.dart ├── runnables │ ├── examples_test.dart │ ├── feature_file_test.dart │ ├── feature_test.dart │ ├── multi_line_string_test.dart │ ├── scenario_outline_test.dart │ ├── scenario_test.dart │ ├── step_test.dart │ └── table_test.dart ├── steps │ └── step_definition_test.dart └── syntax │ ├── background_syntax_test.dart │ ├── comment_syntax_test.dart │ ├── empty_line_syntax_test.dart │ ├── example_syntax_test.dart │ ├── feature_syntax_test.dart │ ├── language_syntax_test.dart │ ├── multiline_string_syntax_test.dart │ ├── scenario_outline_syntax_test.dart │ ├── scenario_syntax_test.dart │ ├── step_syntax_test.dart │ ├── table_line_string_syntax_test.dart │ ├── tag_syntax_test.dart │ └── text_line_syntax_test.dart ├── hooks └── aggregated_hook_test.dart ├── io ├── io_feature_file_accessor_gnu_test.dart ├── io_feature_file_accessor_test.dart ├── io_feature_file_accessor_windows_test.dart └── path_part_matcher.dart ├── mocks ├── en_dialect_mock.dart ├── fr_dialect_mock.dart ├── gherkin_expression_mock.dart ├── hook_mock.dart ├── language_service_mock.dart ├── reporter_mock.dart ├── step_definition_mock.dart ├── tag_expression_evaluator_mock.dart └── world_mock.dart ├── reporters ├── aggregated_reporter_test.dart ├── json_reporter_test.dart ├── json_reports │ ├── report_1.json │ ├── report_2.json │ ├── report_3.json │ ├── report_4.json │ ├── report_5.json │ ├── report_6.json │ ├── report_7.json │ └── report_8.json ├── progress_reporter_test.dart └── test_run_summary_reporter_test.dart ├── test_resources ├── a.feature └── subdir │ ├── b.feature │ └── c.feature └── utils └── step_method_declaration_test.dart /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | # From https://raw.githubusercontent.com/dart-lang/intl/master/.github/workflows/test-package.yml 2 | name: Dart CI 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | env: 11 | PUB_ENVIRONMENT: bot.github 12 | 13 | jobs: 14 | # Check code formatting and static analysis on a single OS (linux) 15 | # against Dart dev. 16 | analyze: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | sdk: [stable, dev] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: dart-lang/setup-dart@v0.3 25 | with: 26 | sdk: ${{ matrix.sdk }} 27 | - id: install 28 | name: Install dependencies 29 | run: dart pub get 30 | - name: Check formatting 31 | run: dart format --output=none --set-exit-if-changed . 32 | if: always() && steps.install.outcome == 'success' 33 | - name: Analyze code 34 | run: dart analyze --fatal-infos 35 | if: always() && steps.install.outcome == 'success' 36 | 37 | # Run tests on a matrix consisting of two dimensions: 38 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 39 | # 2. release channel: dev 40 | test: 41 | needs: analyze 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | # Add macos-latest and/or windows-latest if relevant for this package. 47 | os: [ubuntu-latest, windows-latest] 48 | sdk: [stable, dev] 49 | steps: 50 | - uses: actions/checkout@v2 51 | - uses: dart-lang/setup-dart@v0.3 52 | with: 53 | sdk: ${{ matrix.sdk }} 54 | - id: install 55 | name: Install dependencies 56 | run: dart pub get 57 | - name: Run tests 58 | run: dart test 59 | if: always() && steps.install.outcome == 'success' 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .idea/ 7 | .vscode/ 8 | 9 | build/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jon Samwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:lint/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | linter: 8 | rules: 9 | await_only_futures: true 10 | avoid_function_literals_in_foreach_calls: false 11 | avoid_redundant_argument_values: false 12 | use_setters_to_change_properties: false 13 | sort_pub_dependencies: false 14 | prefer_constructors_over_static_methods: false 15 | avoid_print: false 16 | avoid_classes_with_only_static_members: false 17 | always_use_package_imports: false 18 | prefer_relative_imports: true 19 | prefer_const_constructors: true 20 | 21 | # analyzer: 22 | # exclude: 23 | # - path/to/excluded/files/** 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Running the example 2 | 3 | To run this example: 4 | 5 | 1. Ensure dart is accessible in the command line (on your path variable) 6 | 2. In a command prompt (from the root of this library): 7 | ```bash 8 | dart pub get 9 | cd example 10 | 11 | dart test.dart 12 | ``` 13 | This will run the features files found in the folder `features`. 14 | 15 | ## Debugging the example 16 | 17 | To debug this example and step through the library code. 18 | 19 | 1. Set a break point in `test.dart` 20 | 2. If you are in VSCode you will simply be able to select `Debug example` from the dropdown in the `debugging tab` as the `launch.json` has been configured. 21 | - otherwise you will need to run a debugging session against `test.dart`. 22 | -------------------------------------------------------------------------------- /example/calculator.dart: -------------------------------------------------------------------------------- 1 | class Calculator { 2 | final List _cachedNumbers = []; 3 | final List _cachedCharacters = []; 4 | final List _results = []; 5 | 6 | void storeNumericInput(num input) => _cachedNumbers.add(input); 7 | void storeCharacterInput(String input) => _cachedCharacters.add(input); 8 | 9 | num _retrieveNumericInput() => _cachedNumbers.removeAt(0); 10 | 11 | num add() { 12 | final result = _retrieveNumericInput() + _retrieveNumericInput(); 13 | _results.add(result); 14 | 15 | return result; 16 | } 17 | 18 | num countStringCharacters() { 19 | num result = 0; 20 | for (var i = 0; i < _cachedCharacters[0].length; i += 1) { 21 | result += _cachedCharacters[0].codeUnitAt(i); 22 | } 23 | 24 | _cachedCharacters.clear(); 25 | _results.add(result); 26 | 27 | return result; 28 | } 29 | 30 | num evaluateExpression(String expression) => 1; 31 | 32 | num getNumericResult() => _results.removeLast(); 33 | 34 | void dispose() { 35 | _cachedNumbers.clear(); 36 | _results.clear(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/features/calculator_background_example.feature: -------------------------------------------------------------------------------- 1 | # comment ... 2 | 3 | Feature: Calculator with Background 4 | Tests the addition feature of the calculator 5 | 6 | Background: Set the starting number 7 | Given the numbers 100 and 50 8 | And they are added 9 | 10 | @debug 11 | Scenario: Add two numbers with background 12 | A scenario description! 13 | Then the expected result is 150 14 | -------------------------------------------------------------------------------- /example/features/calculator_can_add.feature: -------------------------------------------------------------------------------- 1 | Feature: Calculator 2 | Tests the addition feature of the calculator 3 | 4 | Scenario: Add two numbers 5 | Given the numbers 1.5 and 2.1 6 | When they are added 7 | Then the expected result is 3.6 8 | -------------------------------------------------------------------------------- /example/features/calculator_can_add_powers_of_two.feature: -------------------------------------------------------------------------------- 1 | Feature: Calculator 2 | Tests the addition of powers of two feature of the calculator 3 | 4 | Scenario: Add two number which are powers of two 5 | Given the powers 2^10 and 5^9 6 | When they are added 7 | Then the expected result is 1954149 8 | -------------------------------------------------------------------------------- /example/features/calculator_count_strings.feature: -------------------------------------------------------------------------------- 1 | @a 2 | Feature: Calculator can work with strings 3 | Tests that the calculator can count the total value of the character code units in a string 4 | 5 | Scenario Outline: Counts string's code units 6 | Given the characters "" 7 | When they are counted 8 | Then the expected result is 9 | 10 | Examples: 11 | | characters | result | 12 | | abc | 294 | 13 | | a b c | 358 | 14 | | a \n b \c | 684 | 15 | -------------------------------------------------------------------------------- /example/features/calculator_scenario_outline_example.feature: -------------------------------------------------------------------------------- 1 | Feature: Calculator 2 | Tests the addition of two numbers 3 | 4 | Scenario Outline: Add two numbers 5 | Given the numbers and 6 | When they are added 7 | Then the expected result is 8 | 9 | Examples: 10 | | number_one | number_two | result | 11 | | 12 | 5 | 17 | 12 | | 20 | 5 | 25 | 13 | | 20937 | 1 | 20938 | 14 | | 20.937 | -1.937 | 19 | 15 | -------------------------------------------------------------------------------- /example/features/calculator_scenario_outline_german_example.feature: -------------------------------------------------------------------------------- 1 | # language: de 2 | Funktionalität: Calculator 3 | Tests the addition of two numbers 4 | 5 | Szenariogrundriss: Add two numbers 6 | Gegeben sei the numbers and 7 | Wenn they are added 8 | Dann the expected result is 9 | 10 | Beispiele: 11 | | number_one | number_two | result | 12 | | 12 | 5 | 17 | 13 | | 20 | 5 | 25 | 14 | | 20937 | 1 | 20938 | 15 | | 20.937 | -1.937 | 19 | 16 | -------------------------------------------------------------------------------- /example/supporting_files/hooks/hook_example.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | 5 | class HookExample extends Hook { 6 | /// The priority to assign to this hook. 7 | /// Higher priority gets run first so a priority of 10 is run before a priority of 2 8 | @override 9 | int get priority => 1; 10 | 11 | /// Run before any scenario in a test run have executed 12 | @override 13 | Future onBeforeRun(TestConfiguration config) async { 14 | print('before run hook'); 15 | } 16 | 17 | /// Run after all scenarios in a test run have completed 18 | @override 19 | Future onAfterRun(TestConfiguration config) async { 20 | print('after run hook'); 21 | } 22 | 23 | /// Run before a scenario and it steps are executed 24 | @override 25 | Future onBeforeScenario( 26 | TestConfiguration config, 27 | String scenario, 28 | Iterable tags, 29 | ) async { 30 | print("running hook before scenario '$scenario'"); 31 | } 32 | 33 | /// Run after a scenario has executed 34 | @override 35 | Future onAfterScenario( 36 | TestConfiguration config, 37 | String scenario, 38 | Iterable tags, { 39 | bool passed = true, 40 | }) async { 41 | print("running hook after scenario '$scenario'"); 42 | } 43 | 44 | /// Run before a step is executed 45 | @override 46 | Future onBeforeStep(World world, String step) async { 47 | print("running hook before step '$step'"); 48 | } 49 | 50 | /// Run after a step has executed 51 | @override 52 | Future onAfterStep( 53 | World world, 54 | String step, 55 | StepResult stepResult, 56 | ) async { 57 | print("running hook after step '$step'"); 58 | 59 | // example of how to add a simple attachment (text, json, image) to a step that a reporter can use 60 | world.attach('attachment data', 'text/plain', step); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/supporting_files/parameters/power_of_two.parameter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | 5 | class PowerOfTwoParameter extends CustomParameter { 6 | PowerOfTwoParameter() 7 | : super( 8 | 'POW', 9 | RegExp(r'([0-9]+\^[0-9]+)'), 10 | (input) { 11 | final parts = input.split('^'); 12 | return pow(int.parse(parts[0]), int.parse(parts[1])) as int; 13 | }, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /example/supporting_files/steps/data_table_example_step.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | 5 | /// This step expects a multi-line string proceeding it 6 | /// 7 | /// For example: 8 | /// 9 | /// `Given I add the users` 10 | /// | Firstname | Surname | Age | Gender | 11 | /// | Woody | Johnson | 28 | Male | 12 | /// | Edith | Summers | 23 | Female | 13 | /// | Megan | Hill | 83 | Female | 14 | StepDefinitionGeneric givenIAddTheUsers() { 15 | return given1( 16 | 'I add the users', 17 | (dataTable, _) async { 18 | for (final row in dataTable.rows) { 19 | // do something with row 20 | for (final columnValue in row.columns) { 21 | print(columnValue); 22 | } 23 | } 24 | 25 | // or get the table as a map (column values keyed by the header) 26 | final columns = dataTable.asMap(); 27 | final personOne = columns.elementAt(0); 28 | final personOneName = personOne['Firstname']; 29 | print('Name of first person: `$personOneName`'); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /example/supporting_files/steps/given_the_characters.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | import '../worlds/custom_world.world.dart'; 4 | 5 | StepDefinitionGeneric givenTheCharacters() { 6 | return given1( 7 | 'the characters {string}', 8 | (input1, context) async { 9 | context.world.calculator.storeCharacterInput(input1); 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /example/supporting_files/steps/given_the_numbers.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import '../worlds/custom_world.world.dart'; 3 | 4 | StepDefinitionGeneric givenTheNumbers() { 5 | return given2( 6 | 'the numbers {num} and {num}', 7 | (input1, input2, context) async { 8 | context.world.calculator.storeNumericInput(input1); 9 | context.world.calculator.storeNumericInput(input2); 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /example/supporting_files/steps/given_the_powers_of_two.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import '../worlds/custom_world.world.dart'; 3 | 4 | StepDefinitionGeneric givenThePowersOfTwo() { 5 | return given2( 6 | 'the powers {POW} and {POW}', 7 | (input1, input2, context) async { 8 | context.world.calculator.storeNumericInput(input1); 9 | context.world.calculator.storeNumericInput(input2); 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /example/supporting_files/steps/multiline_string_example_step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | /// This step expects a multi-line string proceeding it 4 | /// 5 | /// For example: 6 | /// 7 | /// `Given I provide the following "review" comment` 8 | /// """ 9 | /// Some comment 10 | /// """ 11 | StepDefinitionGeneric givenTheMultiLineComment() { 12 | return given2( 13 | 'I provide the following {string} comment', 14 | (commentType, comment, _) async { 15 | // implement step 16 | }, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /example/supporting_files/steps/then_expect_numeric_result.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import '../worlds/custom_world.world.dart'; 3 | 4 | StepDefinitionGeneric thenExpectNumericResult() { 5 | return given1( 6 | 'the expected result is {num}', 7 | (input1, context) async { 8 | final result = context.world.calculator.getNumericResult(); 9 | context.expectMatch(result, input1); 10 | }, 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /example/supporting_files/steps/when_numbers_are_added.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import '../worlds/custom_world.world.dart'; 3 | 4 | StepDefinitionGeneric whenTheStoredNumbersAreAdded() { 5 | return given( 6 | 'they are added', 7 | (context) async { 8 | context.world.calculator.add(); 9 | }, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/supporting_files/steps/when_the_characters_are_counted.step.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import '../worlds/custom_world.world.dart'; 3 | 4 | StepDefinitionGeneric whenTheCharactersAreCounted() { 5 | return given( 6 | 'they are counted', 7 | (context) async { 8 | context.world.calculator.countStringCharacters(); 9 | }, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/supporting_files/worlds/custom_world.world.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | import '../../calculator.dart'; 4 | 5 | class CalculatorWorld extends World { 6 | final Calculator calculator = Calculator(); 7 | 8 | @override 9 | void dispose() { 10 | calculator.dispose(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:gherkin/gherkin.dart'; 3 | import 'supporting_files/hooks/hook_example.dart'; 4 | import 'supporting_files/parameters/power_of_two.parameter.dart'; 5 | import 'supporting_files/steps/given_the_characters.step.dart'; 6 | import 'supporting_files/steps/given_the_numbers.step.dart'; 7 | import 'supporting_files/steps/given_the_powers_of_two.step.dart'; 8 | import 'supporting_files/steps/then_expect_numeric_result.step.dart'; 9 | import 'supporting_files/steps/when_numbers_are_added.step.dart'; 10 | import 'supporting_files/steps/when_the_characters_are_counted.step.dart'; 11 | import 'supporting_files/worlds/custom_world.world.dart'; 12 | 13 | Future main() { 14 | final steps = [ 15 | givenTheNumbers(), 16 | givenThePowersOfTwo(), 17 | givenTheCharacters(), 18 | whenTheStoredNumbersAreAdded(), 19 | whenTheCharactersAreCounted(), 20 | thenExpectNumericResult() 21 | ]; 22 | 23 | final config = TestConfiguration( 24 | features: [RegExp(r'features\\.+\.feature')], 25 | reporters: [ 26 | StdoutReporter(MessageLevel.error), 27 | ProgressReporter(), 28 | TestRunSummaryReporter(), 29 | JsonReporter(), 30 | ], 31 | hooks: [HookExample()], 32 | customStepParameterDefinitions: [PowerOfTwoParameter()], 33 | createWorld: (config) => Future.value(CalculatorWorld()), 34 | stepDefinitions: steps, 35 | stopAfterTestFailed: true, 36 | ); 37 | 38 | // or 39 | 40 | // final config = TestConfiguration.standard( 41 | // steps, 42 | // tagExpression: 'not @skip', 43 | // hooks: [HookExample()], 44 | // customStepParameterDefinitions: [PowerOfTwoParameter()], 45 | // createWorld: (config) => Future.value(CalculatorWorld()), 46 | // stopAfterTestFailed: true, 47 | // ); 48 | 49 | return GherkinRunner().execute(config); 50 | } 51 | -------------------------------------------------------------------------------- /lib/gherkin.dart: -------------------------------------------------------------------------------- 1 | library gherkin; 2 | 3 | export 'src/configuration.dart'; 4 | // Parser 5 | export 'src/gherkin/ast/feature_file_visitor.dart'; 6 | // Attachments 7 | export 'src/gherkin/attachments/attachment.dart'; 8 | export 'src/gherkin/attachments/attachment_manager.dart'; 9 | // Exceptions 10 | export 'src/gherkin/exceptions/dialect_not_supported.dart'; 11 | export 'src/gherkin/exceptions/gherkin_exception.dart'; 12 | export 'src/gherkin/exceptions/parameter_count_mismatch_error.dart'; 13 | export 'src/gherkin/exceptions/step_not_defined_error.dart'; 14 | export 'src/gherkin/exceptions/syntax_error.dart'; 15 | export 'src/gherkin/exceptions/test_run_failed_exception.dart'; 16 | export 'src/gherkin/expressions/gherkin_expression.dart'; 17 | export 'src/gherkin/expressions/tag_expression.dart'; 18 | export 'src/gherkin/languages/language_service.dart'; 19 | // Models 20 | export 'src/gherkin/models/table.dart'; 21 | export 'src/gherkin/models/table_row.dart'; 22 | // Custom Parameters 23 | export 'src/gherkin/parameters/custom_parameter.dart'; 24 | export 'src/gherkin/parameters/float_parameter.dart'; 25 | export 'src/gherkin/parameters/int_parameter.dart'; 26 | export 'src/gherkin/parameters/plural_parameter.dart'; 27 | export 'src/gherkin/parameters/string_parameter.dart'; 28 | export 'src/gherkin/parameters/word_parameter.dart'; 29 | export 'src/gherkin/runnables/debug_information.dart'; 30 | export 'src/gherkin/steps/and.dart'; 31 | export 'src/gherkin/steps/but.dart'; 32 | export 'src/gherkin/steps/executable_step.dart'; 33 | export 'src/gherkin/steps/given.dart'; 34 | export 'src/gherkin/steps/step_configuration.dart'; 35 | export 'src/gherkin/steps/step_definition.dart'; 36 | export 'src/gherkin/steps/step_definition_implementations.dart'; 37 | export 'src/gherkin/steps/step_run_result.dart'; 38 | export 'src/gherkin/steps/then.dart'; 39 | export 'src/gherkin/steps/when.dart'; 40 | export 'src/gherkin/steps/world.dart'; 41 | export 'src/hooks/aggregated_hook.dart'; 42 | // Hooks 43 | export 'src/hooks/hook.dart'; 44 | // IO 45 | export 'src/io/feature_file_matcher.dart'; 46 | export 'src/io/feature_file_reader.dart'; 47 | export 'src/io/io_feature_file_accessor.dart'; 48 | // Process Handler 49 | export 'src/processes/process_handler.dart'; 50 | export 'src/reporters/aggregated_reporter.dart'; 51 | export 'src/reporters/json/json_reporter.dart'; 52 | export 'src/reporters/message_level.dart'; 53 | export 'src/reporters/messages/messages.dart'; 54 | export 'src/reporters/progress_reporter.dart'; 55 | // Reporters 56 | export 'src/reporters/reporter.dart'; 57 | export 'src/reporters/serializable_reporter.dart'; 58 | export 'src/reporters/stdout_reporter.dart'; 59 | export 'src/reporters/test_run_summary_reporter.dart'; 60 | export 'src/test_runner.dart'; 61 | -------------------------------------------------------------------------------- /lib/src/expect/expect_mimic.dart: -------------------------------------------------------------------------------- 1 | import 'package:matcher/matcher.dart'; 2 | 3 | import 'expect_mimic_utils.dart'; 4 | 5 | /// This is an atrocity but I can't see a way around it at the moment 6 | /// To use the expect() it must be called within a test() or this happens: 7 | /// 8 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart#L95 9 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect_async.dart#L237 10 | /// 11 | /// Unfortunately, I cannot get the test framework to play nicely with dynamically 12 | /// creating and adding tests as the tests framework seems to build the tests before 13 | /// I need it to and this happens: 14 | /// 15 | /// "Can't call test() once tests have begun running." 16 | /// 17 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/backend/declarer.dart#L274 18 | /// 19 | /// We still want to be able to use the Matchers are we can't expect people not to use them 20 | /// So we are stuck here using smoke and mirrors and mimicking the expect / expectAsync methods in our step class 21 | /// 22 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart 23 | class ExpectMimic { 24 | /// Assert that [actual] matches [matcher]. 25 | /// 26 | /// This is the main assertion function. [reason] is optional and is typically 27 | /// not supplied, as a reason is generated from [matcher]; if [reason] 28 | /// is included it is appended to the reason generated by the matcher. 29 | /// 30 | /// [matcher] can be a value in which case it will be wrapped in an 31 | /// [equals] matcher. 32 | /// 33 | /// If the assertion fails a [TestFailure] is thrown. 34 | /// 35 | /// If [skip] is a String or `true`, the assertion is skipped. The arguments are 36 | /// still evaluated, but [actual] is not verified to match [matcher]. If 37 | /// [actual] is a [Future], the test won't complete until the future emits a 38 | /// value. 39 | /// 40 | /// Certain matchers, like [completion] and [throwsA], either match or fail 41 | /// asynchronously. When you use [expect] with these matchers, it ensures that 42 | /// the test doesn't complete until the matcher has either matched or failed. If 43 | /// you want to wait for the matcher to complete before continuing the test, you 44 | /// can call [expectLater] instead and `await` the result. 45 | void expect( 46 | dynamic actualValue, 47 | dynamic matcher, { 48 | String? reason, 49 | }) { 50 | final matchState = {}; 51 | final wrappedMatcher = wrapMatcher(matcher); 52 | final result = wrappedMatcher.matches(actualValue, matchState); 53 | String formatter( 54 | actual, 55 | matcher, 56 | String? reason, 57 | Map matchState, 58 | // ignore: avoid_positional_boolean_parameters 59 | bool verbose, 60 | ) { 61 | final mismatchDescription = StringDescription(); 62 | wrappedMatcher.describeMismatch( 63 | actual, 64 | mismatchDescription, 65 | matchState, 66 | verbose, 67 | ); 68 | 69 | return formatFailure( 70 | wrappedMatcher, 71 | actual, 72 | mismatchDescription.toString(), 73 | reason: reason, 74 | ); 75 | } 76 | 77 | if (!result) { 78 | throw GherkinTestFailure( 79 | formatter(actualValue, matcher, reason, matchState, false), 80 | ); 81 | } 82 | } 83 | } 84 | 85 | class GherkinTestFailure { 86 | final String message; 87 | 88 | GherkinTestFailure(this.message); 89 | 90 | @override 91 | String toString() => message; 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/expect/expect_mimic_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:matcher/matcher.dart'; 2 | 3 | /// Returns a pretty-printed representation of [value]. 4 | /// 5 | /// The matcher package doesn't expose its pretty-print function directly, but 6 | /// we can use it through StringDescription. 7 | String prettyPrint(Object? value) => 8 | StringDescription().addDescriptionOf(value).toString(); 9 | 10 | String formatFailure( 11 | Matcher expected, 12 | Object? actual, 13 | String which, { 14 | String? reason, 15 | }) { 16 | final buffer = StringBuffer(); 17 | buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); 18 | buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); 19 | if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: ')); 20 | if (reason != null) buffer.writeln(reason); 21 | 22 | return buffer.toString(); 23 | } 24 | 25 | /// Indent each line in [string] by [size] spaces. 26 | /// 27 | /// If [first] is passed, it's used in place of the first line's indentation and 28 | /// [size] defaults to `first.length`. Otherwise, [size] defaults to 2. 29 | String indent( 30 | String string, { 31 | int? size, 32 | String? first, 33 | }) { 34 | size ??= first == null ? 2 : first.length; 35 | return prefixLines( 36 | string, 37 | ' ' * size, 38 | first: first, 39 | ); 40 | } 41 | 42 | /// Prepends each line in [text] with [prefix]. 43 | /// 44 | /// If [first] or [last] is passed, the first and last lines, respectively, are 45 | /// prefixed with those instead. If [single] is passed, it's used if there's 46 | /// only a single line; otherwise, [first], [last], or [prefix] is used, in that 47 | /// order of precedence. 48 | String prefixLines( 49 | String text, 50 | String prefix, { 51 | String? first, 52 | String? last, 53 | String? single, 54 | }) { 55 | first ??= prefix; 56 | last ??= prefix; 57 | single ??= first; 58 | 59 | final lines = text.split('\n'); 60 | if (lines.length == 1) return '$single$text'; 61 | 62 | final buffer = StringBuffer('$first${lines.first}\n'); 63 | 64 | // Write out all but the first and last lines with [prefix]. 65 | for (final line in lines.skip(1).take(lines.length - 2)) { 66 | buffer.writeln('$prefix$line'); 67 | } 68 | 69 | buffer.write('$last${lines.last}'); 70 | return buffer.toString(); 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/gherkin/ast/feature_file_visitor.dart: -------------------------------------------------------------------------------- 1 | import '../../../gherkin.dart'; 2 | import '../parser.dart'; 3 | import '../runnables/scenario_outline.dart'; 4 | import '../runnables/tags.dart'; 5 | 6 | class FeatureFileVisitor { 7 | Future visit( 8 | String featureFileContents, 9 | String path, 10 | LanguageService languageService, 11 | MessageReporter reporter, 12 | ) async { 13 | final featureFile = await GherkinParser().parseFeatureFile( 14 | featureFileContents, 15 | path, 16 | reporter, 17 | languageService, 18 | ); 19 | 20 | for (final feature in featureFile.features) { 21 | await visitFeature( 22 | feature.name, 23 | feature.description, 24 | _tagsToList(feature.tags), 25 | feature.scenarios.length, 26 | ); 27 | 28 | for (var i = 0; i < feature.scenarios.length; i += 1) { 29 | final scenario = feature.scenarios.elementAt(i); 30 | final isFirst = i == 0; 31 | final isLast = i == (feature.scenarios.length - 1); 32 | final allScenarios = scenario is ScenarioOutlineRunnable 33 | ? scenario.expandOutlinesIntoScenarios() 34 | : [scenario]; 35 | var acknowledgedScenarioPosition = false; 36 | 37 | for (final childScenario in allScenarios) { 38 | await visitScenario( 39 | feature.name, 40 | feature.description, 41 | _tagsToList(feature.tags), 42 | childScenario.name, 43 | childScenario.description, 44 | _tagsToList(childScenario.tags), 45 | path, 46 | isFirst: !acknowledgedScenarioPosition && isFirst, 47 | isLast: !acknowledgedScenarioPosition && isLast, 48 | ); 49 | 50 | acknowledgedScenarioPosition = true; 51 | 52 | if (feature.background != null) { 53 | final bg = feature.background; 54 | 55 | for (final step in bg!.steps) { 56 | await visitScenarioStep( 57 | step.name, 58 | step.multilineStrings, 59 | step.table, 60 | ); 61 | } 62 | } 63 | 64 | for (final step in childScenario.steps) { 65 | await visitScenarioStep( 66 | step.name, 67 | step.multilineStrings, 68 | step.table, 69 | ); 70 | } 71 | } 72 | } 73 | } 74 | 75 | return Future.value(null); 76 | } 77 | 78 | Future visitFeature( 79 | String name, 80 | String? description, 81 | Iterable tags, 82 | int childScenarioCount, 83 | ) async {} 84 | 85 | Future visitScenario( 86 | String featureName, 87 | String? featureDescription, 88 | Iterable featureTags, 89 | String name, 90 | String? description, 91 | Iterable tags, 92 | String path, { 93 | required bool isFirst, 94 | required bool isLast, 95 | }) async {} 96 | 97 | Future visitScenarioStep( 98 | String name, 99 | Iterable multiLineStrings, 100 | GherkinTable? table, 101 | ) async {} 102 | 103 | Iterable _tagsToList(Iterable tags) sync* { 104 | for (final tag in tags.expand((element) => element.tags)) { 105 | yield tag; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/gherkin/attachments/attachment.dart: -------------------------------------------------------------------------------- 1 | class Attachment { 2 | final String data; 3 | final String mimeType; 4 | final String? context; 5 | 6 | Attachment( 7 | this.data, 8 | this.mimeType, [ 9 | this.context, 10 | ]); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/gherkin/attachments/attachment_manager.dart: -------------------------------------------------------------------------------- 1 | import 'attachment.dart'; 2 | 3 | class AttachmentManager { 4 | final List _attachments = []; 5 | 6 | void attach( 7 | String data, 8 | String mimeType, [ 9 | String? context, 10 | ]) { 11 | _attachments.add(Attachment(data, mimeType, context)); 12 | } 13 | 14 | Iterable getAttachmentsForContext([ 15 | String? context, 16 | ]) { 17 | return _attachments.where((attachment) => attachment.context == context); 18 | } 19 | 20 | void dispose() { 21 | _attachments.clear(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/dialect_not_supported.dart: -------------------------------------------------------------------------------- 1 | import 'gherkin_exception.dart'; 2 | 3 | class GherkinDialogNotSupportedException implements GherkinException { 4 | final String? dialect; 5 | 6 | GherkinDialogNotSupportedException(this.dialect); 7 | 8 | @override 9 | String toString() { 10 | if (dialect == null) { 11 | return 'GherkinDialogNotSupportedException'; 12 | } 13 | 14 | return "GherkinDialogNotSupportedException: Dialect is not supported '$dialect'"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/gherkin_exception.dart: -------------------------------------------------------------------------------- 1 | abstract class GherkinException implements Exception {} 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/parameter_count_mismatch_error.dart: -------------------------------------------------------------------------------- 1 | import 'gherkin_exception.dart'; 2 | 3 | class GherkinStepParameterMismatchException implements GherkinException { 4 | final int expectParameterCount; 5 | final int actualParameterCount; 6 | final Type step; 7 | final String message; 8 | 9 | GherkinStepParameterMismatchException( 10 | this.step, 11 | this.expectParameterCount, 12 | this.actualParameterCount, 13 | ) : message = '$step parameter count mismatch. Expect $expectParameterCount parameters but got $actualParameterCount. ' 14 | 'Ensure you are extending the correct step class which would be ' 15 | "Given${actualParameterCount > 0 ? '$actualParameterCount<${List.generate(actualParameterCount, (i) => "TInputType$i").join(", ")}>' : ''}"; 16 | 17 | @override 18 | String toString() => message; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/step_not_defined_error.dart: -------------------------------------------------------------------------------- 1 | import 'gherkin_exception.dart'; 2 | 3 | class GherkinStepNotDefinedException implements GherkinException { 4 | final String? message; 5 | 6 | GherkinStepNotDefinedException(this.message); 7 | 8 | @override 9 | String toString() { 10 | if (message == null) { 11 | return 'GherkinStepNotDefinedException'; 12 | } 13 | 14 | return 'GherkinStepNotDefinedException: $message'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/syntax_error.dart: -------------------------------------------------------------------------------- 1 | import 'gherkin_exception.dart'; 2 | 3 | class GherkinSyntaxException implements GherkinException { 4 | final String message; 5 | 6 | GherkinSyntaxException(this.message); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/test_run_failed_exception.dart: -------------------------------------------------------------------------------- 1 | class GherkinTestRunFailedException implements Exception {} 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/feature.dart: -------------------------------------------------------------------------------- 1 | class Feature { 2 | late String name; 3 | late String language; 4 | late Iterable tags; 5 | 6 | Feature(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/gherkin/languages/README.md: -------------------------------------------------------------------------------- 1 | Language file take from https://raw.githubusercontent.com/cucumber/cucumber/80b282783dca20bbf3d32a8dd6c85abccf5bd70c/gherkin/gherkin-languages.json 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/languages/dialect.dart: -------------------------------------------------------------------------------- 1 | class GherkinDialect { 2 | final String name; 3 | final String nativeName; 4 | final String? languageCode; 5 | final Iterable feature; 6 | final Iterable background; 7 | final Iterable rule; 8 | final Iterable scenario; 9 | final Iterable scenarioOutline; 10 | final Iterable examples; 11 | final Iterable given; 12 | final Iterable when; 13 | final Iterable then; 14 | final Iterable and; 15 | final Iterable but; 16 | 17 | GherkinDialect({ 18 | required this.name, 19 | required this.nativeName, 20 | required this.feature, 21 | required this.background, 22 | required this.rule, 23 | required this.scenario, 24 | required this.scenarioOutline, 25 | required this.examples, 26 | required this.given, 27 | required this.when, 28 | required this.then, 29 | required this.and, 30 | required this.but, 31 | this.languageCode, 32 | }); 33 | 34 | Set get stepKeywords => { 35 | ...given, 36 | ...when, 37 | ...then, 38 | ...and, 39 | ...but, 40 | }; 41 | 42 | factory GherkinDialect.fromJson(Map map) { 43 | final given = map['given'] as List; 44 | final when = map['when'] as List; 45 | final then = map['then'] as List; 46 | final and = map['and'] as List; 47 | final but = map['but'] as List; 48 | return GherkinDialect( 49 | name: map['name'] as String, 50 | nativeName: map['native'] as String, 51 | feature: map['feature'] as List, 52 | background: map['background'] as List, 53 | rule: map['rule'] as List, 54 | scenario: map['scenario'] as List, 55 | scenarioOutline: map['scenarioOutline'] as List, 56 | examples: map['examples'] as List, 57 | given: given, 58 | when: when, 59 | then: then, 60 | and: and, 61 | but: but, 62 | ); 63 | } 64 | 65 | GherkinDialect copyWith({ 66 | String? name, 67 | String? nativeName, 68 | String? languageCode, 69 | Iterable? feature, 70 | Iterable? background, 71 | Iterable? rule, 72 | Iterable? scenario, 73 | Iterable? scenarioOutline, 74 | Iterable? examples, 75 | Iterable? given, 76 | Iterable? when, 77 | Iterable? then, 78 | Iterable? and, 79 | Iterable? but, 80 | }) { 81 | return GherkinDialect( 82 | name: name ?? this.name, 83 | nativeName: nativeName ?? this.nativeName, 84 | languageCode: languageCode ?? this.languageCode, 85 | feature: feature ?? this.feature, 86 | background: background ?? this.background, 87 | rule: rule ?? this.rule, 88 | scenario: scenario ?? this.scenario, 89 | scenarioOutline: scenarioOutline ?? this.scenarioOutline, 90 | examples: examples ?? this.examples, 91 | given: given ?? this.given, 92 | when: when ?? this.when, 93 | then: then ?? this.then, 94 | and: and ?? this.and, 95 | but: but ?? this.but, 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/gherkin/languages/language_service.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/dialect_not_supported.dart'; 2 | import 'dialect.dart'; 3 | import 'languages.dart'; 4 | 5 | class LanguageService { 6 | String _defaultLanguage = 'en'; 7 | final Map _dialects = {}; 8 | 9 | String get defaultLanguage => _defaultLanguage; 10 | 11 | GherkinDialect getDialect([ 12 | String? languageCode, 13 | ]) { 14 | final code = languageCode ?? _defaultLanguage; 15 | 16 | if (_dialects[code] == null) { 17 | throw GherkinDialogNotSupportedException(code); 18 | } 19 | 20 | return _dialects[code]!; 21 | } 22 | 23 | void initialise([String defaultLanguage = 'en']) { 24 | _defaultLanguage = defaultLanguage; 25 | // final uri = Uri.file('dialects/languages.json'); 26 | // final langFile = File.fromUri(uri); 27 | // Map languagesJson = 28 | // json.decode(langFile.readAsStringSync()); 29 | kLanguagesJson.forEach((key, values) { 30 | final dialect = 31 | GherkinDialect.fromJson(values as Map).copyWith( 32 | languageCode: key, 33 | ); 34 | setDialect(key, dialect); 35 | }); 36 | } 37 | 38 | void setDialect(String languageCode, GherkinDialect dialect) { 39 | _dialects[languageCode] = dialect; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/gherkin/models/table.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'table_row.dart'; 4 | 5 | class GherkinTable { 6 | final Iterable rows; 7 | final TableRow? header; 8 | 9 | GherkinTable( 10 | this.rows, 11 | this.header, 12 | ); 13 | 14 | void setStepParameter( 15 | String parameterName, 16 | String value, 17 | ) { 18 | for (final row in rows) { 19 | row.setStepParameter(parameterName, value); 20 | } 21 | } 22 | 23 | /// Returns the table as a iterable of maps. With a single map representing a row in the table 24 | /// keyed by the column name if a header row is present else the column index (as a string) 25 | Iterable> asMap() { 26 | return >[ 27 | ...rows.map( 28 | (row) { 29 | final map = {}; 30 | if (header != null) { 31 | for (var i = 0; i < header!.columns.length; i += 1) { 32 | map[header!.columns.toList().elementAt(i)!] = 33 | row.columns.toList().length > i 34 | ? row.columns.elementAt(i) 35 | : null; 36 | } 37 | } else { 38 | for (var i = 0; i < row.columns.length; i += 1) { 39 | map[i.toString()] = 40 | row.columns.length > i ? row.columns.elementAt(i) : null; 41 | } 42 | } 43 | 44 | return map; 45 | }, 46 | ) 47 | ]; 48 | } 49 | 50 | String toJson() => jsonEncode(asMap()); 51 | 52 | factory GherkinTable.fromJson(String json) { 53 | final data = (jsonDecode(json) as List) 54 | .map((x) => x as Map); 55 | final headerRow = data.isNotEmpty 56 | ? TableRow(data.first.keys, 1, isHeaderRow: true) 57 | : null; 58 | final rows = data 59 | .map((x) => TableRow(x.values.cast(), 1, isHeaderRow: false)); 60 | final table = GherkinTable(rows, headerRow); 61 | 62 | return table; 63 | } 64 | 65 | GherkinTable clone() => GherkinTable( 66 | rows.map((r) => r.clone()).toList(), 67 | header?.clone(), 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/gherkin/models/table_row.dart: -------------------------------------------------------------------------------- 1 | class TableRow { 2 | final bool isHeaderRow; 3 | final int rowIndex; 4 | Iterable _columns; 5 | Iterable get columns => _columns; 6 | 7 | TableRow( 8 | this._columns, 9 | this.rowIndex, { 10 | required this.isHeaderRow, 11 | }); 12 | 13 | void setStepParameter(String parameterName, String value) { 14 | _columns = _columns.map((c) => c?.replaceAll('<$parameterName>', value)); 15 | } 16 | 17 | TableRow clone() => TableRow( 18 | List.of(_columns), 19 | rowIndex, 20 | isHeaderRow: isHeaderRow, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/custom_parameter.dart: -------------------------------------------------------------------------------- 1 | typedef Transformer = TValue Function(String value); 2 | 3 | /// A class used to define and parse custom parameters in step definitions 4 | /// see https://docs.cucumber.io/cucumber/cucumber-expressions/#custom-parameter-types 5 | abstract class CustomParameter { 6 | /// the name in the step definition to search for. This is combined with the identifier prefix / suffix 7 | /// to create a replaceable token that signals this parameter for example 8 | /// "My name is {string}" so the name would be "string". 9 | final String name; 10 | 11 | /// the regex pattern that can parse the step string 12 | /// For example: 13 | /// Template: "My name is {string}" 14 | /// Step: "My name is 'Jon'" 15 | /// Regex: "['|\"](.*)['|\"]" 16 | /// The above regex would pull out the word "Jon" from the step 17 | final RegExp pattern; 18 | 19 | /// A transformer function that takes a string and return the correct type of this parameter 20 | final Transformer transformer; 21 | 22 | /// The prefix used for the name token to identify this parameter. Defaults to "{". 23 | final String identifierPrefix; 24 | 25 | /// The suffix used for the name token to identify this parameter. Defaults to "}". 26 | final String identifierSuffix; 27 | 28 | /// If this parameter should be included in the list of step arguments. Defaults to true. 29 | final bool includeInParameterList; 30 | 31 | String get identifier => '$identifierPrefix$name$identifierSuffix'; 32 | 33 | CustomParameter( 34 | this.name, 35 | this.pattern, 36 | this.transformer, { 37 | this.identifierPrefix = '{', 38 | this.identifierSuffix = '}', 39 | this.includeInParameterList = true, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/float_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class FloatParameterBase extends CustomParameter { 4 | FloatParameterBase(String name) 5 | : super(name, RegExp(r'(-?[0-9]+\.?[0-9]*)'), (String input) { 6 | final n = double.parse(input); 7 | return n; 8 | }); 9 | } 10 | 11 | class FloatParameterLower extends FloatParameterBase { 12 | FloatParameterLower() : super('float'); 13 | } 14 | 15 | class FloatParameterCamel extends FloatParameterBase { 16 | FloatParameterCamel() : super('Float'); 17 | } 18 | 19 | class NumParameterLower extends FloatParameterBase { 20 | NumParameterLower() : super('num'); 21 | } 22 | 23 | class NumParameterCamel extends FloatParameterBase { 24 | NumParameterCamel() : super('Num'); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/int_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class IntParameterBase extends CustomParameter { 4 | IntParameterBase(String name) 5 | : super(name, RegExp('([0-9]+)'), (String input) { 6 | final n = int.parse(input, radix: 10); 7 | return n; 8 | }); 9 | } 10 | 11 | class IntParameterLower extends IntParameterBase { 12 | IntParameterLower() : super('int'); 13 | } 14 | 15 | class IntParameterCamel extends IntParameterBase { 16 | IntParameterCamel() : super('Int'); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/plural_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class PluralParameter extends CustomParameter { 4 | PluralParameter() 5 | : super( 6 | 's', 7 | RegExp('(?:s)?'), 8 | (String input) => null, 9 | identifierPrefix: '(', 10 | identifierSuffix: ')', 11 | includeInParameterList: false, 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/step_defined_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class UserDefinedStepParameterParameter extends CustomParameter { 4 | UserDefinedStepParameterParameter() 5 | : super( 6 | '', 7 | RegExp(''), 8 | (String input) => input, 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/string_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class StringParameterBase extends CustomParameter { 4 | StringParameterBase(String name) 5 | : super( 6 | name, 7 | RegExp("['\"](.*)['\"]", dotAll: true), 8 | (String input) => input, 9 | ); 10 | } 11 | 12 | class StringParameterLower extends StringParameterBase { 13 | StringParameterLower() : super('string'); 14 | } 15 | 16 | class StringParameterCamel extends StringParameterBase { 17 | StringParameterCamel() : super('String'); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/word_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'custom_parameter.dart'; 2 | 3 | class WordParameterBase extends CustomParameter { 4 | WordParameterBase(String name) 5 | : super(name, RegExp("['|\"](\\w+)['|\"]"), (String input) => input); 6 | } 7 | 8 | class WordParameterLower extends WordParameterBase { 9 | WordParameterLower() : super('word'); 10 | } 11 | 12 | class WordParameterCamel extends WordParameterBase { 13 | WordParameterCamel() : super('Word'); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/background.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'scenario.dart'; 3 | 4 | class BackgroundRunnable extends ScenarioRunnable { 5 | BackgroundRunnable(String name, RunnableDebugInformation debug) 6 | : super(name, null, debug); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/comment_line.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable.dart'; 3 | 4 | class CommentLineRunnable extends Runnable { 5 | final String comment; 6 | 7 | @override 8 | String get name => 'Comment Line'; 9 | 10 | CommentLineRunnable(this.comment, RunnableDebugInformation debug) 11 | : super(debug); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/debug_information.dart: -------------------------------------------------------------------------------- 1 | class RunnableDebugInformation { 2 | final String filePath; 3 | final int lineNumber; 4 | final String lineText; 5 | 6 | RunnableDebugInformation(this.filePath, this.lineNumber, this.lineText); 7 | 8 | RunnableDebugInformation.empty() 9 | : filePath = '', 10 | lineNumber = 0, 11 | lineText = ''; 12 | 13 | int get nonZeroAdjustedLineNumber => lineNumber + 1; 14 | 15 | RunnableDebugInformation copyWith({ 16 | String? filePath, 17 | int? lineNumber, 18 | String? lineText, 19 | }) { 20 | return RunnableDebugInformation( 21 | filePath ?? this.filePath, 22 | lineNumber ?? this.lineNumber, 23 | lineText ?? this.lineText, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/dialect_block.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../languages/language_service.dart'; 3 | 4 | import 'debug_information.dart'; 5 | import 'runnable.dart'; 6 | 7 | abstract class DialectBlock extends Runnable { 8 | DialectBlock(RunnableDebugInformation debug) : super(debug); 9 | 10 | GherkinDialect getDialect(LanguageService languageService); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/empty_line.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable.dart'; 3 | 4 | class EmptyLineRunnable extends Runnable { 5 | @override 6 | String get name => 'Empty Line'; 7 | 8 | EmptyLineRunnable(RunnableDebugInformation debug) : super(debug); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/example.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/syntax_error.dart'; 2 | import '../models/table.dart'; 3 | import 'debug_information.dart'; 4 | import 'runnable.dart'; 5 | import 'table.dart'; 6 | import 'taggable_runnable_block.dart'; 7 | import 'tags.dart'; 8 | 9 | class ExampleRunnable extends TaggableRunnableBlock { 10 | final String _name; 11 | GherkinTable? table; 12 | String? description; 13 | 14 | ExampleRunnable( 15 | this._name, 16 | RunnableDebugInformation debug, 17 | ) : super(debug); 18 | 19 | @override 20 | String get name => _name; 21 | 22 | @override 23 | void addChild(Runnable child) { 24 | switch (child.runtimeType) { 25 | case TableRunnable: 26 | if (table != null) { 27 | throw GherkinSyntaxException( 28 | "Only a single table can be added to the example '$name'", 29 | ); 30 | } 31 | 32 | table = (child as TableRunnable).toTable(); 33 | break; 34 | case TagsRunnable: 35 | addTag(child as TagsRunnable); 36 | break; 37 | default: 38 | throw Exception( 39 | "Unknown runnable child given to Step '${child.runtimeType}'", 40 | ); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/feature.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/syntax_error.dart'; 2 | import 'background.dart'; 3 | import 'comment_line.dart'; 4 | import 'debug_information.dart'; 5 | import 'empty_line.dart'; 6 | import 'runnable.dart'; 7 | import 'scenario.dart'; 8 | import 'scenario_outline.dart'; 9 | import 'taggable_runnable_block.dart'; 10 | import 'tags.dart'; 11 | import 'text_line.dart'; 12 | 13 | class FeatureRunnable extends TaggableRunnableBlock { 14 | final String _name; 15 | String? description; 16 | final List scenarios = []; 17 | final List _tagsPendingAssignmentToChild = []; 18 | BackgroundRunnable? background; 19 | 20 | FeatureRunnable(this._name, RunnableDebugInformation debug) : super(debug); 21 | 22 | @override 23 | String get name => _name; 24 | 25 | @override 26 | void addChild(Runnable child) { 27 | switch (child.runtimeType) { 28 | case TextLineRunnable: 29 | description = 30 | "${description == null ? "" : "$description\n"}${(child as TextLineRunnable).text}"; 31 | break; 32 | case TagsRunnable: 33 | _tagsPendingAssignmentToChild.add(child as TagsRunnable); 34 | break; 35 | case ScenarioRunnable: 36 | case ScenarioOutlineRunnable: 37 | Iterable childScenarios = [child as ScenarioRunnable]; 38 | if (child is ScenarioOutlineRunnable) { 39 | childScenarios = child.expandOutlinesIntoScenarios(); 40 | } 41 | 42 | scenarios.addAll(childScenarios); 43 | if (_tagsPendingAssignmentToChild.isNotEmpty) { 44 | for (final t in _tagsPendingAssignmentToChild) { 45 | childScenarios.forEach((s) => s.addTag(t)); 46 | } 47 | _tagsPendingAssignmentToChild.clear(); 48 | } 49 | 50 | break; 51 | case BackgroundRunnable: 52 | if (background == null) { 53 | background = child as BackgroundRunnable; 54 | } else { 55 | throw GherkinSyntaxException( 56 | 'Feature file can only contain one background block. ' 57 | "File'${debug.filePath}' :: line '${child.debug.lineNumber}'", 58 | ); 59 | } 60 | break; 61 | case EmptyLineRunnable: 62 | case CommentLineRunnable: 63 | break; 64 | default: 65 | throw Exception( 66 | "Unknown runnable child given to Feature '${child.runtimeType}' - Line#${child.debug.lineText}: '${child.debug.lineText}'", 67 | ); 68 | } 69 | } 70 | 71 | @override 72 | void onTagAdded(TagsRunnable tag) { 73 | for (final scenario in scenarios) { 74 | scenario.addTag(tag.clone(inherited: true)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/feature_file.dart: -------------------------------------------------------------------------------- 1 | import 'comment_line.dart'; 2 | import 'debug_information.dart'; 3 | import 'empty_line.dart'; 4 | import 'feature.dart'; 5 | import 'language.dart'; 6 | import 'runnable.dart'; 7 | import 'runnable_block.dart'; 8 | import 'tags.dart'; 9 | 10 | class FeatureFile extends RunnableBlock { 11 | String _language = 'en'; 12 | final List _tagsPendingAssignmentToChild = []; 13 | 14 | List features = []; 15 | 16 | FeatureFile(RunnableDebugInformation debug) : super(debug); 17 | 18 | String get language => _language; 19 | 20 | @override 21 | void addChild(Runnable child) { 22 | switch (child.runtimeType) { 23 | case LanguageRunnable: 24 | _language = (child as LanguageRunnable).language; 25 | break; 26 | case TagsRunnable: 27 | _tagsPendingAssignmentToChild.add(child as TagsRunnable); 28 | break; 29 | case FeatureRunnable: 30 | features.add(child as FeatureRunnable); 31 | if (_tagsPendingAssignmentToChild.isNotEmpty) { 32 | for (final tag in _tagsPendingAssignmentToChild) { 33 | child.addTag(tag); 34 | } 35 | _tagsPendingAssignmentToChild.clear(); 36 | } 37 | break; 38 | case CommentLineRunnable: 39 | case EmptyLineRunnable: 40 | break; 41 | default: 42 | throw Exception( 43 | "Unknown runnable child given to FeatureFile '${child.runtimeType}'", 44 | ); 45 | } 46 | } 47 | 48 | @override 49 | String get name => debug.filePath; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/language.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../languages/language_service.dart'; 3 | 4 | import 'debug_information.dart'; 5 | import 'dialect_block.dart'; 6 | 7 | class LanguageRunnable extends DialectBlock { 8 | late String language; 9 | 10 | @override 11 | String get name => 'Language'; 12 | 13 | LanguageRunnable(RunnableDebugInformation debug) : super(debug); 14 | 15 | @override 16 | GherkinDialect getDialect(LanguageService languageService) => 17 | languageService.getDialect(language); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/multi_line_string.dart: -------------------------------------------------------------------------------- 1 | import 'comment_line.dart'; 2 | 3 | import 'debug_information.dart'; 4 | import 'empty_line.dart'; 5 | import 'runnable.dart'; 6 | import 'runnable_block.dart'; 7 | import 'text_line.dart'; 8 | 9 | class MultilineStringRunnable extends RunnableBlock { 10 | int? leadingWhitespace; 11 | 12 | List lines = []; 13 | 14 | @override 15 | String get name => 'Multiline String'; 16 | 17 | MultilineStringRunnable( 18 | RunnableDebugInformation debug, { 19 | this.leadingWhitespace, 20 | }) : super(debug); 21 | 22 | @override 23 | void addChild(Runnable child) { 24 | final exception = Exception( 25 | "Unknown runnable child given to Multiline string '${child.runtimeType}'", 26 | ); 27 | switch (child.runtimeType) { 28 | case TextLineRunnable: 29 | final text = (child as TextLineRunnable).originalText ?? child.text; 30 | lines.add(stripLeadingIndentation(text)); 31 | break; 32 | case EmptyLineRunnable: 33 | lines.add(''); 34 | break; 35 | case CommentLineRunnable: 36 | // at the moment we ignore comments in multiline strings 37 | // this seems standard behaviour in other gherkin implementations 38 | break; 39 | default: 40 | throw exception; 41 | } 42 | } 43 | 44 | /// Trim but retain intentional indentation 45 | String stripLeadingIndentation(String lineText) { 46 | if (lines.isEmpty && leadingWhitespace == null) { 47 | leadingWhitespace = 48 | RegExp(r'^(\s*)').firstMatch(lineText)?.group(1)?.length ?? 0; 49 | } 50 | 51 | return lineText.substring(leadingWhitespace ?? 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/runnable.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | 3 | abstract class Runnable { 4 | RunnableDebugInformation debug; 5 | String get name; 6 | 7 | Runnable(this.debug); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/runnable_block.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable.dart'; 3 | 4 | abstract class RunnableBlock extends Runnable { 5 | RunnableBlock(RunnableDebugInformation debug) : super(debug); 6 | 7 | void addChild(Runnable child); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/runnable_result.dart: -------------------------------------------------------------------------------- 1 | enum RunnableResultState { ignored, skipped, failed, passed } 2 | 3 | class RunnableResult { 4 | final RunnableResultState state; 5 | final dynamic result; 6 | final Exception? error; 7 | 8 | RunnableResult( 9 | this.state, { 10 | this.result, 11 | this.error, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario.dart: -------------------------------------------------------------------------------- 1 | import 'comment_line.dart'; 2 | import 'debug_information.dart'; 3 | import 'empty_line.dart'; 4 | import 'runnable.dart'; 5 | import 'scenario_type_enum.dart'; 6 | import 'step.dart'; 7 | import 'taggable_runnable_block.dart'; 8 | import 'text_line.dart'; 9 | 10 | class ScenarioRunnable extends TaggableRunnableBlock { 11 | final String _name; 12 | String? description; 13 | List steps = []; 14 | 15 | ScenarioType get scenarioType => ScenarioType.scenario; 16 | 17 | ScenarioRunnable( 18 | this._name, 19 | this.description, 20 | RunnableDebugInformation debug, 21 | ) : super(debug); 22 | 23 | @override 24 | String get name => _name; 25 | 26 | @override 27 | void addChild(Runnable child) { 28 | switch (child.runtimeType) { 29 | case StepRunnable: 30 | steps.add(child as StepRunnable); 31 | break; 32 | case TextLineRunnable: 33 | description = (child as TextLineRunnable).text; 34 | break; 35 | case CommentLineRunnable: 36 | case EmptyLineRunnable: 37 | break; 38 | default: 39 | throw Exception( 40 | "Unknown runnable child given to Scenario '${child.runtimeType}'", 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario_expanded_from_outline_example.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'scenario.dart'; 3 | import 'scenario_type_enum.dart'; 4 | 5 | class ScenarioExpandedFromOutlineExampleRunnable extends ScenarioRunnable { 6 | String _name; 7 | 8 | @override 9 | ScenarioType get scenarioType => ScenarioType.scenarioOutline; 10 | 11 | @override 12 | String get name => _name; 13 | 14 | ScenarioExpandedFromOutlineExampleRunnable( 15 | String name, 16 | String? description, 17 | RunnableDebugInformation debug, 18 | ) : _name = name, 19 | super( 20 | name, 21 | description, 22 | debug, 23 | ); 24 | 25 | void setStepParameter(String parameterName, String value) { 26 | _name = _name.replaceAll('<$parameterName>', value); 27 | debug = debug.copyWith( 28 | lineNumber: debug.lineNumber, 29 | lineText: debug.lineText.replaceAll('<$parameterName>', value), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario_outline.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/syntax_error.dart'; 2 | import 'debug_information.dart'; 3 | import 'example.dart'; 4 | import 'runnable.dart'; 5 | import 'scenario.dart'; 6 | import 'scenario_expanded_from_outline_example.dart'; 7 | import 'tags.dart'; 8 | 9 | class ScenarioOutlineRunnable extends ScenarioRunnable { 10 | final List _examples = []; 11 | TagsRunnable? _pendingExampleTags; 12 | Iterable get examples => _examples; 13 | 14 | ScenarioOutlineRunnable( 15 | String name, 16 | String? description, 17 | RunnableDebugInformation debug, 18 | ) : super( 19 | name, 20 | description, 21 | debug, 22 | ); 23 | 24 | @override 25 | void addChild(Runnable child) { 26 | switch (child.runtimeType) { 27 | case ExampleRunnable: 28 | if (_pendingExampleTags != null) { 29 | (child as ExampleRunnable).addChild(_pendingExampleTags!); 30 | _pendingExampleTags = null; 31 | } 32 | 33 | _examples.add(child as ExampleRunnable); 34 | break; 35 | case TagsRunnable: 36 | _pendingExampleTags = child as TagsRunnable; 37 | break; 38 | default: 39 | super.addChild(child); 40 | } 41 | } 42 | 43 | @override 44 | void onTagAdded(TagsRunnable tag) { 45 | examples.forEach( 46 | (ex) { 47 | ex.addTag(tag.clone(inherited: true)); 48 | }, 49 | ); 50 | } 51 | 52 | Iterable expandOutlinesIntoScenarios() { 53 | if (examples.isEmpty) { 54 | throw GherkinSyntaxException( 55 | 'Scenario outline `$name` does not contains an example block.', 56 | ); 57 | } 58 | 59 | final scenarios = []; 60 | examples.forEach( 61 | (example) { 62 | example.table!.asMap().toList(growable: false).asMap().forEach( 63 | (exampleIndex, exampleRow) { 64 | final exampleName = [ 65 | name, 66 | 'Examples:', 67 | if (example.name.isNotEmpty) example.name, 68 | '(${exampleIndex + 1})', 69 | ].join(' '); 70 | 71 | final clonedSteps = steps.map((step) => step.clone()).toList(); 72 | 73 | final scenarioRunnable = ScenarioExpandedFromOutlineExampleRunnable( 74 | exampleName, 75 | description, 76 | debug, 77 | ); 78 | 79 | exampleRow.forEach( 80 | (parameterName, value) { 81 | scenarioRunnable.setStepParameter(parameterName, value ?? ''); 82 | clonedSteps.forEach( 83 | (step) => step.setStepParameter(parameterName, value ?? ''), 84 | ); 85 | }, 86 | ); 87 | 88 | [...tags, ...example.tags] 89 | .forEach((t) => scenarioRunnable.addTag(t.clone())); 90 | 91 | clonedSteps.forEach((step) => scenarioRunnable.addChild(step)); 92 | scenarios.add(scenarioRunnable); 93 | }, 94 | ); 95 | }, 96 | ); 97 | 98 | return scenarios; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario_type_enum.dart: -------------------------------------------------------------------------------- 1 | enum ScenarioType { scenario, scenarioOutline } 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/step.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/syntax_error.dart'; 2 | import '../models/table.dart'; 3 | import 'debug_information.dart'; 4 | import 'multi_line_string.dart'; 5 | import 'runnable.dart'; 6 | import 'runnable_block.dart'; 7 | import 'table.dart'; 8 | 9 | class StepRunnable extends RunnableBlock { 10 | String _name; 11 | String? description; 12 | GherkinTable? table; 13 | List multilineStrings = []; 14 | 15 | StepRunnable(this._name, RunnableDebugInformation debug) : super(debug); 16 | 17 | @override 18 | String get name => _name; 19 | 20 | @override 21 | void addChild(Runnable child) { 22 | switch (child.runtimeType) { 23 | case MultilineStringRunnable: 24 | multilineStrings 25 | .add((child as MultilineStringRunnable).lines.join('\n')); 26 | break; 27 | case TableRunnable: 28 | if (table != null) { 29 | throw GherkinSyntaxException( 30 | "Only a single table can be added to the step '$name'", 31 | ); 32 | } 33 | 34 | table = (child as TableRunnable).toTable(); 35 | break; 36 | default: 37 | throw Exception( 38 | "Unknown runnable child given to Step '${child.runtimeType}'", 39 | ); 40 | } 41 | } 42 | 43 | void setStepParameter(String parameterName, String value) { 44 | _name = _name.replaceAll('<$parameterName>', value); 45 | table?.setStepParameter(parameterName, value); 46 | debug = debug.copyWith( 47 | lineNumber: debug.lineNumber, 48 | lineText: debug.lineText.replaceAll('<$parameterName>', value), 49 | ); 50 | } 51 | 52 | StepRunnable clone() { 53 | final cloned = StepRunnable(_name, debug); 54 | cloned.multilineStrings = multilineStrings.map((s) => s).toList(); 55 | cloned.table = table?.clone(); 56 | 57 | return cloned; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/table.dart: -------------------------------------------------------------------------------- 1 | import '../models/table.dart'; 2 | import '../models/table_row.dart'; 3 | import 'comment_line.dart'; 4 | import 'debug_information.dart'; 5 | import 'runnable.dart'; 6 | import 'runnable_block.dart'; 7 | 8 | class TableRunnable extends RunnableBlock { 9 | final List rows = []; 10 | 11 | @override 12 | String get name => 'Table'; 13 | 14 | TableRunnable(RunnableDebugInformation debug) : super(debug); 15 | 16 | @override 17 | void addChild(Runnable child) { 18 | switch (child.runtimeType) { 19 | case TableRunnable: 20 | rows.addAll((child as TableRunnable).rows); 21 | break; 22 | case CommentLineRunnable: 23 | break; 24 | default: 25 | throw Exception( 26 | "Unknown runnable child given to Table '${child.runtimeType}'", 27 | ); 28 | } 29 | } 30 | 31 | GherkinTable toTable() { 32 | TableRow? header; 33 | final tableRows = []; 34 | if (rows.length > 1) { 35 | header = _toRow(rows.first, 0, true); 36 | } 37 | 38 | for (var i = header == null ? 0 : 1; i < rows.length; i += 1) { 39 | tableRows.add(_toRow(rows.elementAt(i), i)); 40 | } 41 | 42 | return GherkinTable(tableRows, header); 43 | } 44 | 45 | TableRow _toRow(String raw, int rowIndex, [bool isHeaderRow = false]) { 46 | final columns = raw 47 | .trim() 48 | .split(RegExp(r'(? c.trim().replaceAll(r'\|', '|')) 50 | .map((c) => c.isEmpty ? null : c) 51 | .skip(1); 52 | 53 | return TableRow( 54 | columns.take(columns.length - 1).toList( 55 | growable: false, 56 | ), 57 | rowIndex, 58 | isHeaderRow: isHeaderRow, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/taggable_runnable_block.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable_block.dart'; 3 | import 'tags.dart'; 4 | 5 | abstract class TaggableRunnableBlock extends RunnableBlock { 6 | final List _tags = []; 7 | 8 | Iterable get tags => _tags.toList() 9 | ..sort((a, b) => a.debug.lineNumber.compareTo(b.debug.lineNumber)); 10 | 11 | TaggableRunnableBlock(RunnableDebugInformation debug) : super(debug); 12 | 13 | void addTag(TagsRunnable tag) { 14 | _tags.add(tag); 15 | onTagAdded(tag); 16 | } 17 | 18 | void onTagAdded(TagsRunnable tag) {} 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/tags.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable.dart'; 3 | 4 | class TagsRunnable extends Runnable { 5 | late Iterable tags; 6 | bool isInherited = false; 7 | 8 | @override 9 | String get name => 'Tags'; 10 | 11 | TagsRunnable(RunnableDebugInformation debug) : super(debug); 12 | 13 | TagsRunnable clone({ 14 | bool inherited = false, 15 | }) { 16 | return TagsRunnable(debug) 17 | ..tags = tags.map((t) => t).toList() 18 | ..isInherited = inherited; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/text_line.dart: -------------------------------------------------------------------------------- 1 | import 'debug_information.dart'; 2 | import 'runnable.dart'; 3 | 4 | class TextLineRunnable extends Runnable { 5 | /// The trimmed version of the line 6 | late String text; 7 | 8 | /// While [text] can be `trim()`'d, original whitespace will be preserved 9 | /// in `originalText`. 10 | late String? originalText; 11 | 12 | @override 13 | String get name => 'Language'; 14 | 15 | TextLineRunnable(RunnableDebugInformation debug) : super(debug); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/executable_step.dart: -------------------------------------------------------------------------------- 1 | import '../expressions/gherkin_expression.dart'; 2 | import 'step_definition.dart'; 3 | import 'world.dart'; 4 | 5 | class ExecutableStep { 6 | final GherkinExpression expression; 7 | final StepDefinitionGeneric step; 8 | 9 | ExecutableStep(this.expression, this.step); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_configuration.dart: -------------------------------------------------------------------------------- 1 | class StepDefinitionConfiguration { 2 | Duration? timeout; 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_definition.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../../expect/expect_mimic.dart'; 4 | import '../../reporters/reporter.dart'; 5 | import '../../utils/perf.dart'; 6 | import '../exceptions/parameter_count_mismatch_error.dart'; 7 | import 'step_configuration.dart'; 8 | import 'step_run_result.dart'; 9 | import 'world.dart'; 10 | 11 | abstract class StepDefinitionGeneric { 12 | final StepDefinitionConfiguration? config; 13 | final int _expectParameterCount; 14 | late TWorld _world; 15 | late Reporter _reporter; 16 | Duration? _timeout; 17 | Pattern get pattern; 18 | 19 | StepDefinitionGeneric(this.config, this._expectParameterCount) { 20 | _timeout = config?.timeout; 21 | } 22 | 23 | TWorld get world => _world; 24 | Duration? get timeout => _timeout; 25 | Reporter get reporter => _reporter; 26 | 27 | Future run( 28 | TWorld world, 29 | Reporter reporter, 30 | Duration defaultTimeout, 31 | Iterable parameters, 32 | ) async { 33 | _ensureParameterCount(parameters.length, _expectParameterCount); 34 | late int elapsedMilliseconds; 35 | try { 36 | final timeout = _timeout ?? defaultTimeout; 37 | await Perf.measure( 38 | () async { 39 | _world = world; 40 | _reporter = reporter; 41 | _timeout = timeout; 42 | final result = await onRun(parameters).timeout( 43 | timeout, 44 | ); 45 | 46 | return result; 47 | }, 48 | (ms) => elapsedMilliseconds = ms, 49 | ); 50 | } on GherkinTestFailure catch (tf) { 51 | return StepResult( 52 | elapsedMilliseconds, 53 | StepExecutionResult.fail, 54 | resultReason: tf.message, 55 | ); 56 | } on TimeoutException catch (te, st) { 57 | return ErroredStepResult( 58 | elapsedMilliseconds, 59 | StepExecutionResult.timeout, 60 | te, 61 | st, 62 | ); 63 | } catch (e, st) { 64 | final err = e is Exception ? e : Exception(e.toString()); 65 | final reason = e is StateError ? e.message : err.toString(); 66 | 67 | return ErroredStepResult( 68 | elapsedMilliseconds, 69 | StepExecutionResult.error, 70 | err, 71 | st, 72 | resultReason: reason, 73 | ); 74 | } 75 | 76 | return StepResult(elapsedMilliseconds, StepExecutionResult.passed); 77 | } 78 | 79 | Future onRun(Iterable parameters); 80 | 81 | void _ensureParameterCount(int actual, int expected) { 82 | if (actual != expected) { 83 | throw GherkinStepParameterMismatchException( 84 | runtimeType, 85 | expected, 86 | actual, 87 | ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_run_result.dart: -------------------------------------------------------------------------------- 1 | enum StepExecutionResult { 2 | passed, 3 | fail, 4 | skipped, 5 | timeout, 6 | error, 7 | } 8 | 9 | class StepResult { 10 | /// The duration in milliseconds the step took to run 11 | final int elapsedMilliseconds; 12 | 13 | /// The result of executing the step 14 | final StepExecutionResult result; 15 | 16 | // A reason for the result 17 | // This would be a failure message if the result failed. 18 | final String? resultReason; 19 | 20 | StepResult( 21 | this.elapsedMilliseconds, 22 | this.result, { 23 | this.resultReason, 24 | }); 25 | } 26 | 27 | class ErroredStepResult extends StepResult { 28 | final Object exception; 29 | final StackTrace stackTrace; 30 | 31 | ErroredStepResult( 32 | int elapsedMilliseconds, 33 | StepExecutionResult result, 34 | this.exception, 35 | this.stackTrace, { 36 | String? resultReason, 37 | }) : super( 38 | elapsedMilliseconds, 39 | result, 40 | resultReason: resultReason, 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/world.dart: -------------------------------------------------------------------------------- 1 | import '../attachments/attachment_manager.dart'; 2 | 3 | class World { 4 | late AttachmentManager _attachmentManager; 5 | 6 | void setAttachmentManager(AttachmentManager attachmentManager) { 7 | _attachmentManager = attachmentManager; 8 | } 9 | 10 | /// Attach data to the given [context] which can be a step name 11 | /// or if blank it will be attached to the scenario 12 | /// [mimeType] one of 'text/plain', 'text/html', 'application/json', 'image/png' 13 | void attach( 14 | String data, 15 | String mimeType, [ 16 | String? context, 17 | ]) { 18 | _attachmentManager.attach(data, mimeType, context); 19 | } 20 | 21 | void dispose() { 22 | _attachmentManager.dispose(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/background_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/background.dart'; 4 | import '../runnables/debug_information.dart'; 5 | import '../runnables/runnable.dart'; 6 | import 'empty_line_syntax.dart'; 7 | import 'regex_matched_syntax.dart'; 8 | import 'scenario_syntax.dart'; 9 | import 'syntax_matcher.dart'; 10 | import 'tag_syntax.dart'; 11 | 12 | class BackgroundSyntax extends RegExMatchedGherkinSyntax { 13 | @override 14 | RegExp pattern(GherkinDialect dialect) { 15 | final dialectPattern = 16 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern( 17 | dialect.background, 18 | ); 19 | 20 | return RegExp( 21 | '^\\s*$dialectPattern:(\\s*(.+)\\s*)?', 22 | multiLine: false, 23 | caseSensitive: false, 24 | ); 25 | } 26 | 27 | @override 28 | bool get isBlockSyntax => true; 29 | 30 | @override 31 | bool hasBlockEnded(SyntaxMatcher syntax) => 32 | syntax is ScenarioSyntax || 33 | syntax is EmptyLineSyntax || 34 | syntax is TagSyntax; 35 | 36 | @override 37 | Runnable toRunnable( 38 | String line, 39 | RunnableDebugInformation debug, 40 | GherkinDialect dialect, 41 | ) { 42 | final name = (pattern(dialect).firstMatch(line)?.group(1) ?? '').trim(); 43 | final runnable = BackgroundRunnable(name, debug); 44 | 45 | return runnable; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/comment_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/comment_line.dart'; 4 | import '../runnables/debug_information.dart'; 5 | import '../runnables/runnable.dart'; 6 | import 'regex_matched_syntax.dart'; 7 | 8 | class CommentSyntax extends RegExMatchedGherkinSyntax { 9 | @override 10 | RegExp pattern(GherkinDialect dialect) => RegExp( 11 | '^#', 12 | multiLine: false, 13 | caseSensitive: false, 14 | ); 15 | 16 | @override 17 | Runnable toRunnable( 18 | String line, 19 | RunnableDebugInformation debug, 20 | GherkinDialect dialect, 21 | ) => 22 | CommentLineRunnable(line.trim(), debug); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/empty_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/empty_line.dart'; 5 | import '../runnables/runnable.dart'; 6 | import 'regex_matched_syntax.dart'; 7 | 8 | class EmptyLineSyntax extends RegExMatchedGherkinSyntax { 9 | @override 10 | RegExp pattern(GherkinDialect dialect) => RegExp( 11 | r'^\s*$', 12 | multiLine: false, 13 | caseSensitive: false, 14 | ); 15 | 16 | @override 17 | Runnable toRunnable( 18 | String line, 19 | RunnableDebugInformation debug, 20 | GherkinDialect dialect, 21 | ) => 22 | EmptyLineRunnable(debug); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/example_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/example.dart'; 4 | import '../runnables/runnable.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | import 'syntax_matcher.dart'; 7 | import 'table_line_syntax.dart'; 8 | 9 | class ExampleSyntax extends RegExMatchedGherkinSyntax { 10 | @override 11 | RegExp pattern(GherkinDialect dialect) { 12 | final dialectPattern = 13 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.examples); 14 | 15 | return RegExp( 16 | '^\\s*(?:$dialectPattern):(\\s*(.+)\\s*)?\$', 17 | multiLine: false, 18 | caseSensitive: false, 19 | ); 20 | } 21 | 22 | @override 23 | bool get isBlockSyntax => true; 24 | 25 | @override 26 | bool hasBlockEnded(SyntaxMatcher syntax) => syntax is! TableLineSyntax; 27 | 28 | @override 29 | Runnable toRunnable( 30 | String line, 31 | RunnableDebugInformation debug, 32 | GherkinDialect dialect, 33 | ) { 34 | final name = (pattern(dialect).firstMatch(line)?.group(1) ?? '').trim(); 35 | final runnable = ExampleRunnable(name, debug); 36 | 37 | return runnable; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/feature_file_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/runnable.dart'; 4 | import 'syntax_matcher.dart'; 5 | 6 | class FeatureFileSyntax extends SyntaxMatcher { 7 | @override 8 | bool get isBlockSyntax => true; 9 | 10 | @override 11 | bool hasBlockEnded(SyntaxMatcher syntax) => false; 12 | 13 | @override 14 | bool isMatch(String line, GherkinDialect dialect) { 15 | return false; 16 | } 17 | 18 | @override 19 | Runnable toRunnable( 20 | String line, 21 | RunnableDebugInformation debug, 22 | GherkinDialect dialect, 23 | ) { 24 | throw UnimplementedError(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/feature_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/feature.dart'; 4 | import '../runnables/runnable.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | import 'syntax_matcher.dart'; 7 | 8 | class FeatureSyntax extends RegExMatchedGherkinSyntax { 9 | @override 10 | bool get isBlockSyntax => true; 11 | 12 | @override 13 | bool hasBlockEnded(SyntaxMatcher syntax) => false; 14 | 15 | @override 16 | Runnable toRunnable( 17 | String line, 18 | RunnableDebugInformation debug, 19 | GherkinDialect dialect, 20 | ) { 21 | final name = pattern(dialect).firstMatch(line)?.group(1); 22 | final runnable = FeatureRunnable(name!, debug); 23 | return runnable; 24 | } 25 | 26 | @override 27 | RegExp pattern(GherkinDialect dialect) { 28 | final dialectPattern = 29 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.feature); 30 | return RegExp( 31 | '^(?:$dialectPattern):\\s*(.+)\\s*', 32 | multiLine: false, 33 | caseSensitive: false, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/language_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/language.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | 7 | /// see https://docs.cucumber.io/gherkin/reference/#gherkin-dialects 8 | class LanguageSyntax extends RegExMatchedGherkinSyntax { 9 | @override 10 | RegExp pattern(GherkinDialect dialect) => RegExp( 11 | r'^\s*#\s*language:\s*([a-z-]{2,16})\s*$', 12 | multiLine: false, 13 | caseSensitive: false, 14 | ); 15 | 16 | @override 17 | LanguageRunnable toRunnable( 18 | String line, 19 | RunnableDebugInformation debug, 20 | GherkinDialect dialect, 21 | ) { 22 | final runnable = LanguageRunnable(debug); 23 | runnable.language = pattern(dialect).firstMatch(line)!.group(1)!; 24 | 25 | return runnable; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/multiline_string_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../exceptions/syntax_error.dart'; 2 | import '../languages/dialect.dart'; 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/multi_line_string.dart'; 5 | import 'comment_syntax.dart'; 6 | import 'empty_line_syntax.dart'; 7 | import 'regex_matched_syntax.dart'; 8 | import 'syntax_matcher.dart'; 9 | import 'text_line_syntax.dart'; 10 | 11 | class MultilineStringSyntax 12 | extends RegExMatchedGherkinSyntax { 13 | @override 14 | RegExp pattern(GherkinDialect dialect) => RegExp( 15 | r'^\s*(' 16 | '"""' 17 | r"|'''|```)\s*$", 18 | multiLine: false, 19 | caseSensitive: false, 20 | ); 21 | 22 | @override 23 | bool get isBlockSyntax => true; 24 | 25 | @override 26 | bool hasBlockEnded(SyntaxMatcher syntax) { 27 | if (syntax is MultilineStringSyntax) { 28 | return true; 29 | } else if (!(syntax is TextLineSyntax || 30 | syntax is CommentSyntax || 31 | syntax is EmptyLineSyntax)) { 32 | throw GherkinSyntaxException( 33 | 'Multiline string block does not expect ${syntax.runtimeType} syntax. Expects a text line', 34 | ); 35 | } 36 | return false; 37 | } 38 | 39 | @override 40 | MultilineStringRunnable toRunnable( 41 | String line, 42 | RunnableDebugInformation debug, 43 | GherkinDialect dialect, 44 | ) { 45 | final leadingWhitespace = 46 | RegExp(r'^(\s*)').firstMatch(line)?.group(1)?.length ?? 0; 47 | return MultilineStringRunnable(debug, leadingWhitespace: leadingWhitespace); 48 | } 49 | 50 | @override 51 | EndBlockHandling endBlockHandling(SyntaxMatcher syntax) => 52 | syntax is MultilineStringSyntax 53 | ? EndBlockHandling.ignore 54 | : EndBlockHandling.continueProcessing; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/regex_matched_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/runnable.dart'; 3 | 4 | import 'syntax_matcher.dart'; 5 | 6 | abstract class RegExMatchedGherkinSyntax 7 | extends SyntaxMatcher { 8 | RegExp pattern(GherkinDialect dialect); 9 | 10 | @override 11 | bool isMatch(String line, GherkinDialect dialect) => 12 | pattern(dialect).hasMatch(line); 13 | 14 | static String getMultiDialectRegexPattern(Iterable dialectVariants) => 15 | dialectVariants.map((s) => s.trim()).where((s) => s != '*').join('|'); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/scenario_outline_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/scenario_outline.dart'; 4 | import 'regex_matched_syntax.dart'; 5 | import 'scenario_syntax.dart'; 6 | import 'syntax_matcher.dart'; 7 | import 'tag_syntax.dart'; 8 | 9 | class ScenarioOutlineSyntax 10 | extends RegExMatchedGherkinSyntax { 11 | @override 12 | RegExp pattern(GherkinDialect dialect) { 13 | final dialectPattern = 14 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern( 15 | dialect.scenarioOutline, 16 | ); 17 | return RegExp( 18 | '^\\s*(?:$dialectPattern):(?:\\s*(.+)\\s*)?\$', 19 | multiLine: false, 20 | caseSensitive: false, 21 | ); 22 | } 23 | 24 | @override 25 | bool get isBlockSyntax => true; 26 | 27 | @override 28 | bool hasBlockEnded(SyntaxMatcher syntax) => 29 | syntax is ScenarioOutlineSyntax || 30 | syntax is ScenarioSyntax || 31 | (syntax is TagSyntax && syntax.annotating != AnnotatingBlock.examples); 32 | 33 | @override 34 | ScenarioOutlineRunnable toRunnable( 35 | String line, 36 | RunnableDebugInformation debug, 37 | GherkinDialect dialect, 38 | ) { 39 | final name = pattern(dialect).firstMatch(line)!.group(1)!; 40 | return ScenarioOutlineRunnable( 41 | name.trim(), 42 | null, 43 | debug, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/scenario_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/scenario.dart'; 4 | import 'regex_matched_syntax.dart'; 5 | import 'scenario_outline_syntax.dart'; 6 | import 'syntax_matcher.dart'; 7 | import 'tag_syntax.dart'; 8 | 9 | class ScenarioSyntax extends RegExMatchedGherkinSyntax { 10 | @override 11 | RegExp pattern(GherkinDialect dialect) { 12 | final dialectPattern = 13 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.scenario); 14 | 15 | return RegExp( 16 | '^\\s*(?:$dialectPattern):\\s*(.+)\\s*\$', 17 | multiLine: false, 18 | caseSensitive: false, 19 | ); 20 | } 21 | 22 | @override 23 | bool get isBlockSyntax => true; 24 | 25 | @override 26 | bool hasBlockEnded(SyntaxMatcher syntax) => 27 | syntax is ScenarioSyntax || 28 | syntax is ScenarioOutlineSyntax || 29 | syntax is TagSyntax; 30 | 31 | @override 32 | ScenarioRunnable toRunnable( 33 | String line, 34 | RunnableDebugInformation debug, 35 | GherkinDialect dialect, 36 | ) { 37 | final name = pattern(dialect).firstMatch(line)!.group(1)!; 38 | final runnable = ScenarioRunnable( 39 | name, 40 | null, 41 | debug, 42 | ); 43 | 44 | return runnable; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/step_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/step.dart'; 5 | import 'multiline_string_syntax.dart'; 6 | import 'regex_matched_syntax.dart'; 7 | import 'syntax_matcher.dart'; 8 | import 'table_line_syntax.dart'; 9 | 10 | class StepSyntax extends RegExMatchedGherkinSyntax { 11 | @override 12 | RegExp pattern(GherkinDialect dialect) { 13 | final dialectPattern = 14 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern( 15 | dialect.stepKeywords, 16 | ); 17 | 18 | return RegExp( 19 | '^($dialectPattern)\\s?.*', 20 | multiLine: false, 21 | caseSensitive: false, 22 | ); 23 | } 24 | 25 | @override 26 | bool get isBlockSyntax => true; 27 | 28 | @override 29 | bool hasBlockEnded(SyntaxMatcher syntax) => 30 | !(syntax is MultilineStringSyntax || syntax is TableLineSyntax); 31 | 32 | @override 33 | StepRunnable toRunnable( 34 | String line, 35 | RunnableDebugInformation debug, 36 | GherkinDialect dialect, 37 | ) { 38 | final runnable = StepRunnable(line, debug); 39 | return runnable; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/syntax_matcher.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/runnable.dart'; 5 | 6 | enum EndBlockHandling { ignore, continueProcessing } 7 | 8 | abstract class SyntaxMatcher { 9 | bool isMatch(String line, GherkinDialect dialect); 10 | bool get isBlockSyntax => false; 11 | bool hasBlockEnded(SyntaxMatcher syntax) => true; 12 | 13 | EndBlockHandling endBlockHandling(SyntaxMatcher syntax) => 14 | EndBlockHandling.continueProcessing; 15 | 16 | TRunnable toRunnable( 17 | String line, 18 | RunnableDebugInformation debug, 19 | GherkinDialect dialect, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/table_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | import '../runnables/debug_information.dart'; 3 | import '../runnables/table.dart'; 4 | import 'comment_syntax.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | import 'syntax_matcher.dart'; 7 | 8 | class TableLineSyntax extends RegExMatchedGherkinSyntax { 9 | @override 10 | RegExp pattern(GherkinDialect dialect) => RegExp( 11 | r'^\s*(\|.*\|)\s*(?:\s*#\s*.*)?$', 12 | multiLine: false, 13 | caseSensitive: false, 14 | ); 15 | 16 | @override 17 | bool get isBlockSyntax => true; 18 | 19 | @override 20 | bool hasBlockEnded(SyntaxMatcher syntax) { 21 | if (syntax is TableLineSyntax || syntax is CommentSyntax) { 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | @override 29 | TableRunnable toRunnable( 30 | String line, 31 | RunnableDebugInformation debug, 32 | GherkinDialect dialect, 33 | ) { 34 | final runnable = TableRunnable(debug); 35 | runnable.rows.add( 36 | pattern(dialect).firstMatch(line.trim())!.group(1)!.trim(), 37 | ); 38 | 39 | return runnable; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/tag_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/tags.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | 7 | enum AnnotatingBlock { examples, feature, scenarioOutline, scenario } 8 | 9 | class TagSyntax extends RegExMatchedGherkinSyntax { 10 | /// The block following this line such as `Examples` or `Feature` 11 | AnnotatingBlock? annotating; 12 | 13 | @override 14 | RegExp pattern(GherkinDialect dialect) => RegExp( 15 | '^@', 16 | multiLine: false, 17 | caseSensitive: false, 18 | ); 19 | 20 | @override 21 | TagsRunnable toRunnable( 22 | String line, 23 | RunnableDebugInformation debug, 24 | GherkinDialect dialect, 25 | ) { 26 | final runnable = TagsRunnable(debug); 27 | runnable.tags = line 28 | .trim() 29 | .split(RegExp('@')) 30 | .where((t) => t.isNotEmpty) 31 | .map((t) => '@${t.trim()}') 32 | .toList(); 33 | 34 | return runnable; 35 | } 36 | 37 | static AnnotatingBlock? determineAnnotationBlock( 38 | String nextLine, 39 | GherkinDialect dialect, 40 | ) { 41 | final blockLabels = [ 42 | dialect.examples, 43 | dialect.feature, 44 | dialect.scenarioOutline, 45 | dialect.scenario, 46 | ] 47 | .map((b) => RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(b)) 48 | .toList(); 49 | for (final dialectPattern in blockLabels) { 50 | final regex = RegExp('^\\s*(?:$dialectPattern)'); 51 | if (nextLine.startsWith(regex)) { 52 | return AnnotatingBlock.values[blockLabels.indexOf(dialectPattern)]; 53 | } 54 | } 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/text_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import '../languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/text_line.dart'; 5 | import 'regex_matched_syntax.dart'; 6 | 7 | class TextLineSyntax extends RegExMatchedGherkinSyntax { 8 | @override 9 | 10 | /// Regex needs to make sure it does not match comment lines or empty whitespace lines 11 | RegExp pattern(GherkinDialect dialect) => RegExp( 12 | r'^\s*(?!(\s*#\s*.+)|(\s+)).+$', 13 | multiLine: false, 14 | caseSensitive: false, 15 | ); 16 | 17 | @override 18 | TextLineRunnable toRunnable( 19 | String line, 20 | RunnableDebugInformation debug, 21 | GherkinDialect dialect, 22 | ) { 23 | final runnable = TextLineRunnable(debug); 24 | runnable.originalText = line; 25 | runnable.text = line.trim(); 26 | return runnable; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/hooks/aggregated_hook.dart: -------------------------------------------------------------------------------- 1 | import '../configuration.dart'; 2 | import '../gherkin/steps/step_run_result.dart'; 3 | import '../gherkin/steps/world.dart'; 4 | import '../reporters/messages/messages.dart'; 5 | import 'hook.dart'; 6 | 7 | class AggregatedHook extends Hook { 8 | Iterable? _orderedHooks; 9 | 10 | void addHooks(Iterable hooks) { 11 | _orderedHooks = hooks.toList() 12 | ..sort( 13 | (a, b) => b.priority - a.priority, 14 | ); 15 | } 16 | 17 | @override 18 | Future onBeforeRun(TestConfiguration config) => 19 | _invokeHooks((h) => h.onBeforeRun(config)); 20 | 21 | /// Run after all scenarios in a test run have completed 22 | @override 23 | Future onAfterRun(TestConfiguration config) => 24 | _invokeHooks((h) => h.onAfterRun(config)); 25 | 26 | @override 27 | Future onAfterScenarioWorldCreated( 28 | World world, 29 | String scenario, 30 | Iterable tags, 31 | ) => 32 | _invokeHooks( 33 | (h) => h.onAfterScenarioWorldCreated( 34 | world, 35 | scenario, 36 | tags, 37 | ), 38 | ); 39 | 40 | /// Run before a scenario and it steps are executed 41 | @override 42 | Future onBeforeScenario( 43 | TestConfiguration config, 44 | String scenario, 45 | Iterable tags, 46 | ) => 47 | _invokeHooks( 48 | (h) => h.onBeforeScenario( 49 | config, 50 | scenario, 51 | tags, 52 | ), 53 | ); 54 | 55 | /// Run after a scenario has executed 56 | @override 57 | Future onAfterScenario( 58 | TestConfiguration config, 59 | String scenario, 60 | Iterable tags, { 61 | bool passed = true, 62 | }) async => 63 | _invokeHooks( 64 | (h) => h.onAfterScenario( 65 | config, 66 | scenario, 67 | tags, 68 | passed: passed, 69 | ), 70 | ); 71 | 72 | /// Run before a step is executed 73 | @override 74 | Future onBeforeStep( 75 | World world, 76 | String step, 77 | ) => 78 | _invokeHooks((h) => h.onBeforeStep(world, step)); 79 | 80 | /// Run after a step has executed 81 | @override 82 | Future onAfterStep( 83 | World world, 84 | String step, 85 | StepResult result, 86 | ) => 87 | _invokeHooks((h) => h.onAfterStep(world, step, result)); 88 | 89 | Future _invokeHooks( 90 | Future Function(Hook h) invoke, 91 | ) async { 92 | if (_orderedHooks != null && _orderedHooks!.isNotEmpty) { 93 | for (final hook in _orderedHooks!) { 94 | await invoke(hook); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/hooks/hook.dart: -------------------------------------------------------------------------------- 1 | import '../configuration.dart'; 2 | import '../gherkin/steps/step_run_result.dart'; 3 | import '../gherkin/steps/world.dart'; 4 | import '../reporters/messages/messages.dart'; 5 | 6 | /// A hook that is run during certain points in the execution cycle 7 | /// You can override any or none of the methods 8 | abstract class Hook { 9 | /// The priority to assign to this hook. 10 | /// Higher priority gets run first so a priority of 10 is run before a priority of 2 11 | int get priority => 0; 12 | 13 | /// Run before any scenario in a test run have executed 14 | Future onBeforeRun(TestConfiguration config) => Future.value(null); 15 | 16 | /// Run after all scenarios in a test run have completed 17 | Future onAfterRun(TestConfiguration config) => Future.value(null); 18 | 19 | /// Run after the scenario world is created but run before a scenario and its steps are executed 20 | /// Might not be invoked if there is not a world object 21 | Future onAfterScenarioWorldCreated( 22 | World world, 23 | String scenario, 24 | Iterable tags, 25 | ) => 26 | Future.value(null); 27 | 28 | /// Run before a scenario and it steps are executed 29 | Future onBeforeScenario( 30 | TestConfiguration config, 31 | String scenario, 32 | Iterable tags, 33 | ) => 34 | Future.value(null); 35 | 36 | /// Run after a scenario has executed 37 | Future onAfterScenario( 38 | TestConfiguration config, 39 | String scenario, 40 | Iterable tags, { 41 | bool passed = true, 42 | }) => 43 | Future.value(null); 44 | 45 | /// Run before a step is executed 46 | Future onBeforeStep(World world, String step) => Future.value(null); 47 | 48 | /// Run after a step has executed 49 | Future onAfterStep(World world, String step, StepResult stepResult) => 50 | Future.value(null); 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/io/feature_file_matcher.dart: -------------------------------------------------------------------------------- 1 | abstract class FeatureFileMatcher { 2 | Future> listFiles(Pattern pattern); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/io/feature_file_reader.dart: -------------------------------------------------------------------------------- 1 | abstract class FeatureFileReader { 2 | Future read(String path); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/io/io_feature_file_accessor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:path/path.dart'; 5 | 6 | import 'feature_file_matcher.dart'; 7 | import 'feature_file_reader.dart'; 8 | 9 | class IoFeatureFileAccessor implements FeatureFileMatcher, FeatureFileReader { 10 | final Encoding encoding; 11 | final Directory? workingDirectory; 12 | 13 | const IoFeatureFileAccessor({ 14 | this.encoding = utf8, 15 | this.workingDirectory, 16 | }); 17 | 18 | @override 19 | Future read(String path) { 20 | return File(path).readAsString(encoding: encoding); 21 | } 22 | 23 | @override 24 | Future> listFiles(Pattern pattern) { 25 | return _directoryContents( 26 | workingDirectory ?? Directory.current, 27 | pattern, 28 | ); 29 | } 30 | 31 | /// Returns a list of relative paths from [dir] which match [pattern]. 32 | Future> _directoryContents( 33 | Directory directory, 34 | Pattern pattern, 35 | ) async { 36 | final result = []; 37 | 38 | await directory.list(recursive: true).forEach( 39 | (item) { 40 | if (item is File) { 41 | final relativePath = relative( 42 | item.path, 43 | from: directory.path, 44 | ); 45 | 46 | final match = pattern.allMatches(relativePath); 47 | if (match.isNotEmpty) { 48 | result.add(item.path); 49 | } 50 | } 51 | }, 52 | ); 53 | 54 | return result; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/processes/process_handler.dart: -------------------------------------------------------------------------------- 1 | abstract class ProcessHandler { 2 | Future run(); 3 | Future terminate(); 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/reporters/aggregated_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'message_level.dart'; 3 | import 'messages/messages.dart'; 4 | import 'reporter.dart'; 5 | import 'serializable_reporter.dart'; 6 | 7 | class AggregatedReporter extends FullReporter 8 | with AllReporters 9 | implements JsonSerializableReporter { 10 | final List _reporters = []; 11 | 12 | void addReporter(Reporter reporter) => _reporters.add(reporter); 13 | 14 | @override 15 | UnmodifiableListView get reporters => 16 | UnmodifiableListView(_reporters); 17 | 18 | @override 19 | ReportActionHandler get test => ReportActionHandler( 20 | onStarted: ([_]) => invokeReporters( 21 | (r) => r.test.onStarted.invoke(), 22 | ), 23 | onFinished: ([_]) => invokeReporters( 24 | (r) => r.test.onFinished.invoke(), 25 | ), 26 | ); 27 | 28 | @override 29 | ReportActionHandler get feature => ReportActionHandler( 30 | onStarted: ([value]) => invokeReporters( 31 | (r) => r.feature.onStarted.invoke(value), 32 | ), 33 | onFinished: ([message]) => invokeReporters( 34 | (report) => report.feature.onFinished.invoke(message), 35 | ), 36 | ); 37 | 38 | @override 39 | ReportActionHandler get scenario => ReportActionHandler( 40 | onStarted: ([message]) => invokeReporters( 41 | (r) => r.scenario.onStarted.invoke(message), 42 | ), 43 | onFinished: ([message]) => invokeReporters( 44 | (r) => r.scenario.onFinished.invoke(message), 45 | ), 46 | ); 47 | 48 | @override 49 | ReportActionHandler get step => ReportActionHandler( 50 | onStarted: ([message]) => invokeReporters( 51 | (r) => r.step.onStarted.invoke(message), 52 | ), 53 | onFinished: ([message]) => invokeReporters( 54 | (r) => r.step.onFinished.invoke(message), 55 | ), 56 | ); 57 | 58 | @override 59 | String serialize() { 60 | var jsonReports = ''; 61 | if (_reporters.isEmpty) { 62 | return '[]'; 63 | } 64 | 65 | jsonReports = _reporters 66 | .whereType>() 67 | .map((x) => x.serialize()) 68 | .where((x) => x.isNotEmpty) 69 | .join(','); 70 | 71 | return '[$jsonReports]'; 72 | } 73 | 74 | @override 75 | Future message(String message, MessageLevel level) => 76 | invokeReporters((r) => r.message(message, level)); 77 | 78 | @override 79 | Future onException(Object exception, StackTrace stackTrace) => 80 | invokeReporters( 81 | (r) => r.onException(exception, stackTrace), 82 | ); 83 | 84 | @override 85 | Future dispose() async { 86 | await invokeReporters((r) => r.dispose()); 87 | _reporters.clear(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_embedding.dart: -------------------------------------------------------------------------------- 1 | class JsonEmbedding { 2 | late final String mimeType; 3 | late final String data; 4 | 5 | Map toJson() { 6 | return { 7 | 'mime_type': mimeType, 8 | 'data': data, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_feature.dart: -------------------------------------------------------------------------------- 1 | import '../messages/messages.dart'; 2 | import 'json_scenario.dart'; 3 | import 'json_tag.dart'; 4 | 5 | class JsonFeature { 6 | final String uri; 7 | final String name; 8 | final String? description; 9 | final int line; 10 | final String? id; 11 | final Iterable tags; 12 | List scenarios; 13 | 14 | // TODO(shigomany): Maybe immutable? 15 | JsonFeature({ 16 | required this.uri, 17 | required this.name, 18 | required this.line, 19 | this.description, 20 | this.id, 21 | List? scenarios, 22 | Iterable? tags, 23 | }) : scenarios = scenarios ?? [], 24 | tags = tags ?? []; 25 | 26 | /// Convert [StartedMessage] to [JsonFeature] 27 | factory JsonFeature.from(FeatureMessage message) { 28 | final feature = JsonFeature( 29 | uri: message.context.filePath, 30 | id: message.name.toLowerCase(), 31 | name: message.name, 32 | description: message.description, 33 | line: message.context.nonZeroAdjustedLineNumber, 34 | tags: message.tags.map((t) => JsonTag.fromMessageTag(t)), 35 | ); 36 | 37 | return feature; 38 | } 39 | 40 | /// Create [JsonFeature] with empty fields and 41 | /// has one [JsonScenario.empty] in [scenarios] 42 | static JsonFeature get empty => JsonFeature( 43 | name: 'Unnamed feature', 44 | description: 'An unnamed feature is possible ' 45 | 'if something is logged before any feature has started to execute', 46 | scenarios: [JsonScenario.empty], 47 | line: 0, 48 | uri: 'unknown', 49 | ); 50 | 51 | /// Add scenario in [scenarios] and 52 | /// sets the reference [scenario.feature] on this. 53 | void add(JsonScenario scenario) { 54 | scenario.feature = this; 55 | scenarios.add(scenario); 56 | } 57 | 58 | /// Returns the [scenarios.last] if [scenarios.isEmpty] 59 | /// otherwise adds the [JsonScenario.empty] value to the [scenarios] 60 | JsonScenario get currentScenario { 61 | if (scenarios.isEmpty) { 62 | scenarios.add(JsonScenario.empty); 63 | } 64 | 65 | return scenarios.last; 66 | } 67 | 68 | Map toJson() { 69 | final result = { 70 | 'id': id, 71 | 'keyword': 'Feature', 72 | 'line': line, 73 | 'name': name, 74 | 'uri': uri, 75 | }; 76 | 77 | if (description?.isNotEmpty ?? false) { 78 | result['description'] = description; 79 | } 80 | 81 | if (tags.isNotEmpty) { 82 | result['tags'] = tags.toList(); 83 | } 84 | 85 | if (scenarios.isNotEmpty) { 86 | result['elements'] = scenarios.toList(); 87 | } 88 | 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../../../gherkin.dart'; 5 | import 'json_feature.dart'; 6 | import 'json_scenario.dart'; 7 | import 'json_step.dart'; 8 | 9 | class JsonReporter 10 | implements 11 | JsonSerializableReporter, 12 | TestReporter, 13 | FeatureReporter, 14 | ScenarioReporter, 15 | StepReporter, 16 | ExceptionReporter { 17 | final List _features; 18 | final String path; 19 | final WriteReportCallback? writeReport; 20 | 21 | JsonReporter({ 22 | this.path = './report.json', 23 | this.writeReport, 24 | }) : _features = []; 25 | 26 | JsonFeature get _currentFeature { 27 | if (_features.isEmpty) { 28 | _features.add(JsonFeature.empty); 29 | } 30 | 31 | return _features.last; 32 | } 33 | 34 | @override 35 | ReportActionHandler get test => 36 | ReportActionHandler(onFinished: ([message]) => _generateReport(path)); 37 | 38 | @override 39 | ReportActionHandler get feature => ReportActionHandler( 40 | onStarted: ([message]) async => 41 | _features.add(JsonFeature.from(message!)), 42 | ); 43 | 44 | @override 45 | ReportActionHandler get scenario => ReportActionHandler( 46 | onStarted: ([message]) async => 47 | _currentFeature.add(JsonScenario.from(message!)), 48 | ); 49 | 50 | @override 51 | ReportActionHandler get step => ReportActionHandler( 52 | onStarted: ([message]) async => 53 | _currentFeature.currentScenario.add(JsonStep.from(message!)), 54 | onFinished: ([message]) async => 55 | _currentFeature.currentScenario.onStepFinish(message!), 56 | ); 57 | 58 | @override 59 | Future onException(Object exception, StackTrace stackTrace) async { 60 | _currentFeature.currentScenario.currentStep 61 | .onException(exception, stackTrace); 62 | } 63 | 64 | Future onSaveReport(String jsonReport, String path) async { 65 | final file = File(path); 66 | await file.writeAsString(jsonReport); 67 | } 68 | 69 | Future _generateReport(String path) async { 70 | try { 71 | final report = serialize(); 72 | if (writeReport != null) { 73 | await writeReport!(report, path); 74 | } else { 75 | await onSaveReport(report, path); 76 | } 77 | } catch (e) { 78 | print('Failed to generate json report: $e'); 79 | } 80 | } 81 | 82 | @override 83 | String serialize() { 84 | return json.encode(_features); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_row.dart: -------------------------------------------------------------------------------- 1 | class JsonRow { 2 | List cells = []; 3 | 4 | JsonRow(this.cells); 5 | 6 | Map toJson() { 7 | return { 8 | 'cells': cells, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_statuses.dart: -------------------------------------------------------------------------------- 1 | class JsonStatus { 2 | static const passed = 'passed'; 3 | static const failed = 'failed'; 4 | static const skipped = 'skipped'; 5 | static const ambiguous = 'ambiguous'; 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_tag.dart: -------------------------------------------------------------------------------- 1 | import '../messages/messages.dart'; 2 | 3 | class JsonTag { 4 | final String name; 5 | final int line; 6 | 7 | const JsonTag(this.name, this.line); 8 | 9 | JsonTag.fromMessageTag(Tag tag) 10 | : name = tag.name, 11 | line = tag.nonZeroAdjustedLineNumber; 12 | 13 | Map toJson() { 14 | return { 15 | 'line': line, 16 | 'name': name, 17 | }; 18 | } 19 | 20 | JsonTag copyWith({ 21 | String? name, 22 | int? line, 23 | }) { 24 | return JsonTag( 25 | name ?? this.name, 26 | line ?? this.line, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/reporters/message_level.dart: -------------------------------------------------------------------------------- 1 | enum MessageLevel { verbose, debug, info, warning, error } 2 | -------------------------------------------------------------------------------- /lib/src/reporters/messages/feature/feature_message.dart: -------------------------------------------------------------------------------- 1 | part of '../messages.dart'; 2 | 3 | /// {@template messages.featuremessage} 4 | /// A message related to a feature 5 | /// {@endtemplate} 6 | class FeatureMessage extends ActionMessage { 7 | /// Gherkin format tags 8 | final List tags; 9 | final String? description; 10 | 11 | /// {@macro messages.featuremessage} 12 | FeatureMessage({ 13 | required String name, 14 | required RunnableDebugInformation context, 15 | this.tags = const [], 16 | this.description, 17 | }) : super( 18 | target: Target.feature, 19 | name: name, 20 | context: context, 21 | ); 22 | 23 | FeatureMessage copyWith({ 24 | String? name, 25 | RunnableDebugInformation? context, 26 | List? tags, 27 | String? description, 28 | }) { 29 | return FeatureMessage( 30 | name: name ?? this.name, 31 | context: context ?? this.context, 32 | tags: tags ?? this.tags, 33 | description: description ?? this.description, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/reporters/messages/messages.dart: -------------------------------------------------------------------------------- 1 | import '../../gherkin/attachments/attachment.dart'; 2 | import '../../gherkin/models/table.dart'; 3 | import '../../gherkin/runnables/debug_information.dart'; 4 | import '../../gherkin/steps/step_run_result.dart'; 5 | 6 | part 'step/step_message.dart'; 7 | part 'scenario/scenario_message.dart'; 8 | part 'test/test_message.dart'; 9 | part 'feature/feature_message.dart'; 10 | 11 | enum Target { 12 | run, 13 | feature, 14 | scenario, 15 | scenarioOutline, 16 | step, 17 | } 18 | 19 | class Tag { 20 | final String name; 21 | final int lineNumber; 22 | final bool isInherited; 23 | 24 | int get nonZeroAdjustedLineNumber => lineNumber + 1; 25 | 26 | Tag( 27 | this.name, 28 | this.lineNumber, { 29 | this.isInherited = false, 30 | }); 31 | } 32 | 33 | /// {@template messages.actionmessage} 34 | /// A general message related to all kinds of entities in Gherkin tests 35 | /// {@endtemplate} 36 | class ActionMessage { 37 | /// Type of Gherkin entity 38 | final Target target; 39 | 40 | /// The ordinal name of the test, for example `Step 1` 41 | final String name; 42 | 43 | /// Message context in various test states 44 | final RunnableDebugInformation context; 45 | 46 | /// {@macro messages.actionmessage} 47 | const ActionMessage({ 48 | required this.target, 49 | required this.name, 50 | required this.context, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/reporters/messages/scenario/scenario_message.dart: -------------------------------------------------------------------------------- 1 | part of '../messages.dart'; 2 | 3 | /// {@template messages.scenariomessage} 4 | /// A message related to a scenario or scenarioOutline 5 | /// {@endtemplate} 6 | class ScenarioMessage extends ActionMessage { 7 | /// Has the scenario been executed successfully 8 | final bool hasPassed; 9 | 10 | /// Gherkin format tags 11 | final List tags; 12 | 13 | final String? description; 14 | 15 | /// {@macro messages.scenariomessage} 16 | ScenarioMessage({ 17 | required String name, 18 | required RunnableDebugInformation context, 19 | this.description, 20 | this.hasPassed = false, 21 | this.tags = const [], 22 | Target target = Target.scenario, 23 | }) : super( 24 | target: target, 25 | name: name, 26 | context: context, 27 | ); 28 | 29 | ScenarioMessage copyWith({ 30 | String? name, 31 | String? description, 32 | RunnableDebugInformation? context, 33 | bool? hasPassed, 34 | List? tags, 35 | Target? target, 36 | }) { 37 | return ScenarioMessage( 38 | target: target ?? this.target, 39 | name: name ?? this.name, 40 | description: description ?? this.description, 41 | context: context ?? this.context, 42 | hasPassed: hasPassed ?? this.hasPassed, 43 | tags: tags ?? this.tags, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/reporters/messages/step/step_message.dart: -------------------------------------------------------------------------------- 1 | part of '../messages.dart'; 2 | 3 | /// {@template messages.stepmessage} 4 | /// A message related to a step 5 | /// {@endtemplate} 6 | class StepMessage extends ActionMessage { 7 | /// Gherkin format table (optional) 8 | final GherkinTable? table; 9 | 10 | /// Gherkin format multiline string (optional) 11 | final String? multilineString; 12 | 13 | /// Gherkin format tags 14 | final List tags; 15 | 16 | /// Step result during program execution 17 | final StepResult? result; 18 | final List? attachments; 19 | 20 | /// {@macro messages.stepmessage} 21 | StepMessage({ 22 | required String name, 23 | required RunnableDebugInformation context, 24 | this.tags = const [], 25 | this.table, 26 | this.multilineString, 27 | this.result, 28 | this.attachments, 29 | }) : super( 30 | target: Target.step, 31 | name: name, 32 | context: context, 33 | ); 34 | 35 | StepMessage copyWith({ 36 | String? name, 37 | RunnableDebugInformation? context, 38 | GherkinTable? table, 39 | String? multilineString, 40 | List? tags, 41 | StepResult? result, 42 | List? attachments, 43 | }) { 44 | return StepMessage( 45 | name: name ?? this.name, 46 | context: context ?? this.context, 47 | table: table ?? this.table, 48 | multilineString: multilineString ?? this.multilineString, 49 | tags: tags ?? this.tags, 50 | result: result ?? this.result, 51 | attachments: attachments ?? this.attachments, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/reporters/messages/test/test_message.dart: -------------------------------------------------------------------------------- 1 | part of '../messages.dart'; 2 | 3 | class TestMessage extends ActionMessage { 4 | final List tags; 5 | 6 | TestMessage({ 7 | required Target target, 8 | required String name, 9 | required RunnableDebugInformation context, 10 | this.tags = const [], 11 | }) : super( 12 | target: target, 13 | name: name, 14 | context: context, 15 | ); 16 | 17 | TestMessage copyWith({ 18 | Target? target, 19 | String? name, 20 | RunnableDebugInformation? context, 21 | List? tags, 22 | }) { 23 | return TestMessage( 24 | target: target ?? this.target, 25 | name: name ?? this.name, 26 | context: context ?? this.context, 27 | tags: tags ?? this.tags, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/reporters/serializable_reporter.dart: -------------------------------------------------------------------------------- 1 | /// Interface provides [serialize] objects to type [T] 2 | abstract class SerializableReporter { 3 | T serialize(); 4 | } 5 | 6 | abstract class JsonSerializableReporter extends SerializableReporter {} 7 | 8 | typedef WriteReportCallback = Future Function(String report, String path); 9 | -------------------------------------------------------------------------------- /lib/src/reporters/stdout_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'message_level.dart'; 3 | import 'reporter.dart'; 4 | 5 | class StdoutReporter implements InfoReporter { 6 | static const String kNeutralColor = '\u001b[33;34m'; // blue 7 | static const String kDebugColor = '\u001b[1;30m'; // gray 8 | static const String kFailColor = '\u001b[33;31m'; // red 9 | static const String kWarnColor = '\u001b[33;10m'; // yellow 10 | static const String kResetColor = '\u001b[33;0m'; 11 | static const String kPassColor = '\u001b[33;32m'; // green 12 | 13 | final MessageLevel logLevel; 14 | final bool? _supportsAnsiEscapes; 15 | late void Function(String text) _writeln; 16 | late void Function(String text) _write; 17 | 18 | bool get supportsAnsiEscapes { 19 | try { 20 | return _supportsAnsiEscapes != null 21 | ? _supportsAnsiEscapes! 22 | : stdout.supportsAnsiEscapes; 23 | } catch (_, __) { 24 | // stdout.supportsAnsiEscapes throws in the web environment 25 | // see https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/js_dev_runtime/patch/io_patch.dart#L622 26 | return false; 27 | } 28 | } 29 | 30 | StdoutReporter([ 31 | this.logLevel = MessageLevel.verbose, 32 | // ignore: avoid_positional_boolean_parameters 33 | this._supportsAnsiEscapes, 34 | ]) { 35 | _writeln = (text) => stdout.writeln(text); 36 | _write = (text) => stdout.write(text); 37 | } 38 | 39 | void setWriteLineFn(void Function(String text) fn) { 40 | _writeln = fn; 41 | } 42 | 43 | void setWriteFn(void Function(String text) fn) { 44 | _write = fn; 45 | } 46 | 47 | @override 48 | Future message(String message, MessageLevel level) async { 49 | if (level.index >= logLevel.index) { 50 | printMessageLine(message, getColour(level)); 51 | } 52 | } 53 | 54 | @override 55 | Future onException(Object exception, StackTrace stackTrace) async { 56 | printMessageLine(exception.toString(), getColour(MessageLevel.error)); 57 | } 58 | 59 | String getColour(MessageLevel level) { 60 | switch (level) { 61 | case MessageLevel.verbose: 62 | case MessageLevel.debug: 63 | return kDebugColor; 64 | case MessageLevel.error: 65 | return kFailColor; 66 | case MessageLevel.warning: 67 | return kWarnColor; 68 | case MessageLevel.info: 69 | default: 70 | return kNeutralColor; 71 | } 72 | } 73 | 74 | void printMessageLine( 75 | String message, [ 76 | String? colour, 77 | ]) { 78 | if (supportsAnsiEscapes) { 79 | _writeln('${colour ?? kResetColor}$message$kResetColor'); 80 | } else { 81 | _writeln(message); 82 | } 83 | } 84 | 85 | void printMessage( 86 | String message, [ 87 | String? colour, 88 | ]) { 89 | if (supportsAnsiEscapes) { 90 | _write('${colour ?? kResetColor}$message$kResetColor'); 91 | } else { 92 | _writeln(message); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/utils/perf.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | abstract class Perf { 4 | static Future measure( 5 | Future Function() action, 6 | void Function(int elapsedMilliseconds) logFn, 7 | ) async { 8 | final timer = Stopwatch(); 9 | timer.start(); 10 | try { 11 | return await action(); 12 | } finally { 13 | timer.stop(); 14 | logFn(timer.elapsedMilliseconds); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pre-publish-checks.cmd: -------------------------------------------------------------------------------- 1 | CALL dart analyze 2 | CALL dart format . --fix 3 | CALL dart run test . 4 | CALL cd example && dart test.dart 5 | cd ../ 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gherkin 2 | version: 3.1.0 3 | description: A Gherkin parsers and runner for Dart which is very similar to Cucumber, it provides the base BDD functionality ready for use in platform specific implementations i.e. flutter/web 4 | homepage: https://github.com/jonsamwell/dart_gherkin 5 | 6 | dependencies: 7 | path: ^1.8.0 8 | collection: ^1.15.0 9 | uuid: ^3.0.6 10 | matcher: ^0.12.11 11 | 12 | dev_dependencies: 13 | lint: ^1.8.2 14 | test: ">=1.16.8" 15 | glob: ^2.0.1 16 | 17 | environment: 18 | sdk: ">=2.15.0 <3.0.0" 19 | -------------------------------------------------------------------------------- /test/gherkin/attachments/attachment_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('attach', () { 6 | test('saves attachment', () async { 7 | final manager = AttachmentManager(); 8 | const data = 'data'; 9 | const mimeType = 'mimeType'; 10 | 11 | manager.attach(data, mimeType); 12 | 13 | expect(manager.getAttachmentsForContext().length, equals(1)); 14 | expect(manager.getAttachmentsForContext().first.data, equals(data)); 15 | expect( 16 | manager.getAttachmentsForContext().first.mimeType, 17 | equals(mimeType), 18 | ); 19 | expect(manager.getAttachmentsForContext('context').length, equals(0)); 20 | }); 21 | 22 | test('saves attachment with context', () async { 23 | final manager = AttachmentManager(); 24 | const data = 'data'; 25 | const mimeType = 'mimeType'; 26 | const context = 'context'; 27 | 28 | manager.attach(data, mimeType, context); 29 | 30 | expect(manager.getAttachmentsForContext(context).length, equals(1)); 31 | expect( 32 | manager.getAttachmentsForContext(context).first.data, 33 | equals(data), 34 | ); 35 | expect( 36 | manager.getAttachmentsForContext(context).first.mimeType, 37 | equals(mimeType), 38 | ); 39 | expect( 40 | manager.getAttachmentsForContext(context).first.context, 41 | equals(context), 42 | ); 43 | expect(manager.getAttachmentsForContext().length, equals(0)); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/gherkin/expressions/tag_expression_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/expressions/tag_expression.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('TagExpression', () { 6 | test('evaluate simple single tag expression correctly', () async { 7 | final evaluator = TagExpressionEvaluator(); 8 | final tags = ['@a', '@b', '@c']; 9 | 10 | expect(evaluator.evaluate('@a', tags), true); 11 | expect(evaluator.evaluate('@b', tags), true); 12 | expect(evaluator.evaluate('@d', tags), false); 13 | }); 14 | 15 | test('evaluate complex and tag expression correctly', () async { 16 | final evaluator = TagExpressionEvaluator(); 17 | final tags = ['@a', '@b', '@c']; 18 | 19 | expect(evaluator.evaluate('@a and @d', tags), false); 20 | expect(evaluator.evaluate('(@a and not @d)', tags), true); 21 | expect(evaluator.evaluate('(@a and not @c)', tags), false); 22 | }); 23 | 24 | test('evaluate complex or tag expression correctly', () async { 25 | final evaluator = TagExpressionEvaluator(); 26 | final tags = ['@a', '@b', '@c']; 27 | 28 | expect(evaluator.evaluate('(@a or @b)', tags), true); 29 | expect(evaluator.evaluate('not @a or not @d', tags), true); 30 | expect(evaluator.evaluate('not @d or not @e', tags), true); 31 | }); 32 | 33 | test('evaluate complex bracket tag expression correctly', () async { 34 | final evaluator = TagExpressionEvaluator(); 35 | final tags = ['@a', '@b', '@c']; 36 | 37 | expect(evaluator.evaluate('@a or (@b and @c)', tags), true); 38 | expect(evaluator.evaluate('@a and (@d or @e)', tags), false); 39 | expect( 40 | evaluator.evaluate('@a and ((@b or not @e) or (@b and @c))', tags), 41 | true, 42 | ); 43 | expect( 44 | evaluator.evaluate( 45 | '@a and ((@b and not @e) and (@b and @c))', 46 | ['a', 'b', 'c', 'e'], 47 | ), 48 | false, 49 | ); 50 | expect( 51 | evaluator.evaluate('@a and ((@b or not @e) and (@b and @c))', tags), 52 | true, 53 | ); 54 | }); 55 | 56 | test('evaluate single negated tag expression correctly', () async { 57 | final evaluator = TagExpressionEvaluator(); 58 | final tags = ['@skip']; 59 | final tags1 = ['@ignore']; 60 | 61 | expect(evaluator.evaluate('not @skip', tags), false); 62 | expect(evaluator.evaluate('not @skip', tags1), true); 63 | }); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/gherkin/langauges/language_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/language_service.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('service', () { 6 | test('can parse language json file', () async { 7 | final service = LanguageService(); 8 | service.initialise(); 9 | }); 10 | 11 | test('can find default dialect', () async { 12 | final service = LanguageService(); 13 | service.initialise(); 14 | final dialect = service.getDialect(); 15 | expect(dialect, isNotNull); 16 | }); 17 | 18 | test('can find af dialect', () async { 19 | final service = LanguageService(); 20 | service.initialise(); 21 | final dialect = service.getDialect('af'); 22 | expect(dialect, isNotNull); 23 | }); 24 | 25 | test('can find en-au dialect', () async { 26 | final service = LanguageService(); 27 | service.initialise(); 28 | final dialect = service.getDialect('en-au'); 29 | expect(dialect, isNotNull); 30 | }); 31 | 32 | test('parses unicode correctly', () async { 33 | final service = LanguageService(); 34 | service.initialise(); 35 | final dialect = service.getDialect('zh-TW'); 36 | expect(dialect, isNotNull); 37 | expect(dialect.nativeName, '繁體中文'); 38 | }); 39 | 40 | test('parses french correctly', () async { 41 | final service = LanguageService(); 42 | service.initialise(); 43 | final dialect = service.getDialect('fr'); 44 | expect(dialect, isNotNull); 45 | expect(dialect.when.contains("Lorsqu'"), true); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/gherkin/parameters/float_parameter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/parameters/float_parameter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('FloatParameter', () { 6 | test('{float} parsed correctly', () { 7 | final parameter = FloatParameterLower(); 8 | expect(parameter.transformer('12.243'), equals(12.243)); 9 | }); 10 | 11 | test('{Float} parsed correctly', () { 12 | final parameter = FloatParameterCamel(); 13 | expect(parameter.transformer('12.243'), equals(12.243)); 14 | }); 15 | 16 | test('{num} parsed correctly', () { 17 | final parameter = NumParameterLower(); 18 | expect(parameter.transformer('12.243'), equals(12.243)); 19 | expect(parameter.transformer('3'), equals(3)); 20 | expect(parameter.transformer('-1.321'), equals(-1.321)); 21 | }); 22 | 23 | test('{Num} parsed correctly', () { 24 | final parameter = NumParameterCamel(); 25 | expect(parameter.transformer('12.243'), equals(12.243)); 26 | expect(parameter.transformer('3'), equals(3)); 27 | }); 28 | 29 | test('{Num} pattern matches correctly', () { 30 | final parameter = NumParameterCamel(); 31 | expect(parameter.pattern.hasMatch('12'), true); 32 | expect(parameter.pattern.hasMatch('-12'), true); 33 | expect(parameter.pattern.hasMatch('12.0'), true); 34 | expect(parameter.pattern.hasMatch('-12.0'), true); 35 | expect(parameter.pattern.hasMatch('12.00'), true); 36 | expect(parameter.pattern.hasMatch('12.000'), true); 37 | expect(parameter.pattern.hasMatch('12.000000'), true); 38 | expect(parameter.pattern.hasMatch('-12.000000'), true); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/gherkin/parameters/int_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/parameters/int_parameter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('IntParameter', () { 6 | test('{int} parsed correctly', () { 7 | final parameter = IntParameterLower(); 8 | expect(parameter.transformer('12'), equals(12)); 9 | }); 10 | 11 | test('{Int} parsed correctly', () { 12 | final parameter = IntParameterCamel(); 13 | expect(parameter.transformer('12'), equals(12)); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/gherkin/parameters/string_parameter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/parameters/string_parameter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('StringParameter', () { 6 | test('{string} parsed correctly', () { 7 | final parameter = StringParameterLower(); 8 | expect(parameter.transformer('Jon Samwell'), equals('Jon Samwell')); 9 | }); 10 | 11 | test('{String} parsed correctly', () { 12 | final parameter = StringParameterCamel(); 13 | expect(parameter.transformer('Jon Samwell'), equals('Jon Samwell')); 14 | }); 15 | 16 | test('{String} pattern matches correctly', () { 17 | final parameter = StringParameterCamel(); 18 | expect(parameter.pattern.hasMatch("'Jon'"), equals(true)); 19 | }); 20 | 21 | test('{String} pattern matches correctly with multiple words', () { 22 | final parameter = StringParameterCamel(); 23 | expect( 24 | parameter.pattern.hasMatch("'Jon Samwell is a devloper'"), 25 | equals(true), 26 | ); 27 | }); 28 | 29 | test('{String} pattern matches correctly with new line within string', () { 30 | final parameter = StringParameterCamel(); 31 | expect( 32 | parameter.pattern.hasMatch("'Jon Samwell is a \n devloper'"), 33 | equals(true), 34 | ); 35 | }); 36 | 37 | test('{String} pattern matches correctly with non alpha characters', () { 38 | final parameter = StringParameterCamel(); 39 | expect( 40 | parameter.pattern.hasMatch( 41 | "'Jon Samwell is a devloper 123 '!@%^&*()_=+#:';{}'", 42 | ), 43 | equals(true), 44 | ); 45 | }); 46 | 47 | test('{String} parsed correctly with newline character in it', () { 48 | final parameter = StringParameterCamel(); 49 | expect(parameter.pattern.hasMatch("'Jon \n Sam well'"), equals(true)); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/gherkin/parameters/word_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/parameters/string_parameter.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('WordParameter', () { 6 | test('{word} parsed correctly', () { 7 | final parameter = StringParameterLower(); 8 | expect(parameter.transformer('Jon'), equals('Jon')); 9 | }); 10 | 11 | test('{Word} parsed correctly', () { 12 | final parameter = StringParameterCamel(); 13 | expect(parameter.transformer('Jon'), equals('Jon')); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/gherkin/runnables/examples_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/exceptions/syntax_error.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/example.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/table.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | final debugInfo = RunnableDebugInformation.empty(); 9 | 10 | group('addChild', () { 11 | test('can add TableRunnable', () { 12 | final runnable = ExampleRunnable('', debugInfo); 13 | runnable.addChild(TableRunnable(debugInfo)); 14 | }); 15 | 16 | test('can only add single TableRunnable', () { 17 | final runnable = ExampleRunnable('Example one', debugInfo); 18 | runnable.addChild(TableRunnable(debugInfo)); 19 | expect(runnable.table, isNotNull); 20 | 21 | expect( 22 | () => runnable.addChild(TableRunnable(debugInfo)), 23 | throwsA( 24 | (e) => 25 | e is GherkinSyntaxException && 26 | e.message == 27 | "Only a single table can be added to the example 'Example one'", 28 | ), 29 | ); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/gherkin/runnables/feature_file_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/comment_line.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/feature.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/feature_file.dart'; 5 | import 'package:gherkin/src/gherkin/runnables/language.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | final debugInfo = RunnableDebugInformation.empty(); 10 | group('addChild', () { 11 | test('can add LanguageRunnable', () { 12 | final runnable = FeatureFile(debugInfo); 13 | runnable.addChild(LanguageRunnable(debugInfo)..language = 'en'); 14 | expect(runnable.language, 'en'); 15 | }); 16 | test('can add TagsRunnable', () { 17 | final runnable = FeatureFile(debugInfo); 18 | runnable.addChild(FeatureRunnable('1', debugInfo)); 19 | runnable.addChild(FeatureRunnable('2', debugInfo)); 20 | runnable.addChild(FeatureRunnable('3', debugInfo)); 21 | expect(runnable.features.length, 3); 22 | }); 23 | test('can add CommentLineRunnable', () { 24 | final runnable = FeatureFile(debugInfo); 25 | runnable.addChild(CommentLineRunnable('1', debugInfo)); 26 | expect(runnable.features.length, 0); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/gherkin/runnables/feature_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/background.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/comment_line.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/empty_line.dart'; 5 | import 'package:gherkin/src/gherkin/runnables/feature.dart'; 6 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 7 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 8 | import 'package:gherkin/src/gherkin/runnables/text_line.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | Iterable tagsToList(Iterable tags) sync* { 12 | for (final tag in tags.expand((element) => element.tags)) { 13 | yield tag; 14 | } 15 | } 16 | 17 | void main() { 18 | final debugInfo = RunnableDebugInformation.empty(); 19 | group('addChild', () { 20 | test('can add TextLineRunnable', () { 21 | final runnable = FeatureRunnable('', debugInfo); 22 | runnable.addChild(TextLineRunnable(debugInfo)..text = 'text'); 23 | runnable.addChild(TextLineRunnable(debugInfo)..text = 'text line two'); 24 | expect(runnable.description, 'text\ntext line two'); 25 | }); 26 | test('can add TagsRunnable which are given to taggable the taggable child', 27 | () { 28 | final runnable = FeatureRunnable('', debugInfo); 29 | runnable.addChild(TagsRunnable(debugInfo)..tags = ['one', 'two']); 30 | runnable.addChild(TagsRunnable(debugInfo)..tags = ['three']); 31 | final scenario = ScenarioRunnable('', null, debugInfo); 32 | runnable.addChild(scenario); 33 | expect(tagsToList(scenario.tags), ['one', 'two', 'three']); 34 | }); 35 | test('can add EmptyLineRunnable', () { 36 | final runnable = FeatureRunnable('', debugInfo); 37 | runnable.addChild(EmptyLineRunnable(debugInfo)); 38 | }); 39 | test('can add CommentLineRunnable', () { 40 | final runnable = FeatureRunnable('', debugInfo); 41 | runnable.addChild(CommentLineRunnable('', debugInfo)); 42 | }); 43 | test('can add ScenarioRunnable', () { 44 | final runnable = FeatureRunnable('', debugInfo); 45 | runnable.addChild(ScenarioRunnable('1', null, debugInfo)); 46 | runnable.addChild(ScenarioRunnable('2', null, debugInfo)); 47 | runnable.addChild(ScenarioRunnable('3', null, debugInfo)); 48 | expect(runnable.scenarios.length, 3); 49 | expect(runnable.scenarios.elementAt(0).name, '1'); 50 | expect(runnable.scenarios.elementAt(1).name, '2'); 51 | expect(runnable.scenarios.elementAt(2).name, '3'); 52 | }); 53 | test('can add BackgroundRunnable', () { 54 | final runnable = FeatureRunnable('', debugInfo); 55 | runnable.addChild(BackgroundRunnable('1', debugInfo)); 56 | expect(runnable.background, isNotNull); 57 | expect(runnable.background!.name, '1'); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/gherkin/runnables/multi_line_string_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/empty_line.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/multi_line_string.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/text_line.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | final debugInfo = RunnableDebugInformation.empty(); 9 | group('addChild', () { 10 | test('can add EmptyLineRunnable', () { 11 | final runnable = MultilineStringRunnable(debugInfo); 12 | runnable.addChild(EmptyLineRunnable(debugInfo)); 13 | }); 14 | test('can add TextLineRunnable', () { 15 | final runnable = MultilineStringRunnable(debugInfo); 16 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = '1'); 17 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = '2'); 18 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = '3'); 19 | expect(runnable.lines.length, 3); 20 | expect(runnable.lines, ['1', '2', '3']); 21 | }); 22 | }); 23 | 24 | group('stripLeadingIndentation', () { 25 | test('preserve original indentation from the first line', () { 26 | final runnable = MultilineStringRunnable(debugInfo); 27 | 28 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 1'); 29 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 2'); 30 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 3'); 31 | expect(runnable.lines.length, 3); 32 | expect(runnable.lines, ['1', ' 2', ' 3']); 33 | }); 34 | 35 | test('preserve original indentation from a specified position', () { 36 | final runnable = MultilineStringRunnable(debugInfo, leadingWhitespace: 4); 37 | 38 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 1'); 39 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 2'); 40 | runnable.addChild(TextLineRunnable(debugInfo)..originalText = ' 3'); 41 | expect(runnable.lines.length, 3); 42 | expect(runnable.lines, [' 1', ' 2', ' 3']); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/gherkin/runnables/scenario_outline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/empty_line.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/example.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/scenario_outline.dart'; 5 | import 'package:gherkin/src/gherkin/runnables/step.dart'; 6 | import 'package:gherkin/src/gherkin/runnables/table.dart'; 7 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | final debugInfo = RunnableDebugInformation.empty(); 12 | group('addChild', () { 13 | test('can add EmptyLineRunnable', () { 14 | final runnable = ScenarioOutlineRunnable('', null, debugInfo); 15 | runnable.addChild(EmptyLineRunnable(debugInfo)); 16 | }); 17 | 18 | test('can add StepRunnable', () { 19 | final runnable = ScenarioOutlineRunnable('', null, debugInfo); 20 | runnable.addChild(StepRunnable('1', debugInfo)); 21 | runnable.addChild(StepRunnable('2', debugInfo)); 22 | runnable.addChild(StepRunnable('3', debugInfo)); 23 | expect(runnable.steps.length, 3); 24 | expect(runnable.steps.elementAt(0).name, '1'); 25 | expect(runnable.steps.elementAt(1).name, '2'); 26 | expect(runnable.steps.elementAt(2).name, '3'); 27 | }); 28 | 29 | test('can add TagsRunnable which are give to the example', () { 30 | final runnable = ScenarioOutlineRunnable('', null, debugInfo); 31 | final example = ExampleRunnable('', debugInfo); 32 | runnable.addChild(example); 33 | runnable.addTag(TagsRunnable(debugInfo)..tags = ['one']); 34 | expect(example.tags.first.tags.first, 'one'); 35 | expect(example.tags.first.isInherited, true); 36 | }); 37 | 38 | test('can add ExamplesRunnable', () { 39 | final runnable = ScenarioOutlineRunnable('', null, debugInfo); 40 | runnable.addChild(ExampleRunnable('', debugInfo)); 41 | expect(runnable.examples, isNotNull); 42 | }); 43 | 44 | test('can add multiple ExamplesRunnable', () { 45 | final runnable = ScenarioOutlineRunnable('outline one', null, debugInfo); 46 | runnable.addChild(ExampleRunnable('1', debugInfo)); 47 | runnable.addChild(ExampleRunnable('1', debugInfo)); 48 | expect(runnable.examples, isNotNull); 49 | expect(runnable.examples.length, 2); 50 | }); 51 | 52 | test('can interpolate variables in the scenario name', () { 53 | final runnable = ScenarioOutlineRunnable( 54 | 'Scenario outline with parameters: , ', 55 | null, 56 | debugInfo, 57 | ); 58 | final example = ExampleRunnable('', debugInfo); 59 | final exampleTable = TableRunnable(debugInfo); 60 | 61 | exampleTable.rows 62 | ..add('| i | j | k |') 63 | ..add('| 1 | 2 | 3 |') 64 | ..add('| text | 4.5 | false |'); 65 | example.addChild(exampleTable); 66 | runnable.addChild(example); 67 | 68 | final expandedScenarios = runnable.expandOutlinesIntoScenarios(); 69 | final expandedScenario1 = expandedScenarios.elementAt(0); 70 | final expandedScenario2 = expandedScenarios.elementAt(1); 71 | 72 | expect( 73 | expandedScenario1.name, 74 | equals('Scenario outline with parameters: 1, 3 Examples: (1)'), 75 | ); 76 | expect( 77 | expandedScenario2.name, 78 | equals( 79 | 'Scenario outline with parameters: text, false Examples: (2)', 80 | ), 81 | ); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/gherkin/runnables/scenario_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/empty_line.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/step.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | final debugInfo = RunnableDebugInformation.empty(); 9 | group('addChild', () { 10 | test('can add EmptyLineRunnable', () { 11 | final runnable = ScenarioRunnable('', null, debugInfo); 12 | runnable.addChild(EmptyLineRunnable(debugInfo)); 13 | }); 14 | test('can add StepRunnable', () { 15 | final runnable = ScenarioRunnable('', null, debugInfo); 16 | runnable.addChild(StepRunnable('1', debugInfo)); 17 | runnable.addChild(StepRunnable('2', debugInfo)); 18 | runnable.addChild(StepRunnable('3', debugInfo)); 19 | expect(runnable.steps.length, 3); 20 | expect(runnable.steps.elementAt(0).name, '1'); 21 | expect(runnable.steps.elementAt(1).name, '2'); 22 | expect(runnable.steps.elementAt(2).name, '3'); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/gherkin/runnables/step_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/exceptions/syntax_error.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/multi_line_string.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/step.dart'; 5 | import 'package:gherkin/src/gherkin/runnables/table.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | final debugInfo = RunnableDebugInformation.empty(); 10 | group('addChild', () { 11 | test('can add MultilineStringRunnable', () { 12 | final runnable = StepRunnable('', debugInfo); 13 | runnable.addChild( 14 | MultilineStringRunnable(debugInfo)..lines = ['1', '2', '3'].toList(), 15 | ); 16 | runnable.addChild( 17 | MultilineStringRunnable(debugInfo)..lines = ['3', '4', '5'].toList(), 18 | ); 19 | expect(runnable.multilineStrings.length, 2); 20 | expect(runnable.multilineStrings.elementAt(0), '1\n2\n3'); 21 | expect(runnable.multilineStrings.elementAt(1), '3\n4\n5'); 22 | }); 23 | 24 | test('can add TableRunnable', () { 25 | final runnable = StepRunnable('', debugInfo); 26 | runnable.addChild( 27 | TableRunnable(debugInfo) 28 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 29 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 30 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|')), 31 | ); 32 | 33 | expect(runnable.table, isNotNull); 34 | expect(runnable.table!.header, isNotNull); 35 | expect(runnable.table!.header!.columns.length, 2); 36 | expect(runnable.table!.rows.length, 2); 37 | }); 38 | 39 | test('can only add single TableRunnable', () { 40 | final runnable = StepRunnable('Step A', debugInfo); 41 | runnable.addChild( 42 | TableRunnable(debugInfo) 43 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 44 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 45 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|')), 46 | ); 47 | 48 | expect( 49 | () => runnable.addChild( 50 | TableRunnable(debugInfo) 51 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 52 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 53 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|')), 54 | ), 55 | throwsA( 56 | (e) => 57 | e is GherkinSyntaxException && 58 | e.message == 59 | "Only a single table can be added to the step 'Step A'", 60 | ), 61 | ); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/gherkin/syntax/background_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/background.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/background_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = BackgroundSyntax(); 12 | expect(syntax.isMatch('Background: ', EnDialectMock()), true); 13 | expect(syntax.isMatch('Background: ', EnDialectMock()), true); 14 | expect(syntax.isMatch('Background: something', EnDialectMock()), true); 15 | expect(syntax.isMatch(' Background: something', EnDialectMock()), true); 16 | }); 17 | 18 | test('does not match', () { 19 | final syntax = BackgroundSyntax(); 20 | expect(syntax.isMatch('Background', EnDialectMock()), false); 21 | expect(syntax.isMatch('Background something', EnDialectMock()), false); 22 | expect(syntax.isMatch('#Background: something', EnDialectMock()), false); 23 | }); 24 | }); 25 | 26 | group('toRunnable', () { 27 | test('creates BackgroundRunnable', () { 28 | final syntax = BackgroundSyntax(); 29 | final runnable = syntax.toRunnable( 30 | 'Background: A background 123', 31 | RunnableDebugInformation.empty(), 32 | EnDialectMock(), 33 | ); 34 | expect(runnable, isNotNull); 35 | expect(runnable, predicate((x) => x is BackgroundRunnable)); 36 | expect(runnable.name, equals('A background 123')); 37 | }); 38 | 39 | test('creates BackgroundRunnable with empty name', () { 40 | final syntax = BackgroundSyntax(); 41 | final runnable = syntax.toRunnable( 42 | 'Background: ', 43 | RunnableDebugInformation.empty(), 44 | EnDialectMock(), 45 | ); 46 | expect(runnable, isNotNull); 47 | expect(runnable, predicate((x) => x is BackgroundRunnable)); 48 | expect(runnable.name, equals('')); 49 | }); 50 | 51 | test('creates BackgroundRunnable with no name', () { 52 | final syntax = BackgroundSyntax(); 53 | final runnable = syntax.toRunnable( 54 | 'Background:', 55 | RunnableDebugInformation.empty(), 56 | EnDialectMock(), 57 | ); 58 | expect(runnable, isNotNull); 59 | expect(runnable, predicate((x) => x is BackgroundRunnable)); 60 | expect(runnable.name, equals('')); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/gherkin/syntax/comment_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/syntax/comment_syntax.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../../mocks/en_dialect_mock.dart'; 5 | 6 | void main() { 7 | group('isMatch', () { 8 | test('matches correctly', () { 9 | final keyword = CommentSyntax(); 10 | expect( 11 | keyword.isMatch( 12 | '# I am a comment', 13 | EnDialectMock(), 14 | ), 15 | true, 16 | ); 17 | expect( 18 | keyword.isMatch( 19 | '#I am also a comment', 20 | EnDialectMock(), 21 | ), 22 | true, 23 | ); 24 | expect( 25 | keyword.isMatch( 26 | '## I am also a comment', 27 | EnDialectMock(), 28 | ), 29 | true, 30 | ); 31 | expect( 32 | keyword.isMatch( 33 | '# Language something', 34 | EnDialectMock(), 35 | ), 36 | true, 37 | ); 38 | }); 39 | 40 | test('does not match', () { 41 | final keyword = CommentSyntax(); 42 | // expect(keyword.isMatch('# language: en'), false); 43 | expect( 44 | keyword.isMatch( 45 | 'I am not a comment', 46 | EnDialectMock(), 47 | ), 48 | false, 49 | ); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/gherkin/syntax/empty_line_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/syntax/empty_line_syntax.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../../mocks/en_dialect_mock.dart'; 5 | 6 | void main() { 7 | group('isMatch', () { 8 | test('matches correctly', () { 9 | final keyword = EmptyLineSyntax(); 10 | expect( 11 | keyword.isMatch( 12 | '', 13 | EnDialectMock(), 14 | ), 15 | true, 16 | ); 17 | expect( 18 | keyword.isMatch( 19 | ' ', 20 | EnDialectMock(), 21 | ), 22 | true, 23 | ); 24 | expect( 25 | keyword.isMatch( 26 | ' ', 27 | EnDialectMock(), 28 | ), 29 | true, 30 | ); 31 | expect( 32 | keyword.isMatch( 33 | ' ', 34 | EnDialectMock(), 35 | ), 36 | true, 37 | ); 38 | }); 39 | 40 | test('does not match', () { 41 | final keyword = EmptyLineSyntax(); 42 | expect( 43 | keyword.isMatch( 44 | 'a', 45 | EnDialectMock(), 46 | ), 47 | false, 48 | ); 49 | expect( 50 | keyword.isMatch( 51 | ' b', 52 | EnDialectMock(), 53 | ), 54 | false, 55 | ); 56 | expect( 57 | keyword.isMatch( 58 | ' c', 59 | EnDialectMock(), 60 | ), 61 | false, 62 | ); 63 | expect( 64 | keyword.isMatch( 65 | ' ,', 66 | EnDialectMock(), 67 | ), 68 | false, 69 | ); 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /test/gherkin/syntax/example_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/example.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/example_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = ExampleSyntax(); 12 | expect( 13 | syntax.isMatch( 14 | 'Examples:', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | syntax.isMatch( 21 | 'Examples: ', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | expect( 27 | syntax.isMatch( 28 | 'Examples: something', 29 | EnDialectMock(), 30 | ), 31 | true, 32 | ); 33 | expect( 34 | syntax.isMatch( 35 | ' Examples: something', 36 | EnDialectMock(), 37 | ), 38 | true, 39 | ); 40 | }); 41 | 42 | test('does not match', () { 43 | final syntax = ExampleSyntax(); 44 | expect( 45 | syntax.isMatch( 46 | 'Examples', 47 | EnDialectMock(), 48 | ), 49 | false, 50 | ); 51 | expect( 52 | syntax.isMatch( 53 | 'Example something', 54 | EnDialectMock(), 55 | ), 56 | false, 57 | ); 58 | expect( 59 | syntax.isMatch( 60 | '#Examples: something', 61 | EnDialectMock(), 62 | ), 63 | false, 64 | ); 65 | }); 66 | }); 67 | 68 | group('toRunnable', () { 69 | test('creates Runnable', () { 70 | final syntax = ExampleSyntax(); 71 | final runnable = syntax.toRunnable( 72 | 'Examples: An example 123', 73 | RunnableDebugInformation.empty(), 74 | EnDialectMock(), 75 | ); 76 | expect(runnable, isNotNull); 77 | expect(runnable, predicate((x) => x is ExampleRunnable)); 78 | expect(runnable.name, equals('An example 123')); 79 | }); 80 | 81 | test('creates Runnable with empty name', () { 82 | final syntax = ExampleSyntax(); 83 | final runnable = syntax.toRunnable( 84 | 'Examples: ', 85 | RunnableDebugInformation.empty(), 86 | EnDialectMock(), 87 | ); 88 | expect(runnable, isNotNull); 89 | expect(runnable, predicate((x) => x is ExampleRunnable)); 90 | expect(runnable.name, equals('')); 91 | }); 92 | 93 | test('creates Runnable with no name', () { 94 | final syntax = ExampleSyntax(); 95 | final runnable = syntax.toRunnable( 96 | 'Examples:', 97 | RunnableDebugInformation.empty(), 98 | EnDialectMock(), 99 | ); 100 | expect(runnable, isNotNull); 101 | expect(runnable, predicate((x) => x is ExampleRunnable)); 102 | expect(runnable.name, equals('')); 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /test/gherkin/syntax/feature_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/feature.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/feature_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final keyword = FeatureSyntax(); 12 | expect( 13 | keyword.isMatch( 14 | 'Feature: one', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | keyword.isMatch( 21 | 'Feature:one', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | }); 27 | 28 | test('does not match', () { 29 | final keyword = FeatureSyntax(); 30 | expect( 31 | keyword.isMatch( 32 | '#Feature: no', 33 | EnDialectMock(), 34 | ), 35 | false, 36 | ); 37 | expect( 38 | keyword.isMatch( 39 | '# Feature no', 40 | EnDialectMock(), 41 | ), 42 | false, 43 | ); 44 | }); 45 | }); 46 | 47 | group('toRunnable', () { 48 | test('creates FeatureRunnable', () { 49 | final keyword = FeatureSyntax(); 50 | final runnable = keyword.toRunnable( 51 | 'Feature: A feature 123', 52 | RunnableDebugInformation.empty(), 53 | EnDialectMock(), 54 | ); 55 | expect(runnable, isNotNull); 56 | expect(runnable, predicate((x) => x is FeatureRunnable)); 57 | expect(runnable.name, equals('A feature 123')); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/gherkin/syntax/language_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/language.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/language_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final keyword = LanguageSyntax(); 12 | expect( 13 | keyword.isMatch( 14 | '# language: en', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | keyword.isMatch( 21 | '#language: fr', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | expect( 27 | keyword.isMatch( 28 | '#language:de', 29 | EnDialectMock(), 30 | ), 31 | true, 32 | ); 33 | expect( 34 | keyword.isMatch( 35 | '#language:en-au', 36 | EnDialectMock(), 37 | ), 38 | true, 39 | ); 40 | expect( 41 | keyword.isMatch( 42 | '#language:en-Scouse', 43 | EnDialectMock(), 44 | ), 45 | true, 46 | ); 47 | }); 48 | 49 | test('does not match', () { 50 | final keyword = LanguageSyntax(); 51 | expect( 52 | keyword.isMatch( 53 | '#language no', 54 | EnDialectMock(), 55 | ), 56 | false, 57 | ); 58 | expect( 59 | keyword.isMatch( 60 | '# language comment', 61 | EnDialectMock(), 62 | ), 63 | false, 64 | ); 65 | }); 66 | }); 67 | 68 | group('toRunnable', () { 69 | test('creates LanguageRunnable', () { 70 | final keyword = LanguageSyntax(); 71 | final runnable = keyword.toRunnable( 72 | '# language: de', 73 | RunnableDebugInformation.empty(), 74 | EnDialectMock(), 75 | ); 76 | expect(runnable, isNotNull); 77 | expect(runnable, predicate((x) => x is LanguageRunnable)); 78 | expect(runnable.language, equals('de')); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/gherkin/syntax/multiline_string_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/multi_line_string.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/comment_syntax.dart'; 4 | import 'package:gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; 5 | import 'package:gherkin/src/gherkin/syntax/text_line_syntax.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../../mocks/en_dialect_mock.dart'; 9 | 10 | void main() { 11 | group('isMatch', () { 12 | test('matches correctly', () { 13 | final syntax = MultilineStringSyntax(); 14 | expect( 15 | syntax.isMatch( 16 | '"""', 17 | EnDialectMock(), 18 | ), 19 | true, 20 | ); 21 | expect( 22 | syntax.isMatch( 23 | '```', 24 | EnDialectMock(), 25 | ), 26 | true, 27 | ); 28 | expect( 29 | syntax.isMatch( 30 | "'''", 31 | EnDialectMock(), 32 | ), 33 | true, 34 | ); 35 | }); 36 | 37 | test('does not match', () { 38 | final syntax = MultilineStringSyntax(); 39 | expect( 40 | syntax.isMatch( 41 | '#"""', 42 | EnDialectMock(), 43 | ), 44 | false, 45 | ); 46 | expect( 47 | syntax.isMatch( 48 | '#```', 49 | EnDialectMock(), 50 | ), 51 | false, 52 | ); 53 | expect( 54 | syntax.isMatch( 55 | "#'''", 56 | EnDialectMock(), 57 | ), 58 | false, 59 | ); 60 | expect( 61 | syntax.isMatch( 62 | '"', 63 | EnDialectMock(), 64 | ), 65 | false, 66 | ); 67 | expect( 68 | syntax.isMatch( 69 | '`', 70 | EnDialectMock(), 71 | ), 72 | false, 73 | ); 74 | expect( 75 | syntax.isMatch( 76 | "'", 77 | EnDialectMock(), 78 | ), 79 | false, 80 | ); 81 | }); 82 | }); 83 | 84 | group('block', () { 85 | test('is block', () { 86 | final syntax = MultilineStringSyntax(); 87 | expect(syntax.isBlockSyntax, true); 88 | }); 89 | 90 | test('continue block if text line string', () { 91 | final syntax = MultilineStringSyntax(); 92 | expect(syntax.hasBlockEnded(TextLineSyntax()), false); 93 | }); 94 | 95 | test('continue block if comment string', () { 96 | final syntax = MultilineStringSyntax(); 97 | expect(syntax.hasBlockEnded(CommentSyntax()), false); 98 | }); 99 | 100 | test('end block if multiline string', () { 101 | final syntax = MultilineStringSyntax(); 102 | expect(syntax.hasBlockEnded(MultilineStringSyntax()), true); 103 | }); 104 | }); 105 | 106 | group('toRunnable', () { 107 | test('creates TextLineRunnable', () { 108 | final syntax = MultilineStringSyntax(); 109 | final runnable = syntax.toRunnable( 110 | "'''", 111 | RunnableDebugInformation.empty(), 112 | EnDialectMock(), 113 | ); 114 | expect(runnable, isNotNull); 115 | expect(runnable, predicate((x) => x is MultilineStringRunnable)); 116 | expect(runnable.lines.length, 0); 117 | }); 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /test/gherkin/syntax/scenario_outline_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/scenario_outline_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = ScenarioOutlineSyntax(); 12 | expect( 13 | syntax.isMatch( 14 | 'Scenario outline:', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | syntax.isMatch( 21 | 'Scenario outline: ', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | expect( 27 | syntax.isMatch( 28 | 'Scenario outline: something', 29 | EnDialectMock(), 30 | ), 31 | true, 32 | ); 33 | expect( 34 | syntax.isMatch( 35 | ' Scenario Outline: something', 36 | EnDialectMock(), 37 | ), 38 | true, 39 | ); 40 | }); 41 | 42 | test('does not match', () { 43 | final syntax = ScenarioOutlineSyntax(); 44 | expect( 45 | syntax.isMatch( 46 | 'Scenario outline something', 47 | EnDialectMock(), 48 | ), 49 | false, 50 | ); 51 | expect( 52 | syntax.isMatch( 53 | '#Scenario Outline: something', 54 | EnDialectMock(), 55 | ), 56 | false, 57 | ); 58 | }); 59 | }); 60 | 61 | group('toRunnable', () { 62 | test('creates FeatureRunnable', () { 63 | final keyword = ScenarioOutlineSyntax(); 64 | final runnable = keyword.toRunnable( 65 | 'Scenario Outline: A scenario outline 123', 66 | RunnableDebugInformation.empty(), 67 | EnDialectMock(), 68 | ); 69 | expect(runnable, isNotNull); 70 | expect(runnable, predicate((x) => x is ScenarioRunnable)); 71 | expect(runnable.name, equals('A scenario outline 123')); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /test/gherkin/syntax/scenario_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/scenario_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = ScenarioSyntax(); 12 | expect( 13 | syntax.isMatch( 14 | 'Scenario: something', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | syntax.isMatch( 21 | ' Scenario: something', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | }); 27 | 28 | test('does not match', () { 29 | final syntax = ScenarioSyntax(); 30 | expect( 31 | syntax.isMatch( 32 | 'Scenario something', 33 | EnDialectMock(), 34 | ), 35 | false, 36 | ); 37 | expect( 38 | syntax.isMatch( 39 | '#Scenario: something', 40 | EnDialectMock(), 41 | ), 42 | false, 43 | ); 44 | }); 45 | }); 46 | 47 | group('toRunnable', () { 48 | test('creates FeatureRunnable', () { 49 | final keyword = ScenarioSyntax(); 50 | final keyword2 = keyword; 51 | final runnable = keyword2.toRunnable( 52 | 'Scenario: A scenario 123', 53 | RunnableDebugInformation.empty(), 54 | EnDialectMock(), 55 | ); 56 | expect(runnable, isNotNull); 57 | expect(runnable, predicate((x) => x is ScenarioRunnable)); 58 | expect(runnable.name, equals('A scenario 123')); 59 | }); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /test/gherkin/syntax/tag_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/tag_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = TagSyntax(); 12 | expect( 13 | syntax.isMatch( 14 | '@tagone @tagtow @tag_three', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | syntax.isMatch( 21 | '@tag', 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | expect( 27 | syntax.isMatch( 28 | '@tag one', 29 | EnDialectMock(), 30 | ), 31 | true, 32 | ); 33 | }); 34 | 35 | test('does not match', () { 36 | final syntax = TagSyntax(); 37 | expect( 38 | syntax.isMatch( 39 | 'not a tag', 40 | EnDialectMock(), 41 | ), 42 | false, 43 | ); 44 | expect( 45 | syntax.isMatch( 46 | '#@tag @tag2', 47 | EnDialectMock(), 48 | ), 49 | false, 50 | ); 51 | }); 52 | }); 53 | 54 | group('toRunnable', () { 55 | test('creates TextLineRunnable', () { 56 | final syntax = TagSyntax(); 57 | final runnable = syntax.toRunnable( 58 | '@tag1 @tag2 @tag3@tag_4', 59 | RunnableDebugInformation.empty(), 60 | EnDialectMock(), 61 | ); 62 | expect(runnable, isNotNull); 63 | expect(runnable, predicate((x) => x is TagsRunnable)); 64 | expect(runnable.tags, equals(['@tag1', '@tag2', '@tag3', '@tag_4'])); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/gherkin/syntax/text_line_syntax_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/text_line.dart'; 3 | import 'package:gherkin/src/gherkin/syntax/text_line_syntax.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../mocks/en_dialect_mock.dart'; 7 | 8 | void main() { 9 | group('isMatch', () { 10 | test('matches correctly', () { 11 | final syntax = TextLineSyntax(); 12 | expect( 13 | syntax.isMatch( 14 | 'Hello Jon', 15 | EnDialectMock(), 16 | ), 17 | true, 18 | ); 19 | expect( 20 | syntax.isMatch( 21 | "Hello 'Jon'!", 22 | EnDialectMock(), 23 | ), 24 | true, 25 | ); 26 | expect( 27 | syntax.isMatch( 28 | ' Hello Jon', 29 | EnDialectMock(), 30 | ), 31 | true, 32 | ); 33 | expect( 34 | syntax.isMatch( 35 | ' Hello Jon', 36 | EnDialectMock(), 37 | ), 38 | true, 39 | ); 40 | expect( 41 | syntax.isMatch( 42 | ' h ', 43 | EnDialectMock(), 44 | ), 45 | true, 46 | ); 47 | expect( 48 | syntax.isMatch( 49 | '*', 50 | EnDialectMock(), 51 | ), 52 | true, 53 | ); 54 | expect( 55 | syntax.isMatch( 56 | ' + ', 57 | EnDialectMock(), 58 | ), 59 | true, 60 | ); 61 | }); 62 | 63 | test('does not match', () { 64 | final syntax = TextLineSyntax(); 65 | expect( 66 | syntax.isMatch( 67 | '#Hello Jon', 68 | EnDialectMock(), 69 | ), 70 | false, 71 | ); 72 | expect( 73 | syntax.isMatch( 74 | '# Hello Jon', 75 | EnDialectMock(), 76 | ), 77 | false, 78 | ); 79 | expect( 80 | syntax.isMatch( 81 | '# Hello Jon', 82 | EnDialectMock(), 83 | ), 84 | false, 85 | ); 86 | expect( 87 | syntax.isMatch( 88 | ' ', 89 | EnDialectMock(), 90 | ), 91 | false, 92 | ); 93 | expect( 94 | syntax.isMatch( 95 | ' # h ', 96 | EnDialectMock(), 97 | ), 98 | false, 99 | ); 100 | }); 101 | }); 102 | 103 | group('toRunnable', () { 104 | test('creates TextLineRunnable', () { 105 | final syntax = TextLineSyntax(); 106 | final runnable = syntax.toRunnable( 107 | ' Some text ', 108 | RunnableDebugInformation.empty(), 109 | EnDialectMock(), 110 | ); 111 | expect(runnable, isNotNull); 112 | expect(runnable, predicate((x) => x is TextLineRunnable)); 113 | expect(runnable.text, equals('Some text')); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /test/hooks/aggregated_hook_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:test/test.dart'; 3 | import '../mocks/hook_mock.dart'; 4 | 5 | void main() { 6 | group('orders hooks', () { 7 | test('executes hooks in correct order', () async { 8 | final executionOrder = []; 9 | final hookOne = HookMock( 10 | providedPriority: 0, 11 | onBeforeRunCode: () => executionOrder.add(3), 12 | ); 13 | final hookTwo = HookMock( 14 | providedPriority: 10, 15 | onBeforeRunCode: () => executionOrder.add(2), 16 | ); 17 | final hookThree = HookMock( 18 | providedPriority: 20, 19 | onBeforeRunCode: () => executionOrder.add(1), 20 | ); 21 | final hookFour = HookMock( 22 | providedPriority: -1, 23 | onBeforeRunCode: () => executionOrder.add(4), 24 | ); 25 | 26 | final aggregatedHook = AggregatedHook(); 27 | aggregatedHook.addHooks([hookOne, hookTwo, hookThree, hookFour]); 28 | await aggregatedHook.onBeforeRun(TestConfiguration.standard([])); 29 | expect(executionOrder, [1, 2, 3, 4]); 30 | expect(hookOne.onBeforeRunInvocationCount, 1); 31 | expect(hookTwo.onBeforeRunInvocationCount, 1); 32 | expect(hookThree.onBeforeRunInvocationCount, 1); 33 | expect(hookFour.onBeforeRunInvocationCount, 1); 34 | await aggregatedHook.onAfterRun(TestConfiguration.standard([])); 35 | expect(hookOne.onAfterRunInvocationCount, 1); 36 | expect(hookTwo.onAfterRunInvocationCount, 1); 37 | expect(hookThree.onAfterRunInvocationCount, 1); 38 | expect(hookFour.onAfterRunInvocationCount, 1); 39 | await aggregatedHook.onBeforeScenario( 40 | TestConfiguration.standard([]), 41 | '', 42 | const Iterable.empty(), 43 | ); 44 | expect(hookOne.onBeforeScenarioInvocationCount, 1); 45 | expect(hookTwo.onBeforeScenarioInvocationCount, 1); 46 | expect(hookThree.onBeforeScenarioInvocationCount, 1); 47 | expect(hookFour.onBeforeScenarioInvocationCount, 1); 48 | await aggregatedHook.onBeforeStep(World(), ''); 49 | expect(hookOne.onBeforeStepInvocationCount, 1); 50 | expect(hookTwo.onBeforeStepInvocationCount, 1); 51 | expect(hookThree.onBeforeStepInvocationCount, 1); 52 | expect(hookFour.onBeforeStepInvocationCount, 1); 53 | await aggregatedHook.onAfterScenarioWorldCreated( 54 | World(), 55 | '', 56 | const Iterable.empty(), 57 | ); 58 | expect(hookOne.onAfterScenarioWorldCreatedInvocationCount, 1); 59 | expect(hookTwo.onAfterScenarioWorldCreatedInvocationCount, 1); 60 | expect(hookThree.onAfterScenarioWorldCreatedInvocationCount, 1); 61 | expect(hookFour.onAfterScenarioWorldCreatedInvocationCount, 1); 62 | await aggregatedHook.onAfterStep( 63 | World(), 64 | '', 65 | StepResult( 66 | 0, 67 | StepExecutionResult.skipped, 68 | ), 69 | ); 70 | expect(hookOne.onAfterStepInvocationCount, 1); 71 | expect(hookTwo.onAfterStepInvocationCount, 1); 72 | expect(hookThree.onAfterStepInvocationCount, 1); 73 | expect(hookFour.onAfterStepInvocationCount, 1); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /test/io/io_feature_file_accessor_gnu_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('!windows') 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | import 'package:glob/glob.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'path_part_matcher.dart'; 8 | 9 | void main() { 10 | group('Matcher', () { 11 | const indexer = IoFeatureFileAccessor(); 12 | 13 | group('with RegExp', () { 14 | test('lists all matching files', () async { 15 | expect( 16 | await indexer.listFiles(RegExp('test/test_resources/(.*).feature')), 17 | PathPartMatcher([ 18 | 'test/test_resources/a.feature', 19 | 'test/test_resources/subdir/b.feature', 20 | 'test/test_resources/subdir/c.feature', 21 | ]), 22 | ); 23 | }); 24 | 25 | test('lists files from subdirectory', () async { 26 | expect( 27 | await indexer 28 | .listFiles(RegExp(r'test/test_resources/subdir/.*\.feature')), 29 | PathPartMatcher([ 30 | 'test/test_resources/subdir/b.feature', 31 | 'test/test_resources/subdir/c.feature', 32 | ]), 33 | ); 34 | }); 35 | }); 36 | 37 | group('Glob', () { 38 | test('lists all matching files', () async { 39 | expect( 40 | await indexer.listFiles(Glob('test/test_resources/**.feature')), 41 | PathPartMatcher([ 42 | 'test/test_resources/a.feature', 43 | 'test/test_resources/subdir/b.feature', 44 | 'test/test_resources/subdir/c.feature', 45 | ]), 46 | ); 47 | }); 48 | 49 | test('list all matching file without subdirectories', () async { 50 | expect( 51 | await indexer.listFiles(Glob('test/test_resources/*.feature')), 52 | PathPartMatcher([ 53 | 'test/test_resources/a.feature', 54 | ]), 55 | ); 56 | }); 57 | }); 58 | 59 | group('String', () { 60 | test('lists one specified file', () async { 61 | expect( 62 | await indexer.listFiles('test/test_resources/a.feature'), 63 | PathPartMatcher([ 64 | 'test/test_resources/a.feature', 65 | ]), 66 | ); 67 | }); 68 | }); 69 | }); 70 | 71 | group('Reader', () { 72 | test('file contents are read', () async { 73 | const indexer = IoFeatureFileAccessor(); 74 | 75 | expect( 76 | await indexer.read('test/test_resources/a.feature'), 77 | 'Feature: A', 78 | ); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/io/io_feature_file_accessor_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | import 'package:glob/glob.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('Matcher', () { 9 | const indexer = IoFeatureFileAccessor(); 10 | 11 | group('with RegExp', () { 12 | test('does not list directories', () async { 13 | expect( 14 | await indexer.listFiles(RegExp('test/test_resources/')), 15 | [], 16 | ); 17 | }); 18 | 19 | test('does not throw error for weird paths', () async { 20 | expect( 21 | await indexer.listFiles(RegExp('nonexistentpath')), 22 | [], 23 | ); 24 | }); 25 | }); 26 | 27 | group('Glob', () { 28 | test('does not return directories', () async { 29 | expect( 30 | await indexer.listFiles(Glob('test/test_resources/')), 31 | [], 32 | ); 33 | }); 34 | 35 | test('does not throw error for weird paths', () async { 36 | expect( 37 | await indexer.listFiles(Glob('nonexistentpath')), 38 | [], 39 | ); 40 | }); 41 | }); 42 | 43 | group('String', () { 44 | test('does not return directories', () async { 45 | expect( 46 | await indexer.listFiles('test/test_resources/'), 47 | [], 48 | ); 49 | }); 50 | 51 | test('does not throw error for weird paths', () async { 52 | expect( 53 | await indexer.listFiles('nonexistentpath'), 54 | [], 55 | ); 56 | }); 57 | }); 58 | }); 59 | 60 | group('Reader', () { 61 | test('file contents are read', () async { 62 | const indexer = IoFeatureFileAccessor(); 63 | 64 | expect( 65 | await indexer.read('test/test_resources/a.feature'), 66 | 'Feature: A', 67 | ); 68 | }); 69 | 70 | test('file system exception is thrown when file does not exist', () async { 71 | const indexer = IoFeatureFileAccessor(); 72 | 73 | expect( 74 | () => indexer.read('nonexistentpath'), 75 | throwsA(const TypeMatcher()), 76 | ); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/io/io_feature_file_accessor_windows_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('windows') 2 | 3 | import 'package:gherkin/gherkin.dart'; 4 | import 'package:glob/glob.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'path_part_matcher.dart'; 8 | 9 | void main() { 10 | group('Matcher', () { 11 | const indexer = IoFeatureFileAccessor(); 12 | 13 | group('with RegExp', () { 14 | test('lists all matching files', () async { 15 | expect( 16 | await indexer 17 | .listFiles(RegExp(r'test\\test_resources\\(.*).feature')), 18 | PathPartMatcher([ 19 | r'test\test_resources\a.feature', 20 | r'test\test_resources\subdir\b.feature', 21 | r'test\test_resources\subdir\c.feature', 22 | ]), 23 | ); 24 | }); 25 | 26 | test('lists files from subdirectory', () async { 27 | expect( 28 | await indexer 29 | .listFiles(RegExp(r'test\\test_resources\\subdir\\.*\.feature')), 30 | PathPartMatcher([ 31 | r'test\test_resources\subdir\b.feature', 32 | r'test\test_resources\subdir\c.feature', 33 | ]), 34 | ); 35 | }); 36 | }); 37 | 38 | group('Glob', () { 39 | test('lists all matching files', () async { 40 | expect( 41 | await indexer.listFiles(Glob('test/test_resources/**.feature')), 42 | PathPartMatcher([ 43 | r'test\test_resources\a.feature', 44 | r'test\test_resources\subdir\b.feature', 45 | r'test\test_resources\subdir\c.feature', 46 | ]), 47 | ); 48 | }); 49 | 50 | test('list all matching file without subdirectories', () async { 51 | expect( 52 | await indexer.listFiles(Glob('test/test_resources/*.feature')), 53 | PathPartMatcher([ 54 | r'test\test_resources\a.feature', 55 | ]), 56 | ); 57 | }); 58 | }); 59 | 60 | group('String', () { 61 | test('lists one specified file', () async { 62 | expect( 63 | await indexer.listFiles(r'test\test_resources\a.feature'), 64 | PathPartMatcher([ 65 | r'test\test_resources\a.feature', 66 | ]), 67 | ); 68 | }); 69 | }); 70 | }); 71 | 72 | group('Reader', () { 73 | test('file contents are read', () async { 74 | const indexer = IoFeatureFileAccessor(); 75 | 76 | expect( 77 | await indexer.read('test/test_resources/a.feature'), 78 | 'Feature: A', 79 | ); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /test/io/path_part_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:matcher/matcher.dart'; 2 | 3 | class PathPartMatcher extends Matcher { 4 | late final Iterable relativePaths; 5 | final description = StringDescription(); 6 | 7 | PathPartMatcher(this.relativePaths); 8 | 9 | @override 10 | Description describe(Description description) { 11 | return description..add(description.toString()); 12 | } 13 | 14 | @override 15 | bool matches(dynamic absolutePaths, Map matchState) { 16 | var match = true; 17 | 18 | for (final absPath in absolutePaths as Iterable) { 19 | var internalMatch = false; 20 | 21 | for (final relativePath in relativePaths) { 22 | if (absPath.contains(relativePath)) { 23 | internalMatch = true; 24 | break; 25 | } 26 | } 27 | 28 | match = match && internalMatch; 29 | } 30 | 31 | return match; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/mocks/en_dialect_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | class EnDialectMock extends GherkinDialect { 4 | EnDialectMock() 5 | : super( 6 | name: 'English', 7 | nativeName: 'English', 8 | languageCode: 'en', 9 | feature: ['Feature'], 10 | background: ['Background'], 11 | rule: ['Rule'], 12 | scenario: ['Scenario'], 13 | scenarioOutline: ['Scenario Outline'], 14 | examples: ['Scenarios', 'Examples'], 15 | given: ['Given'], 16 | when: ['When'], 17 | then: ['Then'], 18 | and: ['And'], 19 | but: ['But'], 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/mocks/fr_dialect_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | class FrDialectMock extends GherkinDialect { 4 | FrDialectMock() 5 | : super( 6 | name: 'English', 7 | nativeName: 'English', 8 | languageCode: 'en', 9 | feature: ['Fonctionnalité'], 10 | background: ['Contexte'], 11 | rule: ['Rule'], 12 | scenario: ['Exemple', 'Scénario'], 13 | scenarioOutline: ['Plan du scénario', 'Plan du Scénario'], 14 | examples: ['Exemples'], 15 | given: [ 16 | '*', 17 | 'Soit', 18 | 'Sachant que', 19 | "Sachant qu'", 20 | 'Sachant', 21 | 'Etant donné que', 22 | "Etant donné qu'", 23 | 'Etant donné', 24 | 'Etant donnée', 25 | 'Etant donnés', 26 | 'Etant données', 27 | 'Étant donné que', 28 | "Étant donné qu'", 29 | 'Étant donné', 30 | 'Étant donnée', 31 | 'Étant donnés', 32 | 'Étant données', 33 | ], 34 | when: [ 35 | '*', 36 | 'Quand', 37 | 'Lorsque', 38 | "Lorsqu'", 39 | ], 40 | then: [ 41 | '*', 42 | 'Alors', 43 | 'Donc', 44 | ], 45 | and: [ 46 | '*', 47 | 'Et que', 48 | "Et qu'", 49 | 'Et', 50 | ], 51 | but: [ 52 | '*', 53 | 'Mais que', 54 | "Mais qu'", 55 | 'Mais', 56 | ], 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /test/mocks/gherkin_expression_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/expressions/gherkin_expression.dart'; 2 | 3 | typedef IsMatchFn = bool Function(String input); 4 | 5 | class MockGherkinExpression implements GherkinExpression { 6 | final IsMatchFn isMatchFn; 7 | 8 | MockGherkinExpression(this.isMatchFn); 9 | 10 | @override 11 | Iterable getParameters(String input) => const Iterable.empty(); 12 | 13 | @override 14 | bool isMatch(String input) => isMatchFn(input); 15 | 16 | @override 17 | RegExp get originalExpression => RegExp('.'); 18 | } 19 | -------------------------------------------------------------------------------- /test/mocks/hook_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | typedef OnBeforeRunCode = void Function(); 4 | 5 | class HookMock extends Hook { 6 | int onBeforeRunInvocationCount = 0; 7 | int onAfterRunInvocationCount = 0; 8 | int onBeforeScenarioInvocationCount = 0; 9 | int onBeforeStepInvocationCount = 0; 10 | int onAfterScenarioInvocationCount = 0; 11 | int onAfterScenarioWorldCreatedInvocationCount = 0; 12 | int onAfterStepInvocationCount = 0; 13 | late List? onBeforeScenarioTags; 14 | late List? onAfterScenarioTags; 15 | 16 | final int providedPriority; 17 | final OnBeforeRunCode? onBeforeRunCode; 18 | 19 | @override 20 | int get priority => providedPriority; 21 | 22 | HookMock({this.onBeforeRunCode, this.providedPriority = 0}); 23 | 24 | @override 25 | Future onBeforeRun(TestConfiguration config) async { 26 | onBeforeRunInvocationCount += 1; 27 | onBeforeRunCode?.call(); 28 | } 29 | 30 | @override 31 | Future onAfterRun(TestConfiguration config) async => 32 | onAfterRunInvocationCount += 1; 33 | 34 | @override 35 | Future onBeforeScenario( 36 | TestConfiguration config, 37 | String scenario, 38 | Iterable tags, 39 | ) async { 40 | onBeforeScenarioTags = tags.toList(); 41 | onBeforeScenarioInvocationCount += 1; 42 | } 43 | 44 | @override 45 | Future onBeforeStep(World world, String step) async => 46 | onBeforeStepInvocationCount += 1; 47 | 48 | @override 49 | Future onAfterScenario( 50 | TestConfiguration config, 51 | String scenario, 52 | Iterable tags, { 53 | bool passed = true, 54 | }) async { 55 | onAfterScenarioTags = tags.toList(); 56 | onAfterScenarioInvocationCount += 1; 57 | } 58 | 59 | @override 60 | Future onAfterScenarioWorldCreated( 61 | World world, 62 | String scenario, 63 | Iterable tags, 64 | ) async => 65 | onAfterScenarioWorldCreatedInvocationCount += 1; 66 | 67 | @override 68 | Future onAfterStep(World world, String step, StepResult result) async => 69 | onAfterStepInvocationCount += 1; 70 | } 71 | -------------------------------------------------------------------------------- /test/mocks/language_service_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | import 'en_dialect_mock.dart'; 4 | 5 | typedef OnStepFinished = void Function(StepMessage message); 6 | 7 | class LanguageServiceMock extends LanguageService { 8 | String _defaultLangauge = 'en'; 9 | @override 10 | String get defaultLanguage => _defaultLangauge; 11 | 12 | LanguageServiceMock() : super() { 13 | initialise(); 14 | } 15 | 16 | @override 17 | void initialise([String defaultLanguage = 'en']) { 18 | _defaultLangauge = defaultLanguage; 19 | setDialect('en', EnDialectMock()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/mocks/reporter_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | typedef OnStepFinished = void Function(StepMessage message); 4 | 5 | class ReporterMock extends FullReporter { 6 | int onTestRunStartedInvocationCount = 0; 7 | int onTestRunFinishedInvocationCount = 0; 8 | int onFeatureStartedInvocationCount = 0; 9 | int onFeatureFinishedInvocationCount = 0; 10 | int onScenarioStartedInvocationCount = 0; 11 | int onScenarioFinishedInvocationCount = 0; 12 | int onStepStartedInvocationCount = 0; 13 | int onStepFinishedInvocationCount = 0; 14 | int onExceptionInvocationCount = 0; 15 | int messageInvocationCount = 0; 16 | int disposeInvocationCount = 0; 17 | 18 | OnStepFinished? onStepFinishedFn; 19 | 20 | @override 21 | Future onException(Object exception, StackTrace stackTrace) async => 22 | onExceptionInvocationCount += 1; 23 | 24 | @override 25 | Future message(String message, MessageLevel level) async => 26 | messageInvocationCount += 1; 27 | 28 | @override 29 | Future dispose() async => disposeInvocationCount += 1; 30 | 31 | @override 32 | ReportActionHandler get feature => ReportActionHandler( 33 | onStarted: ([_]) async => onFeatureStartedInvocationCount += 1, 34 | onFinished: ([_]) async => onFeatureFinishedInvocationCount += 1, 35 | ); 36 | 37 | @override 38 | ReportActionHandler get scenario => ReportActionHandler( 39 | onStarted: ([message]) async => onScenarioStartedInvocationCount += 1, 40 | onFinished: ([message]) async => onScenarioFinishedInvocationCount += 1, 41 | ); 42 | 43 | @override 44 | ReportActionHandler get step => ReportActionHandler( 45 | onStarted: ([message]) async => onStepStartedInvocationCount += 1, 46 | onFinished: ([message]) async { 47 | if (message != null && onStepFinishedFn != null) { 48 | onStepFinishedFn!(message); 49 | } 50 | 51 | onStepFinishedInvocationCount += 1; 52 | }, 53 | ); 54 | 55 | @override 56 | ReportActionHandler get test => ReportActionHandler( 57 | onStarted: ([massage]) async => onTestRunStartedInvocationCount += 1, 58 | onFinished: ([massage]) async => onTestRunFinishedInvocationCount += 1, 59 | ); 60 | } 61 | 62 | class SerializableReporterMock extends Reporter 63 | implements JsonSerializableReporter { 64 | final String _json; 65 | 66 | SerializableReporterMock(this._json); 67 | 68 | @override 69 | String serialize() { 70 | return _json; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/mocks/step_definition_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | typedef OnRunCode = Future Function(Iterable parameters); 4 | 5 | class MockStepDefinition extends StepDefinitionBase { 6 | bool hasRun = false; 7 | int runCount = 0; 8 | final OnRunCode? code; 9 | 10 | MockStepDefinition([ 11 | this.code, 12 | int expectedParameterCount = 0, 13 | ]) : super( 14 | StepDefinitionConfiguration() 15 | ..timeout = const Duration(milliseconds: 200), 16 | expectedParameterCount, 17 | ); 18 | 19 | @override 20 | Future onRun(Iterable parameters) async { 21 | hasRun = true; 22 | runCount += 1; 23 | if (code != null) { 24 | await code!(parameters); 25 | } 26 | } 27 | 28 | @override 29 | RegExp get pattern => RegExp('.'); 30 | } 31 | -------------------------------------------------------------------------------- /test/mocks/tag_expression_evaluator_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/expressions/tag_expression.dart'; 2 | 3 | class MockTagExpressionEvaluator implements TagExpressionEvaluator { 4 | @override 5 | bool evaluate(String tagExpression, List tags) => true; 6 | } 7 | -------------------------------------------------------------------------------- /test/mocks/world_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | class WorldMock extends World { 4 | bool disposeFnInvoked = false; 5 | 6 | @override 7 | void dispose() => disposeFnInvoked = true; 8 | } 9 | 10 | class WorldMockThatThrowsWhenDisposed extends World { 11 | bool disposeFnInvoked = false; 12 | 13 | @override 14 | void dispose() => throw Exception('Error occurred in dispose'); 15 | } 16 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "description": "Feature 1 description", 9 | "tags": [ 10 | { 11 | "line": 2, 12 | "name": "tag1" 13 | } 14 | ], 15 | "elements": [ 16 | { 17 | "keyword": "Scenario", 18 | "type": "scenario", 19 | "id": "feature 1;scenario 1", 20 | "name": "Scenario 1", 21 | "line": 5, 22 | "status": "passed", 23 | "description": "Scenario 1 description", 24 | "tags": [ 25 | { 26 | "line": 4, 27 | "name": "tag2" 28 | } 29 | ], 30 | "steps": [ 31 | { 32 | "keyword": "Step ", 33 | "name": "1", 34 | "line": 6, 35 | "match": { 36 | "location": "filepath:6" 37 | }, 38 | "result": { 39 | "status": "passed", 40 | "duration": 100000000 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario", 17 | "type": "scenario", 18 | "id": "feature 1;scenario 1", 19 | "name": "Scenario 1", 20 | "line": 5, 21 | "status": "failed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | } 40 | }, 41 | { 42 | "keyword": "Step ", 43 | "name": "2", 44 | "line": 7, 45 | "match": { 46 | "location": "filepath:7" 47 | }, 48 | "result": { 49 | "status": "failed", 50 | "duration": 100000000, 51 | "error_message": "error message" 52 | } 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario", 17 | "type": "scenario", 18 | "id": "feature 1;scenario 1", 19 | "name": "Scenario 1", 20 | "line": 5, 21 | "status": "failed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | } 40 | }, 41 | { 42 | "keyword": "Step ", 43 | "name": "2", 44 | "line": 7, 45 | "match": { 46 | "location": "filepath:7" 47 | }, 48 | "result": { 49 | "status": "failed", 50 | "duration": 100000000, 51 | "error_message": "error message" 52 | } 53 | }, 54 | { 55 | "keyword": "Step ", 56 | "name": "3", 57 | "line": 8, 58 | "match": { 59 | "location": "filepath:8" 60 | }, 61 | "result": { 62 | "status": "skipped", 63 | "duration": 100000000 64 | } 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario", 17 | "type": "scenario", 18 | "id": "feature 1;scenario 1", 19 | "name": "Scenario 1", 20 | "line": 5, 21 | "status": "failed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | } 40 | }, 41 | { 42 | "keyword": "Step ", 43 | "name": "2", 44 | "line": 7, 45 | "match": { 46 | "location": "filepath:7" 47 | }, 48 | "result": { 49 | "status": "failed", 50 | "duration": 100000000, 51 | "error_message": "error message" 52 | }, 53 | "embeddings": [ 54 | { 55 | "mime_type": "mimetype", 56 | "data": "data" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_5.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario", 17 | "type": "scenario", 18 | "id": "feature 1;scenario 1", 19 | "name": "Scenario 1", 20 | "line": 5, 21 | "status": "passed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | }, 40 | "docString": { 41 | "content_type": "", 42 | "value": "a\nb\nc", 43 | "line": 7 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_6.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario Outline", 17 | "type": "scenario", 18 | "id": "feature 1;scenario outline 1", 19 | "name": "Scenario Outline 1", 20 | "line": 5, 21 | "status": "passed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | } 40 | }, 41 | { 42 | "keyword": "Step ", 43 | "name": "2", 44 | "line": 7, 45 | "match": { 46 | "location": "filepath:7" 47 | }, 48 | "result": { 49 | "status": "passed", 50 | "duration": 100000000 51 | }, 52 | "embeddings": [ 53 | { 54 | "mime_type": "mimetype", 55 | "data": "data" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_7.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": null, 4 | "keyword": "Feature", 5 | "line": 0, 6 | "name": "Unnamed feature", 7 | "uri": "unknown", 8 | "description": "An unnamed feature is possible if something is logged before any feature has started to execute", 9 | "elements": [ 10 | { 11 | "keyword": "Scenario", 12 | "type": "scenario", 13 | "id": "null;unnamed", 14 | "name": "Unnamed", 15 | "line": 0, 16 | "status": "failed", 17 | "description": "An unnamed scenario is possible if something is logged before any feature has started to execute", 18 | "steps": [ 19 | { 20 | "keyword": null, 21 | "name": "Unnamed", 22 | "line": 0, 23 | "match": { 24 | "location": "null:0" 25 | }, 26 | "result": { 27 | "status": "failed", 28 | "duration": 0, 29 | "error_message": "Exception: Test exception" 30 | } 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_8.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "feature 1", 4 | "keyword": "Feature", 5 | "line": 3, 6 | "name": "Feature 1", 7 | "uri": "filepath", 8 | "tags": [ 9 | { 10 | "line": 2, 11 | "name": "tag1" 12 | } 13 | ], 14 | "elements": [ 15 | { 16 | "keyword": "Scenario", 17 | "type": "scenario", 18 | "id": "feature 1;scenario 1", 19 | "name": "Scenario 1", 20 | "line": 5, 21 | "status": "failed", 22 | "tags": [ 23 | { 24 | "line": 4, 25 | "name": "tag2" 26 | } 27 | ], 28 | "steps": [ 29 | { 30 | "keyword": "Step ", 31 | "name": "1", 32 | "line": 6, 33 | "match": { 34 | "location": "filepath:6" 35 | }, 36 | "result": { 37 | "status": "passed", 38 | "duration": 100000000 39 | } 40 | }, 41 | { 42 | "keyword": "Step ", 43 | "name": "2", 44 | "line": 7, 45 | "match": { 46 | "location": "filepath:7" 47 | }, 48 | "result": { 49 | "status": "failed", 50 | "duration": 100000000, 51 | "error_message": "error message" 52 | } 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | { 59 | "id": "feature 2", 60 | "keyword": "Feature", 61 | "line": 3, 62 | "name": "Feature 2", 63 | "uri": "filepath", 64 | "tags": [ 65 | { 66 | "line": 2, 67 | "name": "tag1" 68 | } 69 | ], 70 | "elements": [ 71 | { 72 | "keyword": "Scenario", 73 | "type": "scenario", 74 | "id": "feature 2;scenario 2", 75 | "name": "Scenario 2", 76 | "line": 5, 77 | "status": "passed", 78 | "tags": [ 79 | { 80 | "line": 4, 81 | "name": "tag2" 82 | } 83 | ], 84 | "steps": [ 85 | { 86 | "keyword": "Step ", 87 | "name": "1", 88 | "line": 6, 89 | "match": { 90 | "location": "filepath:6" 91 | }, 92 | "result": { 93 | "status": "passed", 94 | "duration": 100000000 95 | } 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /test/reporters/progress_reporter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class TestableProgressReporter extends ProgressReporter { 5 | final output = []; 6 | @override 7 | void printMessageLine( 8 | String message, [ 9 | String? colour, 10 | ]) { 11 | output.add(message); 12 | } 13 | } 14 | 15 | void main() { 16 | group('report', () { 17 | test('provides correct step finished output', () async { 18 | final reporter = TestableProgressReporter(); 19 | 20 | await reporter.step.onFinished.invoke( 21 | StepMessage( 22 | name: 'Step 1', 23 | context: RunnableDebugInformation('filePath', 1, 'line 1'), 24 | result: StepResult(0, StepExecutionResult.passed), 25 | attachments: [Attachment('A string', 'text/plain')], 26 | ), 27 | ); 28 | await reporter.step.onFinished.invoke( 29 | StepMessage( 30 | name: 'Step 2', 31 | context: RunnableDebugInformation('filePath', 2, 'line 2'), 32 | result: StepResult( 33 | 0, 34 | StepExecutionResult.fail, 35 | resultReason: 'Failed Reason', 36 | ), 37 | ), 38 | ); 39 | await reporter.step.onFinished.invoke( 40 | StepMessage( 41 | name: 'Step 3', 42 | context: RunnableDebugInformation('filePath', 3, 'line 3'), 43 | result: StepResult(0, StepExecutionResult.skipped), 44 | ), 45 | ); 46 | await reporter.step.onFinished.invoke( 47 | StepMessage( 48 | name: 'Step 4', 49 | context: RunnableDebugInformation('filePath', 4, 'line 4'), 50 | result: StepResult(0, StepExecutionResult.error), 51 | ), 52 | ); 53 | await reporter.step.onFinished.invoke( 54 | StepMessage( 55 | name: 'Step 5', 56 | context: RunnableDebugInformation('filePath', 5, 'line 5'), 57 | result: StepResult(1, StepExecutionResult.timeout), 58 | ), 59 | ); 60 | 61 | expect(reporter.output, [ 62 | ' √ Step 1 # filePath:1 took 0ms', 63 | ' Attachment (text/plain): A string', 64 | ' × Step 2 # filePath:2 took 0ms \n Failed Reason', 65 | ' - Step 3 # filePath:3 took 0ms', 66 | ' × Step 4 # filePath:4 took 0ms', 67 | ' × Step 5 # filePath:5 took 1ms' 68 | ]); 69 | }); 70 | 71 | test('provides correct scenario started output', () async { 72 | final reporter = TestableProgressReporter(); 73 | 74 | await reporter.scenario.onStarted.invoke( 75 | ScenarioMessage( 76 | name: 'Scenario 1', 77 | context: RunnableDebugInformation('filePath', 1, 'line 1'), 78 | ), 79 | ); 80 | 81 | expect(reporter.output, ['Running scenario: Scenario 1 # filePath:1']); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/reporters/test_run_summary_reporter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class TestableTestRunSummaryReporter extends TestRunSummaryReporter { 5 | final output = []; 6 | @override 7 | void printMessageLine( 8 | String message, [ 9 | String? colour, 10 | ]) { 11 | output.add(message); 12 | } 13 | } 14 | 15 | void main() { 16 | group('report', () { 17 | test('provides correct output', () async { 18 | final reporter = TestableTestRunSummaryReporter(); 19 | 20 | await reporter.step.onFinished.invoke( 21 | StepMessage( 22 | name: '', 23 | context: RunnableDebugInformation.empty(), 24 | result: StepResult(0, StepExecutionResult.passed), 25 | ), 26 | ); 27 | await reporter.step.onFinished.invoke( 28 | StepMessage( 29 | name: '', 30 | context: RunnableDebugInformation.empty(), 31 | result: StepResult(0, StepExecutionResult.fail), 32 | ), 33 | ); 34 | await reporter.step.onFinished.invoke( 35 | StepMessage( 36 | name: '', 37 | context: RunnableDebugInformation.empty(), 38 | result: StepResult(0, StepExecutionResult.skipped), 39 | ), 40 | ); 41 | await reporter.step.onFinished.invoke( 42 | StepMessage( 43 | name: '', 44 | context: RunnableDebugInformation.empty(), 45 | result: StepResult(0, StepExecutionResult.skipped), 46 | ), 47 | ); 48 | await reporter.step.onFinished.invoke( 49 | StepMessage( 50 | name: '', 51 | context: RunnableDebugInformation.empty(), 52 | result: StepResult(0, StepExecutionResult.passed), 53 | ), 54 | ); 55 | await reporter.step.onFinished.invoke( 56 | StepMessage( 57 | name: '', 58 | context: RunnableDebugInformation.empty(), 59 | result: StepResult(0, StepExecutionResult.error), 60 | ), 61 | ); 62 | await reporter.step.onFinished.invoke( 63 | StepMessage( 64 | name: '', 65 | context: RunnableDebugInformation.empty(), 66 | result: StepResult(0, StepExecutionResult.passed), 67 | ), 68 | ); 69 | await reporter.step.onFinished.invoke( 70 | StepMessage( 71 | name: '', 72 | context: RunnableDebugInformation.empty(), 73 | result: StepResult(0, StepExecutionResult.timeout), 74 | ), 75 | ); 76 | 77 | await reporter.scenario.onFinished.invoke( 78 | ScenarioMessage( 79 | name: '', 80 | context: RunnableDebugInformation.empty(), 81 | hasPassed: true, 82 | ), 83 | ); 84 | await reporter.scenario.onFinished.invoke( 85 | ScenarioMessage( 86 | name: '', 87 | context: RunnableDebugInformation.empty(), 88 | hasPassed: false, 89 | ), 90 | ); 91 | await reporter.scenario.onFinished.invoke( 92 | ScenarioMessage( 93 | name: '', 94 | context: RunnableDebugInformation.empty(), 95 | hasPassed: false, 96 | ), 97 | ); 98 | await reporter.scenario.onFinished.invoke( 99 | ScenarioMessage( 100 | name: '', 101 | context: RunnableDebugInformation.empty(), 102 | hasPassed: true, 103 | ), 104 | ); 105 | 106 | await reporter.test.onFinished.invoke(); 107 | expect(reporter.output, [ 108 | '4 scenarios (\x1B[33;32m2 passed\x1B[33;0m, \x1B[33;31m2 failed\x1B[33;0m)', 109 | '8 steps (\x1B[33;32m3 passed\x1B[33;0m, \x1B[33;10m2 skipped\x1B[33;0m, \x1B[33;31m3 failed\x1B[33;0m)', 110 | '0:00:00.000000' 111 | ]); 112 | }); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /test/test_resources/a.feature: -------------------------------------------------------------------------------- 1 | Feature: A -------------------------------------------------------------------------------- /test/test_resources/subdir/b.feature: -------------------------------------------------------------------------------- 1 | Feature: B -------------------------------------------------------------------------------- /test/test_resources/subdir/c.feature: -------------------------------------------------------------------------------- 1 | Feature: C -------------------------------------------------------------------------------- /test/utils/step_method_declaration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import '../mocks/reporter_mock.dart'; 5 | import '../mocks/world_mock.dart'; 6 | 7 | void main() { 8 | group('function steps', () { 9 | test('step with 1 parameter is invoked', () async { 10 | const parameter1Value = 1; 11 | late int parameter1GivenValue; 12 | 13 | final stepMethod = when1( 14 | 'pattern', 15 | (int input1, _) { 16 | parameter1GivenValue = input1; 17 | return Future.value(null); 18 | }, 19 | ); 20 | 21 | await stepMethod.run( 22 | World(), 23 | ReporterMock(), 24 | const Duration(seconds: 10), 25 | [parameter1Value], 26 | ); 27 | 28 | expect(parameter1GivenValue, parameter1Value); 29 | }); 30 | 31 | test('step is invoked with custom world', () async { 32 | final customWorld = WorldMock(); 33 | late World receivedWorld; 34 | 35 | final stepMethod = given2( 36 | 'pattern', 37 | (int input1, String input2, StepContext ctx) { 38 | receivedWorld = ctx.world; 39 | 40 | return Future.value(null); 41 | }, 42 | ); 43 | 44 | await stepMethod.run( 45 | customWorld, 46 | ReporterMock(), 47 | const Duration(seconds: 10), 48 | [1, '2'], 49 | ); 50 | 51 | expect(receivedWorld, customWorld); 52 | }); 53 | }); 54 | } 55 | --------------------------------------------------------------------------------