├── test ├── test_resources │ ├── a.feature │ └── subdir │ │ ├── b.feature │ │ └── c.feature ├── mocks │ ├── tag_expression_evaluator_mock.dart │ ├── fr_dialect_mock.dart │ ├── world_mock.dart │ ├── gherkin_expression_mock.dart │ ├── language_service_mock.dart │ ├── en_dialect_mock.dart │ ├── step_definition_mock.dart │ ├── hook_mock.dart │ └── reporter_mock.dart ├── gherkin │ ├── parameters │ │ ├── int_parameter.dart │ │ ├── word_parameter.dart │ │ ├── float_parameter_test.dart │ │ └── string_parameter_test.dart │ ├── runnables │ │ ├── scenario_test.dart │ │ ├── examples_test.dart │ │ ├── feature_file_test.dart │ │ ├── multi_line_string_test.dart │ │ ├── step_test.dart │ │ ├── feature_test.dart │ │ └── scenario_outline_test.dart │ ├── syntax │ │ ├── comment_syntax_test.dart │ │ ├── empty_line_syntax_test.dart │ │ ├── feature_syntax_test.dart │ │ ├── scenario_syntax_test.dart │ │ ├── tag_syntax_test.dart │ │ ├── scenario_outline_syntax_test.dart │ │ ├── language_syntax_test.dart │ │ ├── background_syntax_test.dart │ │ ├── text_line_syntax_test.dart │ │ ├── example_syntax_test.dart │ │ └── multiline_string_syntax_test.dart │ ├── attachments │ │ └── attachment_manager_test.dart │ ├── langauges │ │ └── language_service_test.dart │ └── expressions │ │ └── tag_expression_test.dart ├── io │ ├── path_part_matcher.dart │ ├── io_feature_file_accessor_test.dart │ ├── io_feature_file_accessor_gnu_test.dart │ └── io_feature_file_accessor_windows_test.dart ├── reporters │ ├── json_reports │ │ ├── report_1.json │ │ ├── report_7.json │ │ ├── report_5.json │ │ ├── report_2.json │ │ ├── report_6.json │ │ ├── report_4.json │ │ ├── report_3.json │ │ └── report_8.json │ ├── progress_reporter_test.dart │ └── test_run_summary_reporter_test.dart ├── utils │ └── step_method_declaration_test.dart └── hooks │ └── aggregated_hook_test.dart ├── .gitignore ├── lib ├── src │ ├── gherkin │ │ ├── runnables │ │ │ ├── scenario_type_enum.dart │ │ │ ├── background.dart │ │ │ ├── runnable_block.dart │ │ │ ├── empty_line.dart │ │ │ ├── runnable_result.dart │ │ │ ├── comment_line.dart │ │ │ ├── runnable.dart │ │ │ ├── dialect_block.dart │ │ │ ├── text_line.dart │ │ │ ├── tags.dart │ │ │ ├── debug_information.dart │ │ │ ├── language.dart │ │ │ ├── taggable_runnable_block.dart │ │ │ ├── scenario_expanded_from_outline_example.dart │ │ │ ├── scenario.dart │ │ │ ├── example.dart │ │ │ ├── feature_file.dart │ │ │ ├── multi_line_string.dart │ │ │ ├── table.dart │ │ │ ├── step.dart │ │ │ ├── feature.dart │ │ │ └── scenario_outline.dart │ │ ├── exceptions │ │ │ ├── gherkin_exception.dart │ │ │ ├── test_run_failed_exception.dart │ │ │ ├── syntax_error.dart │ │ │ ├── step_not_defined_error.dart │ │ │ ├── dialect_not_supported.dart │ │ │ └── parameter_count_mismatch_error.dart │ │ ├── steps │ │ │ ├── step_configuration.dart │ │ │ ├── executable_step.dart │ │ │ ├── world.dart │ │ │ ├── step_run_result.dart │ │ │ └── step_definition.dart │ │ ├── feature.dart │ │ ├── languages │ │ │ ├── README.md │ │ │ ├── language_service.dart │ │ │ └── dialect.dart │ │ ├── attachments │ │ │ ├── attachment.dart │ │ │ └── attachment_manager.dart │ │ ├── parameters │ │ │ ├── step_defined_parameter.dart │ │ │ ├── plural_parameter.dart │ │ │ ├── word_parameter.dart │ │ │ ├── string_parameter.dart │ │ │ ├── int_parameter.dart │ │ │ ├── float_parameter.dart │ │ │ └── custom_parameter.dart │ │ ├── models │ │ │ ├── table_row.dart │ │ │ └── table.dart │ │ ├── syntax │ │ │ ├── regex_matched_syntax.dart │ │ │ ├── empty_line_syntax.dart │ │ │ ├── comment_syntax.dart │ │ │ ├── syntax_matcher.dart │ │ │ ├── feature_file_syntax.dart │ │ │ ├── language_syntax.dart │ │ │ ├── text_line_syntax.dart │ │ │ ├── feature_syntax.dart │ │ │ ├── table_line_syntax.dart │ │ │ ├── step_syntax.dart │ │ │ ├── example_syntax.dart │ │ │ ├── scenario_syntax.dart │ │ │ ├── background_syntax.dart │ │ │ ├── scenario_outline_syntax.dart │ │ │ ├── tag_syntax.dart │ │ │ └── multiline_string_syntax.dart │ │ └── ast │ │ │ └── feature_file_visitor.dart │ ├── reporters │ │ ├── message_level.dart │ │ ├── serializable_reporter.dart │ │ ├── json │ │ │ ├── json_row.dart │ │ │ ├── json_embedding.dart │ │ │ ├── json_tag.dart │ │ │ ├── json_scenario.dart │ │ │ ├── json_feature.dart │ │ │ ├── json_reporter.dart │ │ │ └── json_step.dart │ │ ├── reporter.dart │ │ ├── messages.dart │ │ ├── stdout_reporter.dart │ │ ├── aggregated_reporter.dart │ │ └── test_run_summary_reporter.dart │ ├── io │ │ ├── feature_file_reader.dart │ │ ├── feature_file_matcher.dart │ │ └── io_feature_file_accessor.dart │ ├── processes │ │ └── process_handler.dart │ ├── utils │ │ └── perf.dart │ ├── hooks │ │ ├── hook.dart │ │ └── aggregated_hook.dart │ └── expect │ │ ├── expect_mimic_utils.dart │ │ └── expect_mimic.dart └── gherkin.dart ├── .editorconfig ├── .vscode ├── settings.json └── launch.json ├── example ├── features │ ├── calculator_can_add.feature │ ├── calculator_can_add_powers_of_two.feature │ ├── calculator_background_example.feature │ ├── calculator_count_strings.feature │ ├── calculator_scenario_outline_example.feature │ └── calculator_scenario_outline_german_example.feature ├── supporting_files │ ├── worlds │ │ └── custom_world.world.dart │ ├── steps │ │ ├── when_numbers_are_added.step.dart │ │ ├── when_the_characters_are_counted.step.dart │ │ ├── given_the_characters.step.dart │ │ ├── given_the_numbers.step.dart │ │ ├── then_expect_numeric_result.step.dart │ │ ├── given_the_powers_of_two.step.dart │ │ ├── multiline_string_example_step.dart │ │ └── data_table_example_step.dart │ ├── parameters │ │ └── power_of_two.parameter.dart │ └── hooks │ │ └── hook_example.dart ├── README.md ├── calculator.dart └── test.dart ├── pre-publish-checks.cmd ├── analysis_options.yaml ├── pubspec.yaml ├── LICENSE └── .github └── workflows └── test-package.yml /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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .idea/ 7 | 8 | build/ 9 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario_type_enum.dart: -------------------------------------------------------------------------------- 1 | enum ScenarioType { scenario, scenario_outline } 2 | -------------------------------------------------------------------------------- /lib/src/reporters/message_level.dart: -------------------------------------------------------------------------------- 1 | enum MessageLevel { verbose, debug, info, warning, error } 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/gherkin_exception.dart: -------------------------------------------------------------------------------- 1 | abstract class GherkinException implements Exception {} 2 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_configuration.dart: -------------------------------------------------------------------------------- 1 | class StepDefinitionConfiguration { 2 | Duration? timeout; 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/reporters/serializable_reporter.dart: -------------------------------------------------------------------------------- 1 | abstract class SerializableReporter { 2 | String toJson(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/gherkin/exceptions/test_run_failed_exception.dart: -------------------------------------------------------------------------------- 1 | class GherkinTestRunFailedException implements Exception {} 2 | -------------------------------------------------------------------------------- /lib/src/io/feature_file_reader.dart: -------------------------------------------------------------------------------- 1 | abstract class FeatureFileReader { 2 | Future read(String path); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/processes/process_handler.dart: -------------------------------------------------------------------------------- 1 | abstract class ProcessHandler { 2 | Future run(); 3 | Future terminate(); 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/io/feature_file_matcher.dart: -------------------------------------------------------------------------------- 1 | abstract class FeatureFileMatcher { 2 | Future> listFiles(Pattern pattern); 3 | } 4 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Debuggable", 4 | "Errored", 5 | "Multiline", 6 | "Serializable", 7 | "behaviour", 8 | "pubspec", 9 | "writeln" 10 | ] 11 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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, debug); 7 | } 8 | -------------------------------------------------------------------------------- /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/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_tag.dart: -------------------------------------------------------------------------------- 1 | class JsonTag { 2 | final String name; 3 | final int line; 4 | 5 | JsonTag(this.name, this.line); 6 | 7 | Map toJson() { 8 | return { 9 | 'line': line, 10 | 'name': name, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /test/mocks/fr_dialect_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | class FrDialectMock extends GherkinDialect { 4 | FrDialectMock() { 5 | name = 'French'; 6 | when = ['* ', 'Quand ', 'Lorsque ', "Lorsqu'"]; 7 | 8 | stepKeywords = ([ 9 | ...when, 10 | ]).toSet(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pre-publish-checks.cmd: -------------------------------------------------------------------------------- 1 | CALL "C:\Google\flutter\bin\cache\dart-sdk\bin\dartanalyzer" --options analysis_options.yaml . 2 | CALL "C:\Google\flutter\bin\cache\dart-sdk\bin\dartfmt" . -w 3 | CALL "C:\Google\flutter\bin\cache\dart-sdk\bin\pub" run test . 4 | CALL cd example && "C:\Google\flutter\bin\cache\dart-sdk\bin\dart" test.dart 5 | cd ../ 6 | REM pana --source path ./ 7 | -------------------------------------------------------------------------------- /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 | Scenario: Add two numbers with background 11 | Then the expected result is 150 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/runnable.dart: -------------------------------------------------------------------------------- 1 | import './debug_information.dart'; 2 | 3 | abstract class Runnable { 4 | RunnableDebugInformation _debug; 5 | RunnableDebugInformation get debug => _debug; 6 | String get name; 7 | 8 | Runnable(this._debug); 9 | 10 | void updateDebugInformation(RunnableDebugInformation debugInformation) { 11 | _debug = debugInformation; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/utils/perf.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | 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 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/dialect_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/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 | -------------------------------------------------------------------------------- /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:pedantic/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 | 11 | # analyzer: 12 | # exclude: 13 | # - path/to/excluded/files/** 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/supporting_files/parameters/power_of_two.parameter.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'dart:math'; 3 | 4 | class PowerOfTwoParameter extends CustomParameter { 5 | PowerOfTwoParameter() 6 | : super( 7 | 'POW', 8 | RegExp(r'([0-9]+\^[0-9]+)', caseSensitive: true), 9 | (input) { 10 | final parts = input.split('^'); 11 | return pow(int.parse(parts[0]), int.parse(parts[1])) as int; 12 | }, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Example", 9 | "program": "test.dart", 10 | "cwd": "example", 11 | "request": "launch", 12 | "type": "dart" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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(this._columns, this.rowIndex, this.isHeaderRow); 8 | 9 | void setStepParameter(String parameterName, String value) { 10 | _columns = _columns.map((c) => c?.replaceAll('<$parameterName>', value)); 11 | } 12 | 13 | TableRow clone() => 14 | TableRow(_columns.map((c) => c).toList(), rowIndex, isHeaderRow); 15 | } 16 | -------------------------------------------------------------------------------- /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 | @debug 6 | Scenario Outline: Counts string's code units 7 | Given the characters "" 8 | When they are counted 9 | Then the expected result is 10 | 11 | Examples: 12 | | characters | result | 13 | | abc | 294 | 14 | | a b c | 358 | 15 | | a \n b \c | 684 | 16 | -------------------------------------------------------------------------------- /lib/src/gherkin/parameters/string_parameter.dart: -------------------------------------------------------------------------------- 1 | import './custom_parameter.dart'; 2 | 3 | class StringParameterBase extends CustomParameter { 4 | StringParameterBase(String name) 5 | : super(name, RegExp("['\"](.*)['\"]", dotAll: true), 6 | (String input) => input); 7 | } 8 | 9 | class StringParameterLower extends StringParameterBase { 10 | StringParameterLower() : super('string'); 11 | } 12 | 13 | class StringParameterCamel extends StringParameterBase { 14 | StringParameterCamel() : super('String'); 15 | } 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gherkin 2 | version: 2.0.5+2 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 | matcher: ^0.12.10 9 | collection: ^1.15.0 10 | 11 | dev_dependencies: 12 | test: ">=1.16.8" 13 | pedantic: ^1.11.0 14 | glob: ^2.0.1 15 | 16 | environment: 17 | sdk: ">=2.12.0 <3.0.0" 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/debug_information.dart: -------------------------------------------------------------------------------- 1 | class RunnableDebugInformation { 2 | final String filePath; 3 | final int lineNumber; 4 | final String lineText; 5 | 6 | int get nonZeroAdjustedLineNumber => lineNumber + 1; 7 | 8 | RunnableDebugInformation(this.filePath, this.lineNumber, this.lineText); 9 | 10 | RunnableDebugInformation copyWith(int lineNumber, String line) { 11 | return RunnableDebugInformation(filePath, lineNumber, line); 12 | } 13 | 14 | static RunnableDebugInformation EMPTY() { 15 | return RunnableDebugInformation('', 0, ''); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/runnables/language.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/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/syntax/regex_matched_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/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().replaceAll('*', '\\*')).join('|'); 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/language_service_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/languages/language_service.dart'; 3 | 4 | import 'en_dialect_mock.dart'; 5 | 6 | typedef OnStepFinished = void Function(StepFinishedMessage message); 7 | 8 | class LanguageServiceMock extends LanguageService { 9 | String _defaultLangauge = 'en'; 10 | @override 11 | String get defaultLanguage => _defaultLangauge; 12 | 13 | LanguageServiceMock() : super() { 14 | initialise(); 15 | } 16 | 17 | @override 18 | void initialise([String defaultLanguage = 'en']) { 19 | _defaultLangauge = defaultLanguage; 20 | setDialect('en', EnDialectMock()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/empty_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/runnables/taggable_runnable_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/runnable_block.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 3 | 4 | import './debug_information.dart'; 5 | 6 | abstract class TaggableRunnableBlock extends RunnableBlock { 7 | final List _tags = []; 8 | 9 | Iterable get tags => _tags.toList() 10 | ..sort((a, b) => a.debug.lineNumber.compareTo(b.debug.lineNumber)); 11 | 12 | TaggableRunnableBlock(RunnableDebugInformation debug) : super(debug); 13 | 14 | void addTag(TagsRunnable tag) { 15 | _tags.add(tag); 16 | onTagAdded(tag); 17 | } 18 | 19 | void onTagAdded(TagsRunnable tag) {} 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/comment_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/syntax_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/mocks/en_dialect_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | class EnDialectMock extends GherkinDialect { 4 | EnDialectMock() { 5 | name = 'English'; 6 | nativeName = name; 7 | feature = ['Feature']; 8 | background = ['Background']; 9 | rule = ['Rule']; 10 | scenario = ['Scenario']; 11 | scenarioOutline = ['Scenario Outline']; 12 | examples = ['Scenarios', 'Examples']; 13 | given = ['Given', '*']; 14 | when = ['When', '*']; 15 | then = ['Then', '*']; 16 | and = ['And', '*']; 17 | but = ['But', '*']; 18 | 19 | stepKeywords = ([ 20 | ...given, 21 | ...when, 22 | ...then, 23 | ...and, 24 | ...but, 25 | ]).toSet(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/world.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/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/syntax/feature_file_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/runnable.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 4 | 5 | import '../syntax/syntax_matcher.dart'; 6 | 7 | class FeatureFileSyntax extends SyntaxMatcher { 8 | @override 9 | bool get isBlockSyntax => true; 10 | 11 | @override 12 | bool hasBlockEnded(SyntaxMatcher syntax) => false; 13 | 14 | @override 15 | bool isMatch(String line, GherkinDialect dialect) { 16 | return false; 17 | } 18 | 19 | @override 20 | Runnable toRunnable( 21 | String line, 22 | RunnableDebugInformation debug, 23 | GherkinDialect dialect, 24 | ) { 25 | throw UnimplementedError(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/reporters/reporter.dart: -------------------------------------------------------------------------------- 1 | import './message_level.dart'; 2 | import './messages.dart'; 3 | 4 | abstract class Reporter { 5 | Future onTestRunStarted() async {} 6 | Future onTestRunFinished() async {} 7 | Future onFeatureStarted(StartedMessage message) async {} 8 | Future onFeatureFinished(FinishedMessage message) async {} 9 | Future onScenarioStarted(StartedMessage message) async {} 10 | Future onScenarioFinished(ScenarioFinishedMessage message) async {} 11 | Future onStepStarted(StepStartedMessage message) async {} 12 | Future onStepFinished(StepFinishedMessage message) async {} 13 | Future onException(Object exception, StackTrace stackTrace) async {} 14 | Future message(String message, MessageLevel level) async {} 15 | Future dispose() async {} 16 | } 17 | -------------------------------------------------------------------------------- /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, this.expectParameterCount, this.actualParameterCount) 11 | : message = 12 | '$step parameter count mismatch. Expect $expectParameterCount parameters but got $actualParameterCount. ' 13 | 'Ensure you are extending the correct step class which would be ' 14 | "Given${actualParameterCount > 0 ? '$actualParameterCount<${List.generate(actualParameterCount, (i) => "TInputType$i").join(", ")}>' : ''}"; 15 | 16 | @override 17 | String toString() => message; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_run_result.dart: -------------------------------------------------------------------------------- 1 | enum StepExecutionResult { pass, fail, skipped, timeout, error } 2 | 3 | class StepResult { 4 | /// The duration in milliseconds the step took to run 5 | final int elapsedMilliseconds; 6 | 7 | /// The result of executing the step 8 | final StepExecutionResult result; 9 | 10 | // A reason for the result 11 | // This would be a failure message if the result failed. 12 | final String? resultReason; 13 | 14 | StepResult( 15 | this.elapsedMilliseconds, 16 | this.result, [ 17 | this.resultReason, 18 | ]); 19 | } 20 | 21 | class ErroredStepResult extends StepResult { 22 | final Object exception; 23 | final StackTrace stackTrace; 24 | 25 | ErroredStepResult( 26 | int elapsedMilliseconds, 27 | StepExecutionResult result, 28 | this.exception, 29 | this.stackTrace, 30 | ) : super(elapsedMilliseconds, result); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/language_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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 | -------------------------------------------------------------------------------- /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 (var absPath in (absolutePaths as Iterable)) { 19 | var internalMatch = false; 20 | 21 | for (var 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/step_definition_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/steps/step_definition_implementations.dart'; 3 | 4 | typedef OnRunCode = Future Function(Iterable parameters); 5 | 6 | class MockStepDefinition extends StepDefinitionBase { 7 | bool hasRun = false; 8 | int runCount = 0; 9 | final OnRunCode? code; 10 | 11 | MockStepDefinition([ 12 | this.code, 13 | int expectedParameterCount = 0, 14 | ]) : super( 15 | StepDefinitionConfiguration() 16 | ..timeout = const Duration(milliseconds: 200), 17 | expectedParameterCount, 18 | ); 19 | 20 | @override 21 | Future onRun(Iterable parameters) async { 22 | hasRun = true; 23 | runCount += 1; 24 | if (code != null) { 25 | await code!(parameters); 26 | } 27 | } 28 | 29 | @override 30 | RegExp get pattern => RegExp('.'); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/text_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/gherkin/runnables/scenario_expanded_from_outline_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/scenario_type_enum.dart'; 3 | 4 | import './debug_information.dart'; 5 | 6 | class ScenarioExpandedFromOutlineExampleRunnable extends ScenarioRunnable { 7 | String _name; 8 | 9 | @override 10 | ScenarioType get scenarioType => ScenarioType.scenario_outline; 11 | 12 | @override 13 | String get name => _name; 14 | 15 | ScenarioExpandedFromOutlineExampleRunnable( 16 | String name, RunnableDebugInformation debug) 17 | : _name = name, 18 | super(name, debug); 19 | 20 | void setStepParameter(String parameterName, String value) { 21 | _name = _name.replaceAll('<$parameterName>', value); 22 | updateDebugInformation( 23 | debug.copyWith( 24 | debug.lineNumber, 25 | debug.lineText.replaceAll('<$parameterName>', value), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/supporting_files/steps/data_table_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 add the users` 8 | /// | Firstname | Surname | Age | Gender | 9 | /// | Woody | Johnson | 28 | Male | 10 | /// | Edith | Summers | 23 | Female | 11 | /// | Megan | Hill | 83 | Female | 12 | StepDefinitionGeneric GivenIAddTheUsers() { 13 | return given1( 14 | 'I add the users', 15 | (dataTable, _) async { 16 | for (var row in dataTable.rows) { 17 | // do something with row 18 | row.columns.forEach((columnValue) => print(columnValue)); 19 | } 20 | 21 | // or get the table as a map (column values keyed by the header) 22 | final columns = dataTable.asMap(); 23 | final personOne = columns.elementAt(0); 24 | final personOneName = personOne['Firstname']; 25 | print('Name of first person: `$personOneName`'); 26 | }, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] -------------------------------------------------------------------------------- /test/reporters/json_reports/report_7.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "An unnamed feature is possible if something is logged before any feature has started to execute", 4 | "id": null, 5 | "keyword": "Feature", 6 | "line": 0, 7 | "name": "Unnamed feature", 8 | "uri": "unknown", 9 | "elements": [ 10 | { 11 | "keyword": "Scenario", 12 | "type": "scenario", 13 | "id": "null;unnamed", 14 | "name": "Unnamed", 15 | "description": "An unnamed scenario is possible if something is logged before any feature has started to execute", 16 | "line": 0, 17 | "steps": [ 18 | { 19 | "keyword": null, 20 | "name": "Unnamed", 21 | "line": 0, 22 | "match": { 23 | "location": "null:0" 24 | }, 25 | "result": { 26 | "status": "failed", 27 | "duration": 0, 28 | "error_message": "Exception: Test exception" 29 | } 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /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 evalulateExpression(String expression) => 1; 31 | 32 | num getNumericResult() => _results.removeLast(); 33 | 34 | void dispose() { 35 | _cachedNumbers.clear(); 36 | _results.clear(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 './step.dart'; 6 | import 'scenario_type_enum.dart'; 7 | import 'taggable_runnable_block.dart'; 8 | 9 | class ScenarioRunnable extends TaggableRunnableBlock { 10 | final String _name; 11 | List steps = []; 12 | 13 | ScenarioType get scenarioType => ScenarioType.scenario; 14 | 15 | Map metadata = {}; 16 | 17 | ScenarioRunnable( 18 | this._name, 19 | RunnableDebugInformation debug, 20 | ) : super(debug); 21 | 22 | @override 23 | String get name => _name; 24 | 25 | @override 26 | void addChild(Runnable child) { 27 | switch (child.runtimeType) { 28 | case StepRunnable: 29 | steps.add(child as StepRunnable); 30 | break; 31 | case CommentLineRunnable: 32 | case EmptyLineRunnable: 33 | break; 34 | default: 35 | throw Exception("Unknown runnable child given to Scenario '${child.runtimeType}'"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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('', debugInfo); 12 | runnable.addChild(EmptyLineRunnable(debugInfo)); 13 | }); 14 | test('can add StepRunnable', () { 15 | final runnable = ScenarioRunnable('', 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 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/feature_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/runnable.dart'; 5 | import '../runnables/feature.dart'; 6 | import './syntax_matcher.dart'; 7 | import './regex_matched_syntax.dart'; 8 | 9 | class FeatureSyntax extends RegExMatchedGherkinSyntax { 10 | @override 11 | bool get isBlockSyntax => true; 12 | 13 | @override 14 | bool hasBlockEnded(SyntaxMatcher syntax) => false; 15 | 16 | @override 17 | Runnable toRunnable( 18 | String line, 19 | RunnableDebugInformation debug, 20 | GherkinDialect dialect, 21 | ) { 22 | final name = pattern(dialect).firstMatch(line)?.group(1); 23 | final runnable = FeatureRunnable(name!, debug); 24 | return runnable; 25 | } 26 | 27 | @override 28 | RegExp pattern(GherkinDialect dialect) { 29 | final dialectPattern = 30 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.feature); 31 | return RegExp( 32 | '^(?:$dialectPattern):\\s*(.+)\\s*', 33 | multiLine: false, 34 | caseSensitive: false, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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((e) => 24 | e is GherkinSyntaxException && 25 | e.message == 26 | "Only a single table can be added to the example 'Example one'")); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/table_line_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/table.dart'; 5 | import './comment_syntax.dart'; 6 | import './regex_matched_syntax.dart'; 7 | import './syntax_matcher.dart'; 8 | 9 | class TableLineSyntax extends RegExMatchedGherkinSyntax { 10 | @override 11 | RegExp pattern(GherkinDialect dialect) => RegExp( 12 | r'^\s*(\|.*\|)\s*(?:\s*#\s*.*)?$', 13 | multiLine: false, 14 | caseSensitive: false, 15 | ); 16 | 17 | @override 18 | bool get isBlockSyntax => true; 19 | 20 | @override 21 | bool hasBlockEnded(SyntaxMatcher syntax) { 22 | if (syntax is TableLineSyntax || syntax is CommentSyntax) { 23 | return false; 24 | } 25 | 26 | return true; 27 | } 28 | 29 | @override 30 | TableRunnable toRunnable( 31 | String line, 32 | RunnableDebugInformation debug, 33 | GherkinDialect dialect, 34 | ) { 35 | final runnable = TableRunnable(debug); 36 | runnable.rows.add( 37 | pattern(dialect).firstMatch(line.trim())!.group(1)!.trim(), 38 | ); 39 | 40 | return runnable; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/step_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/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 './tags.dart'; 7 | import 'taggable_runnable_block.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 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_5.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | }, 41 | "docString": { 42 | "content_type": "", 43 | "value": "a\nb\nc", 44 | "line": 7 45 | } 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/example_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | import '../runnables/example.dart'; 4 | import '../runnables/debug_information.dart'; 5 | import '../runnables/runnable.dart'; 6 | import './regex_matched_syntax.dart'; 7 | import './syntax_matcher.dart'; 8 | import './table_line_syntax.dart'; 9 | 10 | class ExampleSyntax extends RegExMatchedGherkinSyntax { 11 | @override 12 | RegExp pattern(GherkinDialect dialect) { 13 | final dialectPattern = 14 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.examples); 15 | 16 | return RegExp( 17 | '^\\s*(?:$dialectPattern):(\\s*(.+)\\s*)?\$', 18 | multiLine: false, 19 | caseSensitive: false, 20 | ); 21 | } 22 | 23 | @override 24 | bool get isBlockSyntax => true; 25 | 26 | @override 27 | bool hasBlockEnded(SyntaxMatcher syntax) => syntax is! TableLineSyntax; 28 | 29 | @override 30 | Runnable toRunnable( 31 | String line, 32 | RunnableDebugInformation debug, 33 | GherkinDialect dialect, 34 | ) { 35 | final name = (pattern(dialect).firstMatch(line)?.group(1) ?? '').trim(); 36 | final runnable = ExampleRunnable(name, debug); 37 | 38 | return runnable; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/gherkin/languages/language_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/exceptions/dialect_not_supported.dart'; 2 | 3 | import 'dialect.dart'; 4 | import 'languages.dart'; 5 | 6 | class LanguageService { 7 | String _defaultLanguage = 'en'; 8 | final Map _dialects = {}; 9 | 10 | String get defaultLanguage => _defaultLanguage; 11 | 12 | GherkinDialect getDialect([ 13 | String? languageCode, 14 | ]) { 15 | final code = languageCode ?? _defaultLanguage; 16 | 17 | if (_dialects[code] == null) { 18 | throw GherkinDialogNotSupportedException(code); 19 | } 20 | 21 | return _dialects[code]!; 22 | } 23 | 24 | void initialise([String defaultLanguage = 'en']) { 25 | _defaultLanguage = defaultLanguage; 26 | // final uri = Uri.file('dialects/languages.json'); 27 | // final langFile = File.fromUri(uri); 28 | // Map languagesJson = 29 | // json.decode(langFile.readAsStringSync()); 30 | LANGUAGES_JSON.forEach((key, values) { 31 | final dialect = GherkinDialect.fromJSON(values)..languageCode = key; 32 | setDialect(key, dialect); 33 | }); 34 | } 35 | 36 | void setDialect(String languageCode, GherkinDialect dialect) { 37 | _dialects[languageCode] = dialect; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | expect( 17 | keyword.isMatch( 18 | '#I am also a comment', 19 | EnDialectMock(), 20 | ), 21 | true); 22 | expect( 23 | keyword.isMatch( 24 | '## I am also a comment', 25 | EnDialectMock(), 26 | ), 27 | true); 28 | expect( 29 | keyword.isMatch( 30 | '# Language something', 31 | EnDialectMock(), 32 | ), 33 | true); 34 | }); 35 | 36 | test('does not match', () { 37 | final keyword = CommentSyntax(); 38 | // expect(keyword.isMatch('# language: en'), false); 39 | expect( 40 | keyword.isMatch( 41 | 'I am not a comment', 42 | EnDialectMock(), 43 | ), 44 | false); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/scenario_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | 3 | import '../runnables/debug_information.dart'; 4 | import '../runnables/scenario.dart'; 5 | import './regex_matched_syntax.dart'; 6 | import './syntax_matcher.dart'; 7 | import './tag_syntax.dart'; 8 | import 'scenario_outline_syntax.dart'; 9 | 10 | class ScenarioSyntax extends RegExMatchedGherkinSyntax { 11 | @override 12 | RegExp pattern(GherkinDialect dialect) { 13 | final dialectPattern = 14 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern(dialect.scenario); 15 | 16 | return RegExp( 17 | '^\\s*(?:$dialectPattern):\\s*(.+)\\s*\$', 18 | multiLine: false, 19 | caseSensitive: false, 20 | ); 21 | } 22 | 23 | @override 24 | bool get isBlockSyntax => true; 25 | 26 | @override 27 | bool hasBlockEnded(SyntaxMatcher syntax) => 28 | syntax is ScenarioSyntax || 29 | syntax is ScenarioOutlineSyntax || 30 | syntax is TagSyntax; 31 | 32 | @override 33 | ScenarioRunnable toRunnable( 34 | String line, 35 | RunnableDebugInformation debug, 36 | GherkinDialect dialect, 37 | ) { 38 | final name = pattern(dialect).firstMatch(line)!.group(1)!; 39 | final runnable = ScenarioRunnable(name, debug); 40 | 41 | return runnable; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/background_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/scenario_outline_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/syntax/tag_syntax.dart'; 3 | 4 | import '../runnables/scenario_outline.dart'; 5 | import '../runnables/debug_information.dart'; 6 | import './regex_matched_syntax.dart'; 7 | import './scenario_syntax.dart'; 8 | import './syntax_matcher.dart'; 9 | 10 | class ScenarioOutlineSyntax 11 | extends RegExMatchedGherkinSyntax { 12 | @override 13 | RegExp pattern(GherkinDialect dialect) { 14 | final dialectPattern = 15 | RegExMatchedGherkinSyntax.getMultiDialectRegexPattern( 16 | dialect.scenarioOutline); 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(name.trim(), debug); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | }, 42 | { 43 | "keyword": "Step ", 44 | "name": "2", 45 | "line": 7, 46 | "match": { 47 | "location": "filepath:7" 48 | }, 49 | "result": { 50 | "status": "failed", 51 | "duration": 100000000, 52 | "error_message": "error message" 53 | } 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | final data = 'data'; 9 | final 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, equals(mimeType)); 17 | expect(manager.getAttachmentsForContext('context').length, equals(0)); 18 | }); 19 | 20 | test('saves attachment with context', () async { 21 | final manager = AttachmentManager(); 22 | final data = 'data'; 23 | final mimeType = 'mimeType'; 24 | final context = 'context'; 25 | 26 | manager.attach(data, mimeType, context); 27 | 28 | expect(manager.getAttachmentsForContext(context).length, equals(1)); 29 | expect( 30 | manager.getAttachmentsForContext(context).first.data, equals(data)); 31 | expect(manager.getAttachmentsForContext(context).first.mimeType, 32 | equals(mimeType)); 33 | expect(manager.getAttachmentsForContext(context).first.context, 34 | equals(context)); 35 | expect(manager.getAttachmentsForContext().length, equals(0)); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/io/io_feature_file_accessor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:path/path.dart'; 4 | 5 | import 'package:gherkin/src/io/feature_file_matcher.dart'; 6 | 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) async { 20 | return await File(path).readAsString(encoding: encoding); 21 | } 22 | 23 | @override 24 | Future> listFiles(Pattern pattern) async { 25 | return await _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.matchAsPrefix(relativePath); 47 | if (match?.group(0) == relativePath) { 48 | result.add(item.path); 49 | } 50 | } 51 | }, 52 | ); 53 | 54 | return result; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/feature_file.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 2 | 3 | import './debug_information.dart'; 4 | import './empty_line.dart'; 5 | import './feature.dart'; 6 | import './language.dart'; 7 | import './runnable.dart'; 8 | import './runnable_block.dart'; 9 | import 'comment_line.dart'; 10 | 11 | class FeatureFile extends RunnableBlock { 12 | String _language = 'en'; 13 | final List _tagsPendingAssignmentToChild = []; 14 | 15 | List features = []; 16 | 17 | FeatureFile(RunnableDebugInformation debug) : super(debug); 18 | 19 | String get language => _language; 20 | 21 | @override 22 | void addChild(Runnable child) { 23 | switch (child.runtimeType) { 24 | case LanguageRunnable: 25 | _language = (child as LanguageRunnable).language; 26 | break; 27 | case TagsRunnable: 28 | _tagsPendingAssignmentToChild.add(child as TagsRunnable); 29 | break; 30 | case FeatureRunnable: 31 | features.add(child as FeatureRunnable); 32 | if (_tagsPendingAssignmentToChild.isNotEmpty) { 33 | _tagsPendingAssignmentToChild.forEach((t) => (child).addTag(t)); 34 | _tagsPendingAssignmentToChild.clear(); 35 | } 36 | break; 37 | case CommentLineRunnable: 38 | case EmptyLineRunnable: 39 | break; 40 | default: 41 | throw Exception( 42 | "Unknown runnable child given to FeatureFile '${child.runtimeType}'"); 43 | } 44 | } 45 | 46 | @override 47 | String get name => debug.filePath; 48 | } 49 | -------------------------------------------------------------------------------- /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 | expect( 17 | keyword.isMatch( 18 | ' ', 19 | EnDialectMock(), 20 | ), 21 | true); 22 | expect( 23 | keyword.isMatch( 24 | ' ', 25 | EnDialectMock(), 26 | ), 27 | true); 28 | expect( 29 | keyword.isMatch( 30 | ' ', 31 | EnDialectMock(), 32 | ), 33 | true); 34 | }); 35 | 36 | test('does not match', () { 37 | final keyword = EmptyLineSyntax(); 38 | expect( 39 | keyword.isMatch( 40 | 'a', 41 | EnDialectMock(), 42 | ), 43 | false); 44 | expect( 45 | keyword.isMatch( 46 | ' b', 47 | EnDialectMock(), 48 | ), 49 | false); 50 | expect( 51 | keyword.isMatch( 52 | ' c', 53 | EnDialectMock(), 54 | ), 55 | false); 56 | expect( 57 | keyword.isMatch( 58 | ' ,', 59 | EnDialectMock(), 60 | ), 61 | false); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_6.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 9 | "tags": [ 10 | { 11 | "line": 2, 12 | "name": "tag1" 13 | } 14 | ], 15 | "elements": [ 16 | { 17 | "keyword": "Scenario Outline", 18 | "type": "scenario", 19 | "id": "feature 1;scenario outline 1", 20 | "name": "Scenario Outline 1", 21 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | }, 42 | { 43 | "keyword": "Step ", 44 | "name": "2", 45 | "line": 7, 46 | "match": { 47 | "location": "filepath:7" 48 | }, 49 | "result": { 50 | "status": "passed", 51 | "duration": 100000000 52 | }, 53 | "embeddings": [ 54 | { 55 | "mime_type": "mimetype", 56 | "data": "data" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ] -------------------------------------------------------------------------------- /lib/src/gherkin/languages/dialect.dart: -------------------------------------------------------------------------------- 1 | class GherkinDialect { 2 | late String name; 3 | late String nativeName; 4 | late String languageCode; 5 | late Iterable feature; 6 | late Iterable background; 7 | late Iterable rule; 8 | late Iterable scenario; 9 | late Iterable scenarioOutline; 10 | late Iterable examples; 11 | late Iterable given; 12 | late Iterable when; 13 | late Iterable then; 14 | late Iterable and; 15 | late Iterable but; 16 | late Set stepKeywords; 17 | 18 | static GherkinDialect fromJSON(Map map) { 19 | final dialect = GherkinDialect(); 20 | dialect.name = map['name']; 21 | dialect.nativeName = map['native']; 22 | dialect.feature = map['feature'] as List; 23 | dialect.background = map['background'] as List; 24 | dialect.rule = map['rule'] as List; 25 | dialect.scenario = map['scenario'] as List; 26 | dialect.scenarioOutline = map['scenarioOutline'] as List; 27 | dialect.examples = map['examples'] as List; 28 | dialect.given = map['given'] as List; 29 | dialect.when = map['when'] as List; 30 | dialect.then = map['then'] as List; 31 | dialect.and = map['and'] as List; 32 | dialect.but = map['but'] as List; 33 | 34 | dialect.stepKeywords = ([ 35 | ...dialect.given, 36 | ...dialect.when, 37 | ...dialect.then, 38 | ...dialect.and, 39 | ...dialect.but 40 | ]).toSet(); 41 | 42 | return dialect; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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/reporters/json_reports/report_4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | }, 42 | { 43 | "keyword": "Step ", 44 | "name": "2", 45 | "line": 7, 46 | "match": { 47 | "location": "filepath:7" 48 | }, 49 | "result": { 50 | "status": "failed", 51 | "duration": 100000000, 52 | "error_message": "error message" 53 | }, 54 | "embeddings": [ 55 | { 56 | "mime_type": "mimetype", 57 | "data": "data" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] -------------------------------------------------------------------------------- /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 | expect( 19 | keyword.isMatch( 20 | 'Feature:one', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | }); 25 | 26 | test('does not match', () { 27 | final keyword = FeatureSyntax(); 28 | expect( 29 | keyword.isMatch( 30 | '#Feature: no', 31 | EnDialectMock(), 32 | ), 33 | false); 34 | expect( 35 | keyword.isMatch( 36 | '# Feature no', 37 | EnDialectMock(), 38 | ), 39 | false); 40 | }); 41 | }); 42 | 43 | group('toRunnable', () { 44 | test('creates FeatureRunnable', () { 45 | final keyword = FeatureSyntax(); 46 | final runnable = keyword.toRunnable( 47 | 'Feature: A feature 123', 48 | RunnableDebugInformation.EMPTY(), 49 | EnDialectMock(), 50 | ); 51 | expect(runnable, isNotNull); 52 | expect(runnable, predicate((x) => x is FeatureRunnable)); 53 | expect(runnable.name, equals('A feature 123')); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /example/test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:gherkin/gherkin.dart'; 4 | import 'supporting_files/hooks/hook_example.dart'; 5 | import 'supporting_files/parameters/power_of_two.parameter.dart'; 6 | import 'supporting_files/steps/given_the_characters.step.dart'; 7 | import 'supporting_files/steps/given_the_numbers.step.dart'; 8 | import 'supporting_files/steps/given_the_powers_of_two.step.dart'; 9 | import 'supporting_files/steps/then_expect_numeric_result.step.dart'; 10 | import 'supporting_files/steps/when_numbers_are_added.step.dart'; 11 | import 'supporting_files/steps/when_the_characters_are_counted.step.dart'; 12 | import 'supporting_files/worlds/custom_world.world.dart'; 13 | 14 | String buildFeaturesPathRegex() { 15 | // '\' must be escaped, '/' must not be escaped: 16 | var featuresPath = (Platform.isWindows) 17 | ? 'features${Platform.pathSeparator}\\.*\.feature' 18 | : 'features${Platform.pathSeparator}.*\.feature'; 19 | 20 | return featuresPath; 21 | } 22 | 23 | Future main() { 24 | final steps = [ 25 | GivenTheNumbers(), 26 | GivenThePowersOfTwo(), 27 | GivenTheCharacters(), 28 | WhenTheStoredNumbersAreAdded(), 29 | WhenTheCharactersAreCounted(), 30 | ThenExpectNumericResult() 31 | ]; 32 | final featuresPath = buildFeaturesPathRegex(); 33 | final config = TestConfiguration.DEFAULT(steps) 34 | ..features = [RegExp(featuresPath)] 35 | ..tagExpression = 'not @skip' 36 | ..hooks = [HookExample()] 37 | ..customStepParameterDefinitions = [PowerOfTwoParameter()] 38 | ..createWorld = 39 | (TestConfiguration config) => Future.value(CalculatorWorld()); 40 | 41 | return GherkinRunner().execute(config); 42 | } 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/multi_line_string.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/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 | -------------------------------------------------------------------------------- /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 | expect( 19 | syntax.isMatch( 20 | ' Scenario: something', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | }); 25 | 26 | test('does not match', () { 27 | final syntax = ScenarioSyntax(); 28 | expect( 29 | syntax.isMatch( 30 | 'Scenario something', 31 | EnDialectMock(), 32 | ), 33 | false); 34 | expect( 35 | syntax.isMatch( 36 | '#Scenario: something', 37 | EnDialectMock(), 38 | ), 39 | false); 40 | }); 41 | }); 42 | 43 | group('toRunnable', () { 44 | test('creates FeatureRunnable', () { 45 | final keyword = ScenarioSyntax(); 46 | var keyword2 = keyword; 47 | final runnable = keyword2.toRunnable( 48 | 'Scenario: A scenario 123', 49 | RunnableDebugInformation.EMPTY(), 50 | EnDialectMock(), 51 | ); 52 | expect(runnable, isNotNull); 53 | expect(runnable, predicate((x) => x is ScenarioRunnable)); 54 | expect(runnable.name, equals('A scenario 123')); 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /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/syntax/tag_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/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/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 | GherkinTable toTable() { 31 | TableRow? header; 32 | final tableRows = []; 33 | if (rows.length > 1) { 34 | header = _toRow(rows.first, 0, true); 35 | } 36 | 37 | for (var i = (header == null ? 0 : 1); i < rows.length; i += 1) { 38 | tableRows.add(_toRow(rows.elementAt(i), i)); 39 | } 40 | 41 | return GherkinTable(tableRows, header); 42 | } 43 | 44 | TableRow _toRow(String raw, int rowIndex, [isHeaderRow = false]) { 45 | final columns = raw 46 | .trim() 47 | .split(RegExp(r'(? c.trim().replaceAll(r'\|', '|')) 49 | .map((c) => c.isEmpty ? null : c) 50 | .skip(1) 51 | .toList(); 52 | 53 | return TableRow( 54 | columns.take(columns.length - 1).toList( 55 | growable: false, 56 | ), 57 | rowIndex, 58 | isHeaderRow, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | }, 42 | { 43 | "keyword": "Step ", 44 | "name": "2", 45 | "line": 7, 46 | "match": { 47 | "location": "filepath:7" 48 | }, 49 | "result": { 50 | "status": "failed", 51 | "duration": 100000000, 52 | "error_message": "error message" 53 | } 54 | }, 55 | { 56 | "keyword": "Step ", 57 | "name": "3", 58 | "line": 8, 59 | "match": { 60 | "location": "filepath:8" 61 | }, 62 | "result": { 63 | "status": "skipped", 64 | "duration": 100000000 65 | } 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | ] -------------------------------------------------------------------------------- /example/supporting_files/hooks/hook_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | class HookExample extends Hook { 4 | /// The priority to assign to this hook. 5 | /// Higher priority gets run first so a priority of 10 is run before a priority of 2 6 | @override 7 | int get priority => 1; 8 | 9 | /// Run before any scenario in a test run have executed 10 | @override 11 | Future onBeforeRun(TestConfiguration config) async { 12 | print('before run hook'); 13 | } 14 | 15 | /// Run after all scenarios in a test run have completed 16 | @override 17 | Future onAfterRun(TestConfiguration config) async { 18 | print('after run hook'); 19 | } 20 | 21 | /// Run before a scenario and it steps are executed 22 | @override 23 | Future onBeforeScenario( 24 | TestConfiguration config, 25 | String scenario, 26 | Iterable tags, 27 | ) async { 28 | print("running hook before scenario '$scenario'"); 29 | } 30 | 31 | /// Run after a scenario has executed 32 | @override 33 | Future onAfterScenario( 34 | TestConfiguration config, 35 | String scenario, 36 | Iterable tags, 37 | ) async { 38 | print("running hook after scenario '$scenario'"); 39 | } 40 | 41 | /// Run before a step is executed 42 | @override 43 | Future onBeforeStep(World world, String step) async { 44 | print("running hook before step '$step'"); 45 | } 46 | 47 | /// Run after a step has executed 48 | @override 49 | Future onAfterStep( 50 | World world, String step, StepResult stepResult) async { 51 | print("running hook after step '$step'"); 52 | 53 | // example of how to add a simple attachment (text, json, image) to a step that a reporter can use 54 | world.attach('attachment data', 'text/plain', step); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | expect( 19 | syntax.isMatch( 20 | '@tag', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | expect( 25 | syntax.isMatch( 26 | '@tag one', 27 | EnDialectMock(), 28 | ), 29 | true); 30 | }); 31 | 32 | test('does not match', () { 33 | final syntax = TagSyntax(); 34 | expect( 35 | syntax.isMatch( 36 | 'not a tag', 37 | EnDialectMock(), 38 | ), 39 | false); 40 | expect( 41 | syntax.isMatch( 42 | '#@tag @tag2', 43 | EnDialectMock(), 44 | ), 45 | false); 46 | }); 47 | }); 48 | 49 | group('toRunnable', () { 50 | test('creates TextLineRunnable', () { 51 | final syntax = TagSyntax(); 52 | final runnable = syntax.toRunnable( 53 | '@tag1 @tag2 @tag3@tag_4', 54 | RunnableDebugInformation.EMPTY(), 55 | EnDialectMock(), 56 | ); 57 | expect(runnable, isNotNull); 58 | expect(runnable, predicate((x) => x is TagsRunnable)); 59 | expect(runnable.tags, equals(['@tag1', '@tag2', '@tag3', '@tag_4'])); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_scenario.dart: -------------------------------------------------------------------------------- 1 | import '../messages.dart'; 2 | import 'json_feature.dart'; 3 | import 'json_step.dart'; 4 | import 'json_tag.dart'; 5 | 6 | class JsonScenario { 7 | late final Target target; 8 | late final String name; 9 | late final String description; 10 | late final int line; 11 | late final List steps; 12 | late final Iterable tags; 13 | JsonFeature? feature; 14 | 15 | static JsonScenario from(StartedMessage message) { 16 | final scenario = JsonScenario(); 17 | scenario.target = message.target; 18 | scenario.name = message.name; 19 | scenario.description = ''; 20 | scenario.line = message.context.nonZeroAdjustedLineNumber; 21 | scenario.tags = message.tags 22 | .where((t) => !t.isInherited) 23 | .map((t) => JsonTag(t.name, t.nonZeroAdjustedLineNumber)) 24 | .toList(); 25 | 26 | scenario.steps = []; 27 | 28 | return scenario; 29 | } 30 | 31 | void add(JsonStep step) { 32 | steps.add(step); 33 | } 34 | 35 | JsonStep currentStep() { 36 | if (steps.isEmpty) { 37 | final step = JsonStep() 38 | ..name = 'Unnamed' 39 | ..line = 0; 40 | 41 | steps.add(step); 42 | } 43 | 44 | return steps.last; 45 | } 46 | 47 | Map toJson() { 48 | final result = { 49 | 'keyword': 50 | target == Target.scenario_outline ? 'Scenario Outline' : 'Scenario', 51 | 'type': 'scenario', 52 | 'id': '${feature?.id};${name.toLowerCase()}', 53 | 'name': name, 54 | 'description': description, 55 | 'line': line, 56 | }; 57 | 58 | if (tags.isNotEmpty) { 59 | result['tags'] = tags.toList(); 60 | } 61 | 62 | if (steps.isNotEmpty) { 63 | result['steps'] = steps.toList(); 64 | } 65 | 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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(parameter.pattern.hasMatch('\'Jon Samwell is a devloper\''), 24 | equals(true)); 25 | }); 26 | 27 | test('{String} pattern matches correctly with new line within string', () { 28 | final parameter = StringParameterCamel(); 29 | expect(parameter.pattern.hasMatch('\'Jon Samwell is a \n devloper\''), 30 | equals(true)); 31 | }); 32 | 33 | test('{String} pattern matches correctly with non alpha characters', () { 34 | final parameter = StringParameterCamel(); 35 | expect( 36 | parameter.pattern.hasMatch( 37 | "\'Jon Samwell is a devloper 123 \'!@%^&*()_=+#:';{}\'"), 38 | equals(true)); 39 | }); 40 | 41 | test('{String} parsed correctly with newline character in it', () { 42 | final parameter = StringParameterCamel(); 43 | expect(parameter.pattern.hasMatch('\'Jon \n Sam well\''), equals(true)); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /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 | table = (child as TableRunnable).toTable(); 34 | break; 35 | default: 36 | throw Exception( 37 | "Unknown runnable child given to Step '${child.runtimeType}'"); 38 | } 39 | } 40 | 41 | void setStepParameter(String parameterName, String value) { 42 | _name = _name.replaceAll('<$parameterName>', value); 43 | table?.setStepParameter(parameterName, value); 44 | updateDebugInformation( 45 | debug.copyWith( 46 | debug.lineNumber, 47 | debug.lineText.replaceAll('<$parameterName>', value), 48 | ), 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /lib/src/gherkin/syntax/multiline_string_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/languages/dialect.dart'; 2 | import 'package:gherkin/src/gherkin/syntax/empty_line_syntax.dart'; 3 | 4 | import '../runnables/debug_information.dart'; 5 | import '../runnables/multi_line_string.dart'; 6 | import '../exceptions/syntax_error.dart'; 7 | import './comment_syntax.dart'; 8 | import './regex_matched_syntax.dart'; 9 | import './syntax_matcher.dart'; 10 | import './text_line_syntax.dart'; 11 | 12 | class MultilineStringSyntax 13 | extends RegExMatchedGherkinSyntax { 14 | @override 15 | RegExp pattern(GherkinDialect dialect) => RegExp( 16 | r'^\s*(' 17 | '"""' 18 | r"|'''|```)\s*$", 19 | multiLine: false, 20 | caseSensitive: false, 21 | ); 22 | 23 | @override 24 | bool get isBlockSyntax => true; 25 | 26 | @override 27 | bool hasBlockEnded(SyntaxMatcher syntax) { 28 | if (syntax is MultilineStringSyntax) { 29 | return true; 30 | } else if (!(syntax is TextLineSyntax || 31 | syntax is CommentSyntax || 32 | syntax is EmptyLineSyntax)) { 33 | throw GherkinSyntaxException( 34 | 'Multiline string block does not expect ${syntax.runtimeType} syntax. Expects a text line'); 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/hooks/hook.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 2 | 3 | import '../gherkin/steps/step_run_result.dart'; 4 | import '../gherkin/steps/world.dart'; 5 | import '../reporters/messages.dart'; 6 | import '../configuration.dart'; 7 | 8 | /// A hook that is run during certain points in the execution cycle 9 | /// You can override any or none of the methods 10 | abstract class Hook { 11 | /// The priority to assign to this hook. 12 | /// Higher priority gets run first so a priority of 10 is run before a priority of 2 13 | int get priority => 0; 14 | 15 | /// Run before any scenario in a test run have executed 16 | Future onBeforeRun(TestConfiguration config) => Future.value(null); 17 | 18 | /// Run after all scenarios in a test run have completed 19 | Future onAfterRun(TestConfiguration config) => Future.value(null); 20 | 21 | /// Run after the scenario world is created but run before a scenario and its steps are executed 22 | /// Might not be invoked if there is not a world object 23 | Future onAfterScenarioWorldCreated( 24 | World world, 25 | ScenarioRunnable scenario, 26 | Iterable tags, 27 | ) => 28 | Future.value(null); 29 | 30 | /// Run before a scenario and it steps are executed 31 | Future onBeforeScenario( 32 | TestConfiguration config, 33 | String scenario, 34 | Iterable tags, 35 | ) => 36 | Future.value(null); 37 | 38 | /// Run after a scenario has executed 39 | Future onAfterScenario( 40 | TestConfiguration config, 41 | String scenario, 42 | Iterable tags, 43 | ) => 44 | Future.value(null); 45 | 46 | /// Run before a step is executed 47 | Future onBeforeStep(World world, String step) => Future.value(null); 48 | 49 | /// Run after a step has executed 50 | Future onAfterStep(World world, ScenarioRunnable scenario, String step, StepResult stepResult) => 51 | Future.value(null); 52 | } 53 | -------------------------------------------------------------------------------- /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 | expect( 19 | syntax.isMatch( 20 | 'Scenario outline: ', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | expect( 25 | syntax.isMatch( 26 | 'Scenario outline: something', 27 | EnDialectMock(), 28 | ), 29 | true); 30 | expect( 31 | syntax.isMatch( 32 | ' Scenario Outline: something', 33 | EnDialectMock(), 34 | ), 35 | true); 36 | }); 37 | 38 | test('does not match', () { 39 | final syntax = ScenarioOutlineSyntax(); 40 | expect( 41 | syntax.isMatch( 42 | 'Scenario outline something', 43 | EnDialectMock(), 44 | ), 45 | false); 46 | expect( 47 | syntax.isMatch( 48 | '#Scenario Outline: something', 49 | EnDialectMock(), 50 | ), 51 | false); 52 | }); 53 | }); 54 | 55 | group('toRunnable', () { 56 | test('creates FeatureRunnable', () { 57 | final keyword = ScenarioOutlineSyntax(); 58 | final runnable = keyword.toRunnable( 59 | 'Scenario Outline: A scenario outline 123', 60 | RunnableDebugInformation.EMPTY(), 61 | EnDialectMock(), 62 | ); 63 | expect(runnable, isNotNull); 64 | expect(runnable, predicate((x) => x is ScenarioRunnable)); 65 | expect(runnable.name, equals('A scenario outline 123')); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/reporters/messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/attachments/attachment.dart'; 2 | 3 | import '../gherkin/models/table.dart'; 4 | import '../gherkin/runnables/debug_information.dart'; 5 | import '../gherkin/steps/step_run_result.dart'; 6 | 7 | enum Target { run, feature, scenario, scenario_outline, step } 8 | 9 | class Tag { 10 | final String name; 11 | final int lineNumber; 12 | final bool isInherited; 13 | 14 | int get nonZeroAdjustedLineNumber => lineNumber + 1; 15 | 16 | Tag( 17 | this.name, 18 | this.lineNumber, [ 19 | this.isInherited = false, 20 | ]); 21 | } 22 | 23 | class StartedMessage { 24 | final Target target; 25 | final String name; 26 | final RunnableDebugInformation context; 27 | final Iterable tags; 28 | 29 | StartedMessage( 30 | this.target, 31 | this.name, 32 | this.context, 33 | this.tags, 34 | ); 35 | } 36 | 37 | class FinishedMessage { 38 | final Target target; 39 | final String name; 40 | final RunnableDebugInformation context; 41 | 42 | FinishedMessage(this.target, this.name, this.context); 43 | } 44 | 45 | class StepStartedMessage extends StartedMessage { 46 | final GherkinTable? table; 47 | final String? multilineString; 48 | 49 | StepStartedMessage( 50 | String name, 51 | RunnableDebugInformation context, { 52 | this.table, 53 | this.multilineString, 54 | }) : super( 55 | Target.step, 56 | name, 57 | context, 58 | Iterable.empty(), 59 | ); 60 | } 61 | 62 | class StepFinishedMessage extends FinishedMessage { 63 | final StepResult result; 64 | final Iterable attachments; 65 | 66 | StepFinishedMessage( 67 | String name, 68 | RunnableDebugInformation context, 69 | this.result, [ 70 | this.attachments = const Iterable.empty(), 71 | ]) : super(Target.step, name, context); 72 | } 73 | 74 | class ScenarioFinishedMessage extends FinishedMessage { 75 | final bool passed; 76 | 77 | ScenarioFinishedMessage( 78 | String name, RunnableDebugInformation context, this.passed) 79 | : super(Target.scenario, name, context); 80 | } 81 | -------------------------------------------------------------------------------- /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 | expect( 19 | keyword.isMatch( 20 | '#language: fr', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | expect( 25 | keyword.isMatch( 26 | '#language:de', 27 | EnDialectMock(), 28 | ), 29 | true); 30 | expect( 31 | keyword.isMatch( 32 | '#language:en-au', 33 | EnDialectMock(), 34 | ), 35 | true); 36 | expect( 37 | keyword.isMatch( 38 | '#language:en-Scouse', 39 | EnDialectMock(), 40 | ), 41 | true); 42 | }); 43 | 44 | test('does not match', () { 45 | final keyword = LanguageSyntax(); 46 | expect( 47 | keyword.isMatch( 48 | '#language no', 49 | EnDialectMock(), 50 | ), 51 | false); 52 | expect( 53 | keyword.isMatch( 54 | '# language comment', 55 | EnDialectMock(), 56 | ), 57 | false); 58 | }); 59 | }); 60 | 61 | group('toRunnable', () { 62 | test('creates LanguageRunnable', () { 63 | final keyword = LanguageSyntax(); 64 | final runnable = keyword.toRunnable( 65 | '# language: de', 66 | RunnableDebugInformation.EMPTY(), 67 | EnDialectMock(), 68 | ); 69 | expect(runnable, isNotNull); 70 | expect(runnable, predicate((x) => x is LanguageRunnable)); 71 | expect(runnable.language, equals('de')); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/gherkin/models/table.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../models/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 | rows.forEach( 19 | (row) { 20 | row.setStepParameter(parameterName, value); 21 | }, 22 | ); 23 | } 24 | 25 | /// Returns the table as a iterable of maps. With a single map representing a row in the table 26 | /// keyed by the column name if a header row is present else the column index (as a string) 27 | Iterable> asMap() { 28 | return >[ 29 | ...rows.map( 30 | (row) { 31 | final map = {}; 32 | if (header != null) { 33 | for (var i = 0; i < header!.columns.length; i += 1) { 34 | map[header!.columns.toList().elementAt(i)!] = 35 | row.columns.toList().length > i 36 | ? row.columns.elementAt(i) 37 | : null; 38 | } 39 | } else { 40 | for (var i = 0; i < row.columns.length; i += 1) { 41 | map[i.toString()] = 42 | row.columns.length > i ? row.columns.elementAt(i) : null; 43 | } 44 | } 45 | 46 | return map; 47 | }, 48 | ) 49 | ]; 50 | } 51 | 52 | String toJson() { 53 | return '${jsonEncode(asMap())}'; 54 | } 55 | 56 | static GherkinTable fromJson(String json) { 57 | final data = (jsonDecode(json) as List) 58 | .map((x) => x as Map); 59 | final headerRow = 60 | data.isNotEmpty ? TableRow(data.first.keys, 1, true) : null; 61 | final rows = data.map((x) => TableRow(x.values.cast(), 1, false)); 62 | final table = GherkinTable(rows, headerRow); 63 | 64 | return table; 65 | } 66 | 67 | GherkinTable clone() => GherkinTable( 68 | rows.map((r) => r.clone()).toList(), 69 | header?.clone(), 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /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/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 | final indexer = IoFeatureFileAccessor(); 10 | 11 | group('with RegExp', () { 12 | test('does not list directories', () async { 13 | expect( 14 | await indexer.listFiles(RegExp(r'test_resources')), 15 | [], 16 | ); 17 | }); 18 | 19 | test('does not throw error for weird paths', () async { 20 | expect( 21 | await indexer.listFiles(RegExp(r'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 | final 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 | final indexer = IoFeatureFileAccessor(); 72 | 73 | expect( 74 | () async => await indexer.read('nonexistentpath'), 75 | throwsA(TypeMatcher()), 76 | ); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/reporters/stdout_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import './message_level.dart'; 3 | import './reporter.dart'; 4 | 5 | class StdoutReporter extends Reporter { 6 | final MessageLevel _logLevel; 7 | late void Function(String text) _writeln; 8 | late void Function(String text) _write; 9 | 10 | static const String NEUTRAL_COLOR = '\u001b[33;34m'; // blue 11 | static const String DEBUG_COLOR = '\u001b[1;30m'; // gray 12 | static const String FAIL_COLOR = '\u001b[33;31m'; // red 13 | static const String WARN_COLOR = '\u001b[33;10m'; // yellow 14 | static const String RESET_COLOR = '\u001b[33;0m'; 15 | static const String PASS_COLOR = '\u001b[33;32m'; // green 16 | 17 | StdoutReporter([ 18 | this._logLevel = MessageLevel.verbose, 19 | ]) { 20 | _writeln = (text) => stdout.writeln(text); 21 | _write = (text) => stdout.write(text); 22 | } 23 | 24 | void setWriteLineFn(void Function(String text) fn) { 25 | _writeln = fn; 26 | } 27 | 28 | void setWriteFn(void Function(String text) fn) { 29 | _write = fn; 30 | } 31 | 32 | @override 33 | Future message(String message, MessageLevel level) async { 34 | if (level.index >= _logLevel.index) { 35 | printMessageLine(message, getColour(level)); 36 | } 37 | } 38 | 39 | @override 40 | Future onException(Object exception, StackTrace stackTrace) async { 41 | printMessageLine(exception.toString(), getColour(MessageLevel.error)); 42 | } 43 | 44 | String getColour(MessageLevel level) { 45 | switch (level) { 46 | case MessageLevel.verbose: 47 | case MessageLevel.debug: 48 | return DEBUG_COLOR; 49 | case MessageLevel.error: 50 | return FAIL_COLOR; 51 | case MessageLevel.warning: 52 | return WARN_COLOR; 53 | case MessageLevel.info: 54 | default: 55 | return NEUTRAL_COLOR; 56 | } 57 | } 58 | 59 | void printMessageLine( 60 | String message, [ 61 | String? colour, 62 | ]) { 63 | _writeln('${colour ?? RESET_COLOR}$message$RESET_COLOR'); 64 | } 65 | 66 | void printMessage( 67 | String message, [ 68 | String? colour, 69 | ]) { 70 | _write('${colour ?? RESET_COLOR}$message$RESET_COLOR'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_feature.dart: -------------------------------------------------------------------------------- 1 | import '../messages.dart'; 2 | import 'json_scenario.dart'; 3 | import 'json_step.dart'; 4 | import 'json_tag.dart'; 5 | 6 | class JsonFeature { 7 | late final String uri; 8 | late final String name; 9 | late final String description; 10 | late final int line; 11 | String? id; 12 | Iterable? tags; 13 | List? scenarios; 14 | 15 | static JsonFeature from(StartedMessage message) { 16 | final feature = JsonFeature(); 17 | feature.uri = message.context.filePath; 18 | feature.id = message.name.toLowerCase(); 19 | feature.name = message.name; 20 | feature.description = ''; 21 | feature.line = message.context.nonZeroAdjustedLineNumber; 22 | feature.tags = 23 | message.tags.map((t) => JsonTag(t.name, t.nonZeroAdjustedLineNumber)); 24 | 25 | return feature; 26 | } 27 | 28 | void add( 29 | JsonScenario scenario, 30 | ) { 31 | scenario.feature = this; 32 | scenarios ??= []; 33 | scenarios?.add(scenario); 34 | } 35 | 36 | JsonScenario currentScenario() { 37 | if (scenarios == null || scenarios!.isEmpty) { 38 | final scenario = JsonScenario() 39 | ..target = Target.scenario 40 | ..name = 'Unnamed' 41 | ..description = 42 | 'An unnamed scenario is possible if something is logged before any scenario steps have started to execute' 43 | ..line = 0 44 | ..tags = [] 45 | ..steps = [ 46 | JsonStep() 47 | ..name = 'Unnamed' 48 | ..line = 0 49 | ]; 50 | 51 | scenarios = (scenarios ?? [])..add(scenario); 52 | } 53 | 54 | return scenarios!.last; 55 | } 56 | 57 | Map toJson() { 58 | final result = { 59 | 'description': description, 60 | 'id': id, 61 | 'keyword': 'Feature', 62 | 'line': line, 63 | 'name': name, 64 | 'uri': uri, 65 | }; 66 | 67 | if (tags != null && tags!.isNotEmpty) { 68 | result['tags'] = tags!.toList(); 69 | } 70 | 71 | if (scenarios != null && scenarios!.isNotEmpty) { 72 | result['elements'] = scenarios!.toList(); 73 | } 74 | 75 | return result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/mocks/hook_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 3 | 4 | typedef OnBeforeRunCode = void Function(); 5 | 6 | class HookMock extends Hook { 7 | int onBeforeRunInvocationCount = 0; 8 | int onAfterRunInvocationCount = 0; 9 | int onBeforeScenarioInvocationCount = 0; 10 | int onBeforeStepInvocationCount = 0; 11 | int onAfterScenarioInvocationCount = 0; 12 | int onAfterScenarioWorldCreatedInvocationCount = 0; 13 | int onAfterStepInvocationCount = 0; 14 | late List? onBeforeScenarioTags; 15 | late List? onAfterScenarioTags; 16 | 17 | final int providedPriority; 18 | final OnBeforeRunCode? onBeforeRunCode; 19 | 20 | @override 21 | int get priority => providedPriority; 22 | 23 | HookMock({this.onBeforeRunCode, this.providedPriority = 0}); 24 | 25 | @override 26 | Future onBeforeRun(TestConfiguration config) async { 27 | onBeforeRunInvocationCount += 1; 28 | if (onBeforeRunCode != null) { 29 | onBeforeRunCode!(); 30 | } 31 | } 32 | 33 | @override 34 | Future onAfterRun(TestConfiguration config) async => onAfterRunInvocationCount += 1; 35 | 36 | @override 37 | Future onBeforeScenario( 38 | TestConfiguration config, 39 | String scenario, 40 | Iterable tags, 41 | ) async { 42 | onBeforeScenarioTags = tags.toList(); 43 | onBeforeScenarioInvocationCount += 1; 44 | } 45 | 46 | @override 47 | Future onBeforeStep(World world, String step) async => onBeforeStepInvocationCount += 1; 48 | 49 | @override 50 | Future onAfterScenario( 51 | TestConfiguration config, 52 | String scenario, 53 | Iterable tags, 54 | ) async { 55 | onAfterScenarioTags = tags.toList(); 56 | onAfterScenarioInvocationCount += 1; 57 | } 58 | 59 | @override 60 | Future onAfterScenarioWorldCreated( 61 | World world, 62 | ScenarioRunnable scenario, 63 | Iterable tags, 64 | ) async => 65 | onAfterScenarioWorldCreatedInvocationCount += 1; 66 | 67 | @override 68 | Future onAfterStep(World world, ScenarioRunnable scenario, String step, StepResult result) async { 69 | onAfterStepInvocationCount += 1; 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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(value) => 8 | StringDescription().addDescriptionOf(value).toString(); 9 | 10 | String formatFailure( 11 | Matcher expected, 12 | 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 | -------------------------------------------------------------------------------- /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(evaluator.evaluate('@a and ((@b or not @e) or (@b and @c))', tags), 40 | true); 41 | expect( 42 | evaluator.evaluate( 43 | '@a and ((@b and not @e) and (@b and @c))', ['a', 'b', 'c', 'e']), 44 | false); 45 | expect( 46 | evaluator.evaluate('@a and ((@b or not @e) and (@b and @c))', tags), 47 | true); 48 | }); 49 | 50 | test('evaluate single negated tag expression correctly', () async { 51 | final evaluator = TagExpressionEvaluator(); 52 | final tags = ['@skip']; 53 | final tags1 = ['@ignore']; 54 | 55 | expect(evaluator.evaluate('not @skip', tags), false); 56 | expect(evaluator.evaluate('not @skip', tags1), true); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/mocks/reporter_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | 3 | typedef OnStepFinished = void Function(StepFinishedMessage message); 4 | 5 | class ReporterMock extends Reporter { 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 onTestRunStarted() async => onTestRunStartedInvocationCount += 1; 22 | @override 23 | Future onTestRunFinished() async => 24 | onTestRunFinishedInvocationCount += 1; 25 | @override 26 | Future onFeatureStarted(StartedMessage message) async => 27 | onFeatureStartedInvocationCount += 1; 28 | @override 29 | Future onFeatureFinished(FinishedMessage message) async => 30 | onFeatureFinishedInvocationCount += 1; 31 | @override 32 | Future onScenarioStarted(StartedMessage message) async => 33 | onScenarioStartedInvocationCount += 1; 34 | @override 35 | Future onScenarioFinished(FinishedMessage message) async => 36 | onScenarioFinishedInvocationCount += 1; 37 | @override 38 | Future onStepStarted(StepStartedMessage message) async => 39 | onStepStartedInvocationCount += 1; 40 | @override 41 | Future onStepFinished(StepFinishedMessage message) async { 42 | if (onStepFinishedFn != null) { 43 | onStepFinishedFn!(message); 44 | } 45 | 46 | onStepFinishedInvocationCount += 1; 47 | } 48 | 49 | @override 50 | Future onException(Object exception, StackTrace stackTrace) async => 51 | onExceptionInvocationCount += 1; 52 | 53 | @override 54 | Future message(String message, MessageLevel level) async => 55 | messageInvocationCount += 1; 56 | 57 | @override 58 | Future dispose() async => disposeInvocationCount += 1; 59 | } 60 | 61 | class SerializableReporterMock extends Reporter 62 | implements SerializableReporter { 63 | final String _json; 64 | 65 | SerializableReporterMock(this._json); 66 | 67 | @override 68 | String toJson() { 69 | return _json; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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/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 | final indexer = IoFeatureFileAccessor(); 12 | 13 | group('with RegExp', () { 14 | test('lists all matching files', () async { 15 | expect( 16 | await indexer.listFiles(RegExp(r'test/test_resources/(.*).feature')), 17 | PathPartMatcher([ 18 | r'test/test_resources/a.feature', 19 | r'test/test_resources/subdir/b.feature', 20 | r'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 | r'test/test_resources/subdir/b.feature', 31 | r'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 | r'test/test_resources/a.feature', 43 | r'test/test_resources/subdir/b.feature', 44 | r'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 | r'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(r'test/test_resources/a.feature'), 63 | PathPartMatcher([ 64 | r'test/test_resources/a.feature', 65 | ]), 66 | ); 67 | }); 68 | }); 69 | }); 70 | 71 | group('Reader', () { 72 | test('file contents are read', () async { 73 | final indexer = IoFeatureFileAccessor(); 74 | 75 | expect( 76 | await indexer.read('test/test_resources/a.feature'), 77 | 'Feature: A', 78 | ); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /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 | runnable.addChild( 16 | MultilineStringRunnable(debugInfo)..lines = ['3', '4', '5'].toList()); 17 | expect(runnable.multilineStrings.length, 2); 18 | expect(runnable.multilineStrings.elementAt(0), '1\n2\n3'); 19 | expect(runnable.multilineStrings.elementAt(1), '3\n4\n5'); 20 | }); 21 | 22 | test('can add TableRunnable', () { 23 | final runnable = StepRunnable('', debugInfo); 24 | runnable.addChild(TableRunnable(debugInfo) 25 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 26 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 27 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|'))); 28 | 29 | expect(runnable.table, isNotNull); 30 | expect(runnable.table!.header, isNotNull); 31 | expect(runnable.table!.header!.columns.length, 2); 32 | expect(runnable.table!.rows.length, 2); 33 | }); 34 | 35 | test('can only add single TableRunnable', () { 36 | final runnable = StepRunnable('Step A', debugInfo); 37 | runnable.addChild(TableRunnable(debugInfo) 38 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 39 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 40 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|'))); 41 | 42 | expect( 43 | () => runnable.addChild(TableRunnable(debugInfo) 44 | ..addChild(TableRunnable(debugInfo)..rows.add('|Col A|Col B|')) 45 | ..addChild(TableRunnable(debugInfo)..rows.add('|1|2|')) 46 | ..addChild(TableRunnable(debugInfo)..rows.add('|3|4|'))), 47 | throwsA((e) => 48 | e is GherkinSyntaxException && 49 | e.message == 50 | "Only a single table can be added to the step 'Step A'")); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /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 | final 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 | final indexer = IoFeatureFileAccessor(); 75 | 76 | expect( 77 | await indexer.read('test/test_resources/a.feature'), 78 | 'Feature: A', 79 | ); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /test/reporters/json_reports/report_8.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "", 4 | "id": "feature 1", 5 | "keyword": "Feature", 6 | "line": 3, 7 | "name": "Feature 1", 8 | "uri": "filepath", 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 | "description": "", 22 | "line": 5, 23 | "tags": [ 24 | { 25 | "line": 4, 26 | "name": "tag2" 27 | } 28 | ], 29 | "steps": [ 30 | { 31 | "keyword": "Step ", 32 | "name": "1", 33 | "line": 6, 34 | "match": { 35 | "location": "filepath:6" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 100000000 40 | } 41 | }, 42 | { 43 | "keyword": "Step ", 44 | "name": "2", 45 | "line": 7, 46 | "match": { 47 | "location": "filepath:7" 48 | }, 49 | "result": { 50 | "status": "failed", 51 | "duration": 100000000, 52 | "error_message": "error message" 53 | } 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | { 60 | "description": "", 61 | "id": "feature 2", 62 | "keyword": "Feature", 63 | "line": 3, 64 | "name": "Feature 2", 65 | "uri": "filepath", 66 | "tags": [ 67 | { 68 | "line": 2, 69 | "name": "tag1" 70 | } 71 | ], 72 | "elements": [ 73 | { 74 | "keyword": "Scenario", 75 | "type": "scenario", 76 | "id": "feature 2;scenario 2", 77 | "name": "Scenario 2", 78 | "description": "", 79 | "line": 5, 80 | "tags": [ 81 | { 82 | "line": 4, 83 | "name": "tag2" 84 | } 85 | ], 86 | "steps": [ 87 | { 88 | "keyword": "Step ", 89 | "name": "1", 90 | "line": 6, 91 | "match": { 92 | "location": "filepath:6" 93 | }, 94 | "result": { 95 | "status": "passed", 96 | "duration": 100000000 97 | } 98 | } 99 | ] 100 | } 101 | ] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /test/reporters/progress_reporter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/debug_information.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | class TestableProgressReporter extends ProgressReporter { 6 | final output = []; 7 | @override 8 | void printMessageLine( 9 | String message, [ 10 | String? colour, 11 | ]) { 12 | output.add(message); 13 | } 14 | } 15 | 16 | void main() { 17 | group('report', () { 18 | test('provides correct step finished output', () async { 19 | final reporter = TestableProgressReporter(); 20 | 21 | await reporter.onStepFinished( 22 | StepFinishedMessage( 23 | 'Step 1', 24 | RunnableDebugInformation('filePath', 1, 'line 1'), 25 | StepResult(0, StepExecutionResult.pass), 26 | [Attachment('A string', 'text/plain')], 27 | ), 28 | ); 29 | await reporter.onStepFinished( 30 | StepFinishedMessage( 31 | 'Step 2', 32 | RunnableDebugInformation('filePath', 2, 'line 2'), 33 | StepResult(0, StepExecutionResult.fail, 'Failed Reason'), 34 | ), 35 | ); 36 | await reporter.onStepFinished( 37 | StepFinishedMessage( 38 | 'Step 3', 39 | RunnableDebugInformation('filePath', 3, 'line 3'), 40 | StepResult(0, StepExecutionResult.skipped), 41 | ), 42 | ); 43 | await reporter.onStepFinished(StepFinishedMessage( 44 | 'Step 4', 45 | RunnableDebugInformation('filePath', 4, 'line 4'), 46 | StepResult(0, StepExecutionResult.error))); 47 | await reporter.onStepFinished( 48 | StepFinishedMessage( 49 | 'Step 5', 50 | RunnableDebugInformation('filePath', 5, 'line 5'), 51 | StepResult(1, StepExecutionResult.timeout), 52 | ), 53 | ); 54 | 55 | expect(reporter.output, [ 56 | ' √ Step 1 # filePath:1 took 0ms', 57 | ' Attachment (text/plain): A string', 58 | ' × Step 2 # filePath:2 took 0ms \n Failed Reason', 59 | ' - Step 3 # filePath:3 took 0ms', 60 | ' × Step 4 # filePath:4 took 0ms', 61 | ' × Step 5 # filePath:5 took 1ms' 62 | ]); 63 | }); 64 | 65 | test('provides correct scenario started output', () async { 66 | final reporter = TestableProgressReporter(); 67 | 68 | await reporter.onScenarioStarted(StartedMessage( 69 | Target.scenario, 70 | 'Scenario 1', 71 | RunnableDebugInformation('filePath', 1, 'line 1'), 72 | Iterable.empty(), 73 | )); 74 | 75 | expect(reporter.output, ['Running scenario: Scenario 1 # filePath:1']); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/feature.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/taggable_runnable_block.dart'; 2 | 3 | import '../exceptions/syntax_error.dart'; 4 | import './background.dart'; 5 | import './comment_line.dart'; 6 | import './debug_information.dart'; 7 | import './empty_line.dart'; 8 | import './runnable.dart'; 9 | import './scenario.dart'; 10 | import './scenario_outline.dart'; 11 | import './tags.dart'; 12 | import './text_line.dart'; 13 | 14 | class FeatureRunnable extends TaggableRunnableBlock { 15 | final String _name; 16 | String? description; 17 | final List scenarios = []; 18 | final List _tagsPendingAssignmentToChild = []; 19 | BackgroundRunnable? background; 20 | 21 | FeatureRunnable(this._name, RunnableDebugInformation debug) : super(debug); 22 | 23 | @override 24 | String get name => _name; 25 | 26 | @override 27 | void addChild(Runnable child) { 28 | switch (child.runtimeType) { 29 | case TextLineRunnable: 30 | description = 31 | "${description == null ? "" : "$description\n"}${(child as TextLineRunnable).text}"; 32 | break; 33 | case TagsRunnable: 34 | _tagsPendingAssignmentToChild.add(child as TagsRunnable); 35 | break; 36 | case ScenarioRunnable: 37 | case ScenarioOutlineRunnable: 38 | Iterable childScenarios = [child as ScenarioRunnable]; 39 | if (child is ScenarioOutlineRunnable) { 40 | childScenarios = child.expandOutlinesIntoScenarios(); 41 | } 42 | 43 | scenarios.addAll(childScenarios); 44 | if (_tagsPendingAssignmentToChild.isNotEmpty) { 45 | _tagsPendingAssignmentToChild 46 | .forEach((t) => childScenarios.forEach((s) => s.addTag(t))); 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 | @override 71 | void onTagAdded(TagsRunnable tag) { 72 | for (var scenario in scenarios) { 73 | scenario.addTag(tag.clone(inherited: true)); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/gherkin.dart: -------------------------------------------------------------------------------- 1 | library gherkin; 2 | 3 | export 'src/test_runner.dart'; 4 | export 'src/configuration.dart'; 5 | export 'src/gherkin/steps/world.dart'; 6 | export 'src/gherkin/steps/step_definition.dart'; 7 | export 'src/gherkin/steps/step_definition_implementations.dart'; 8 | export 'src/gherkin/steps/step_configuration.dart'; 9 | export 'src/gherkin/steps/given.dart'; 10 | export 'src/gherkin/steps/then.dart'; 11 | export 'src/gherkin/steps/when.dart'; 12 | export 'src/gherkin/steps/and.dart'; 13 | export 'src/gherkin/steps/but.dart'; 14 | export 'src/gherkin/steps/step_run_result.dart'; 15 | 16 | // Custom Parameters 17 | export 'src/gherkin/parameters/custom_parameter.dart'; 18 | export 'src/gherkin/parameters/float_parameter.dart'; 19 | export 'src/gherkin/parameters/int_parameter.dart'; 20 | export 'src/gherkin/parameters/plural_parameter.dart'; 21 | export 'src/gherkin/parameters/string_parameter.dart'; 22 | export 'src/gherkin/parameters/word_parameter.dart'; 23 | 24 | // Models 25 | export 'src/gherkin/models/table.dart'; 26 | export 'src/gherkin/models/table_row.dart'; 27 | 28 | // Reporters 29 | export 'src/reporters/reporter.dart'; 30 | export 'src/reporters/message_level.dart'; 31 | export 'src/reporters/messages.dart'; 32 | export 'src/reporters/stdout_reporter.dart'; 33 | export 'src/reporters/progress_reporter.dart'; 34 | export 'src/reporters/test_run_summary_reporter.dart'; 35 | export 'src/reporters/json/json_reporter.dart'; 36 | export 'src/reporters/aggregated_reporter.dart'; 37 | export 'src/reporters/serializable_reporter.dart'; 38 | export 'src/gherkin/runnables/debug_information.dart'; 39 | 40 | // Attachments 41 | export 'src/gherkin/attachments/attachment.dart'; 42 | export 'src/gherkin/attachments/attachment_manager.dart'; 43 | 44 | // Hooks 45 | export 'src/hooks/hook.dart'; 46 | export 'src/hooks/aggregated_hook.dart'; 47 | 48 | // Process Handler 49 | export 'src/processes/process_handler.dart'; 50 | 51 | // Exceptions 52 | export 'src/gherkin/exceptions/dialect_not_supported.dart'; 53 | export 'src/gherkin/exceptions/gherkin_exception.dart'; 54 | export 'src/gherkin/exceptions/parameter_count_mismatch_error.dart'; 55 | export 'src/gherkin/exceptions/step_not_defined_error.dart'; 56 | export 'src/gherkin/exceptions/syntax_error.dart'; 57 | export 'src/gherkin/exceptions/test_run_failed_exception.dart'; 58 | 59 | // Parser 60 | export 'src/gherkin/ast/feature_file_visitor.dart'; 61 | export 'src/gherkin/languages/language_service.dart'; 62 | export 'src/gherkin/steps/executable_step.dart'; 63 | export 'src/gherkin/expressions/gherkin_expression.dart'; 64 | export 'src/gherkin/expressions/tag_expression.dart'; 65 | 66 | // IO 67 | export 'src/io/feature_file_matcher.dart'; 68 | export 'src/io/io_feature_file_accessor.dart'; 69 | export 'src/io/feature_file_reader.dart'; 70 | -------------------------------------------------------------------------------- /lib/src/gherkin/steps/step_definition.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/expect/expect_mimic.dart'; 2 | 3 | import '../../utils/perf.dart'; 4 | import './step_run_result.dart'; 5 | import '../../reporters/reporter.dart'; 6 | import './step_configuration.dart'; 7 | import './world.dart'; 8 | import '../exceptions/parameter_count_mismatch_error.dart'; 9 | import 'dart:async'; 10 | 11 | abstract class StepDefinitionGeneric { 12 | final StepDefinitionConfiguration? config; 13 | final int _expectParameterCount; 14 | TWorld? _world; 15 | 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 | tf.message, 55 | ); 56 | } on TimeoutException catch (te, st) { 57 | return ErroredStepResult( 58 | elapsedMilliseconds, 59 | StepExecutionResult.timeout, 60 | te, 61 | st, 62 | ); 63 | } on Error catch (e, st) { 64 | return ErroredStepResult( 65 | elapsedMilliseconds, 66 | StepExecutionResult.error, 67 | Exception(e.toString()), 68 | st, 69 | ); 70 | } catch (e, st) { 71 | return ErroredStepResult( 72 | elapsedMilliseconds, 73 | StepExecutionResult.error, 74 | e, 75 | st, 76 | ); 77 | } 78 | 79 | return StepResult(elapsedMilliseconds, StepExecutionResult.pass); 80 | } 81 | 82 | Future onRun(Iterable parameters); 83 | 84 | void _ensureParameterCount(int actual, int expected) { 85 | if (actual != expected) { 86 | throw GherkinStepParameterMismatchException( 87 | runtimeType, 88 | expected, 89 | actual, 90 | ); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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 (var tgs in tags) { 13 | for (var tag in tgs.tags) { 14 | yield tag; 15 | } 16 | } 17 | } 18 | 19 | void main() { 20 | final debugInfo = RunnableDebugInformation.EMPTY(); 21 | group('addChild', () { 22 | test('can add TextLineRunnable', () { 23 | final runnable = FeatureRunnable('', debugInfo); 24 | runnable.addChild(TextLineRunnable(debugInfo)..text = 'text'); 25 | runnable.addChild(TextLineRunnable(debugInfo)..text = 'text line two'); 26 | expect(runnable.description, 'text\ntext line two'); 27 | }); 28 | test('can add TagsRunnable which are given to taggable the taggable child', 29 | () { 30 | final runnable = FeatureRunnable('', debugInfo); 31 | runnable.addChild(TagsRunnable(debugInfo)..tags = ['one', 'two']); 32 | runnable.addChild(TagsRunnable(debugInfo)..tags = ['three']); 33 | final scenario = ScenarioRunnable('', debugInfo); 34 | runnable.addChild(scenario); 35 | expect(tagsToList(scenario.tags), ['one', 'two', 'three']); 36 | }); 37 | test('can add EmptyLineRunnable', () { 38 | final runnable = FeatureRunnable('', debugInfo); 39 | runnable.addChild(EmptyLineRunnable(debugInfo)); 40 | }); 41 | test('can add CommentLineRunnable', () { 42 | final runnable = FeatureRunnable('', debugInfo); 43 | runnable.addChild(CommentLineRunnable('', debugInfo)); 44 | }); 45 | test('can add ScenarioRunnable', () { 46 | final runnable = FeatureRunnable('', debugInfo); 47 | runnable.addChild(ScenarioRunnable('1', debugInfo)); 48 | runnable.addChild(ScenarioRunnable('2', debugInfo)); 49 | runnable.addChild(ScenarioRunnable('3', debugInfo)); 50 | expect(runnable.scenarios.length, 3); 51 | expect(runnable.scenarios.elementAt(0).name, '1'); 52 | expect(runnable.scenarios.elementAt(1).name, '2'); 53 | expect(runnable.scenarios.elementAt(2).name, '3'); 54 | }); 55 | test('can add BackgroundRunnable', () { 56 | final runnable = FeatureRunnable('', debugInfo); 57 | runnable.addChild(BackgroundRunnable('1', debugInfo)); 58 | expect(runnable.background, isNotNull); 59 | expect(runnable.background!.name, '1'); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/reporters/aggregated_reporter.dart: -------------------------------------------------------------------------------- 1 | import './serializable_reporter.dart'; 2 | import './messages.dart'; 3 | import './message_level.dart'; 4 | import './reporter.dart'; 5 | 6 | class AggregatedReporter extends Reporter implements SerializableReporter { 7 | final List _reporters = []; 8 | 9 | void addReporter(Reporter reporter) => _reporters.add(reporter); 10 | 11 | @override 12 | Future message(String message, MessageLevel level) async { 13 | await _invokeReporters((r) async => await r.message(message, level)); 14 | } 15 | 16 | @override 17 | Future onTestRunStarted() async { 18 | await _invokeReporters((r) async => await r.onTestRunStarted()); 19 | } 20 | 21 | @override 22 | Future onTestRunFinished() async { 23 | await _invokeReporters((r) async => await r.onTestRunFinished()); 24 | } 25 | 26 | @override 27 | Future onFeatureStarted(StartedMessage message) async { 28 | await _invokeReporters((r) async => await r.onFeatureStarted(message)); 29 | } 30 | 31 | @override 32 | Future onFeatureFinished(FinishedMessage message) async { 33 | await _invokeReporters((r) async => await r.onFeatureFinished(message)); 34 | } 35 | 36 | @override 37 | Future onScenarioStarted(StartedMessage message) async { 38 | await _invokeReporters((r) async => await r.onScenarioStarted(message)); 39 | } 40 | 41 | @override 42 | Future onScenarioFinished(ScenarioFinishedMessage message) async { 43 | await _invokeReporters((r) async => await r.onScenarioFinished(message)); 44 | } 45 | 46 | @override 47 | Future onStepStarted(StepStartedMessage message) async { 48 | await _invokeReporters((r) async => await r.onStepStarted(message)); 49 | } 50 | 51 | @override 52 | Future onStepFinished(StepFinishedMessage message) async { 53 | await _invokeReporters((r) async => await r.onStepFinished(message)); 54 | } 55 | 56 | @override 57 | Future onException(Object exception, StackTrace stackTrace) async { 58 | await _invokeReporters( 59 | (r) async => await r.onException(exception, stackTrace)); 60 | } 61 | 62 | @override 63 | Future dispose() async { 64 | await _invokeReporters((r) async => await r.dispose()); 65 | _reporters.clear(); 66 | } 67 | 68 | Future _invokeReporters( 69 | Future Function(Reporter r) invoke, 70 | ) async { 71 | if (_reporters.isNotEmpty) { 72 | for (var reporter in _reporters) { 73 | await invoke(reporter); 74 | } 75 | } 76 | } 77 | 78 | @override 79 | String toJson() { 80 | var jsonReports = ''; 81 | if (_reporters.isNotEmpty) { 82 | jsonReports = _reporters 83 | .whereType() 84 | .map((x) => x.toJson()) 85 | .where((x) => x.isNotEmpty) 86 | .join(','); 87 | } 88 | 89 | return '[$jsonReports]'; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /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 | expect( 19 | syntax.isMatch( 20 | "Hello 'Jon'!", 21 | EnDialectMock(), 22 | ), 23 | true); 24 | expect( 25 | syntax.isMatch( 26 | ' Hello Jon', 27 | EnDialectMock(), 28 | ), 29 | true); 30 | expect( 31 | syntax.isMatch( 32 | ' Hello Jon', 33 | EnDialectMock(), 34 | ), 35 | true); 36 | expect( 37 | syntax.isMatch( 38 | ' h ', 39 | EnDialectMock(), 40 | ), 41 | true); 42 | expect( 43 | syntax.isMatch( 44 | '*', 45 | EnDialectMock(), 46 | ), 47 | true); 48 | expect( 49 | syntax.isMatch( 50 | ' + ', 51 | EnDialectMock(), 52 | ), 53 | true); 54 | }); 55 | 56 | test('does not match', () { 57 | final syntax = TextLineSyntax(); 58 | expect( 59 | syntax.isMatch( 60 | '#Hello Jon', 61 | EnDialectMock(), 62 | ), 63 | false); 64 | expect( 65 | syntax.isMatch( 66 | '# Hello Jon', 67 | EnDialectMock(), 68 | ), 69 | false); 70 | expect( 71 | syntax.isMatch( 72 | '# Hello Jon', 73 | EnDialectMock(), 74 | ), 75 | false); 76 | expect( 77 | syntax.isMatch( 78 | ' ', 79 | EnDialectMock(), 80 | ), 81 | false); 82 | expect( 83 | syntax.isMatch( 84 | ' # h ', 85 | EnDialectMock(), 86 | ), 87 | false); 88 | }); 89 | }); 90 | 91 | group('toRunnable', () { 92 | test('creates TextLineRunnable', () { 93 | final syntax = TextLineSyntax(); 94 | final runnable = syntax.toRunnable( 95 | ' Some text ', 96 | RunnableDebugInformation.EMPTY(), 97 | EnDialectMock(), 98 | ); 99 | expect(runnable, isNotNull); 100 | expect(runnable, predicate((x) => x is TextLineRunnable)); 101 | expect(runnable.text, equals('Some text')); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/gherkin/ast/feature_file_visitor.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/parser.dart'; 3 | import 'package:gherkin/src/gherkin/runnables/scenario_outline.dart'; 4 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 5 | 6 | class FeatureFileVisitor { 7 | Future visit( 8 | String featureFileContents, 9 | String path, 10 | LanguageService languageService, 11 | Reporter 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 | ); 26 | 27 | for (var i = 0; i < feature.scenarios.length; i += 1) { 28 | final scenario = feature.scenarios.elementAt(i); 29 | final isFirst = i == 0; 30 | final isLast = i == (feature.scenarios.length - 1); 31 | final allScenarios = scenario is ScenarioOutlineRunnable 32 | ? scenario.expandOutlinesIntoScenarios() 33 | : [scenario]; 34 | var acknowledgedScenarioPosition = false; 35 | 36 | for (var childScenario in allScenarios) { 37 | await visitScenario( 38 | childScenario.name, 39 | _tagsToList(childScenario.tags), 40 | acknowledgedScenarioPosition ? false : isFirst, 41 | acknowledgedScenarioPosition ? false : isLast, 42 | ); 43 | 44 | acknowledgedScenarioPosition = true; 45 | 46 | if (feature.background != null) { 47 | final bg = feature.background; 48 | 49 | for (final step in bg!.steps) { 50 | await visitScenarioStep( 51 | step.name, 52 | step.multilineStrings, 53 | step.table, 54 | ); 55 | } 56 | } 57 | 58 | for (final step in childScenario.steps) { 59 | await visitScenarioStep( 60 | step.name, 61 | step.multilineStrings, 62 | step.table, 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | 69 | return Future.value(''); 70 | } 71 | 72 | Future visitFeature( 73 | String name, 74 | String? description, 75 | Iterable tags, 76 | ) async {} 77 | 78 | Future visitScenario( 79 | String name, 80 | Iterable tags, 81 | bool isFirst, 82 | bool isLast, 83 | ) async {} 84 | 85 | Future visitScenarioStep( 86 | String name, 87 | Iterable multiLineStrings, 88 | GherkinTable? table, 89 | ) async {} 90 | 91 | Iterable _tagsToList(Iterable tags) sync* { 92 | for (var tgs in tags) { 93 | for (var tag in tgs.tags) { 94 | yield tag; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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 | expect( 19 | syntax.isMatch( 20 | 'Examples: ', 21 | EnDialectMock(), 22 | ), 23 | true); 24 | expect( 25 | syntax.isMatch( 26 | 'Examples: something', 27 | EnDialectMock(), 28 | ), 29 | true); 30 | expect( 31 | syntax.isMatch( 32 | ' Examples: something', 33 | EnDialectMock(), 34 | ), 35 | true); 36 | }); 37 | 38 | test('does not match', () { 39 | final syntax = ExampleSyntax(); 40 | expect( 41 | syntax.isMatch( 42 | 'Examples', 43 | EnDialectMock(), 44 | ), 45 | false); 46 | expect( 47 | syntax.isMatch( 48 | 'Example something', 49 | EnDialectMock(), 50 | ), 51 | false); 52 | expect( 53 | syntax.isMatch( 54 | '#Examples: something', 55 | EnDialectMock(), 56 | ), 57 | false); 58 | }); 59 | }); 60 | 61 | group('toRunnable', () { 62 | test('creates Runnable', () { 63 | final syntax = ExampleSyntax(); 64 | final runnable = syntax.toRunnable( 65 | 'Examples: An example 123', 66 | RunnableDebugInformation.EMPTY(), 67 | EnDialectMock(), 68 | ); 69 | expect(runnable, isNotNull); 70 | expect(runnable, predicate((x) => x is ExampleRunnable)); 71 | expect(runnable.name, equals('An example 123')); 72 | }); 73 | 74 | test('creates Runnable with empty name', () { 75 | final syntax = ExampleSyntax(); 76 | final runnable = syntax.toRunnable( 77 | 'Examples: ', 78 | RunnableDebugInformation.EMPTY(), 79 | EnDialectMock(), 80 | ); 81 | expect(runnable, isNotNull); 82 | expect(runnable, predicate((x) => x is ExampleRunnable)); 83 | expect(runnable.name, equals('')); 84 | }); 85 | 86 | test('creates Runnable with no name', () { 87 | final syntax = ExampleSyntax(); 88 | final runnable = syntax.toRunnable( 89 | 'Examples:', 90 | RunnableDebugInformation.EMPTY(), 91 | EnDialectMock(), 92 | ); 93 | expect(runnable, isNotNull); 94 | expect(runnable, predicate((x) => x is ExampleRunnable)); 95 | expect(runnable.name, equals('')); 96 | }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/hooks/aggregated_hook.dart: -------------------------------------------------------------------------------- 1 | import '../gherkin/runnables/scenario.dart'; 2 | import '../gherkin/steps/step_run_result.dart'; 3 | import '../gherkin/steps/world.dart'; 4 | import '../reporters/messages.dart'; 5 | import '../configuration.dart'; 6 | import './hook.dart'; 7 | 8 | export '../gherkin/runnables/scenario.dart'; 9 | 10 | class AggregatedHook extends Hook { 11 | Iterable? _orderedHooks; 12 | 13 | void addHooks(Iterable hooks) { 14 | _orderedHooks = hooks.toList() 15 | ..sort( 16 | (a, b) => b.priority - a.priority, 17 | ); 18 | } 19 | 20 | @override 21 | Future onBeforeRun(TestConfiguration config) async => await _invokeHooks((h) => h.onBeforeRun(config)); 22 | 23 | /// Run after all scenarios in a test run have completed 24 | @override 25 | Future onAfterRun(TestConfiguration config) async => await _invokeHooks((h) => h.onAfterRun(config)); 26 | 27 | @override 28 | Future onAfterScenarioWorldCreated( 29 | World world, 30 | ScenarioRunnable scenario, 31 | Iterable tags, 32 | ) async => 33 | await _invokeHooks( 34 | (h) => h.onAfterScenarioWorldCreated( 35 | world, 36 | scenario, 37 | tags, 38 | ), 39 | ); 40 | 41 | /// Run before a scenario and it steps are executed 42 | @override 43 | Future onBeforeScenario( 44 | TestConfiguration config, 45 | String scenario, 46 | Iterable tags, 47 | ) async => 48 | await _invokeHooks( 49 | (h) => h.onBeforeScenario( 50 | config, 51 | scenario, 52 | tags, 53 | ), 54 | ); 55 | 56 | /// Run after a scenario has executed 57 | @override 58 | Future onAfterScenario( 59 | TestConfiguration config, 60 | String scenario, 61 | Iterable tags, 62 | ) async => 63 | await _invokeHooks( 64 | (h) => h.onAfterScenario( 65 | config, 66 | scenario, 67 | tags, 68 | ), 69 | ); 70 | 71 | /// Run before a step is executed 72 | @override 73 | Future onBeforeStep( 74 | World world, 75 | String step, 76 | ) async => 77 | await _invokeHooks((h) => h.onBeforeStep(world, step)); 78 | 79 | /// Run after a step has executed 80 | @override 81 | Future onAfterStep( 82 | World world, 83 | ScenarioRunnable scenario, 84 | String step, 85 | StepResult result, 86 | ) async { 87 | var tempResult = result; 88 | 89 | await _invokeHooks((h) async { 90 | tempResult = await h.onAfterStep(world, scenario, step, tempResult) ?? result; 91 | }); 92 | 93 | return tempResult; 94 | } 95 | 96 | Future _invokeHooks( 97 | Future Function(Hook h) invoke, 98 | ) async { 99 | if (_orderedHooks != null && _orderedHooks!.isNotEmpty) { 100 | for (var hook in _orderedHooks!) { 101 | await invoke(hook); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/gherkin/runnables/scenario_outline.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/src/gherkin/runnables/scenario_expanded_from_outline_example.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/tags.dart'; 3 | 4 | import '../exceptions/syntax_error.dart'; 5 | import '../runnables/example.dart'; 6 | import '../runnables/scenario.dart'; 7 | import './debug_information.dart'; 8 | import './runnable.dart'; 9 | 10 | class ScenarioOutlineRunnable extends ScenarioRunnable { 11 | final List _examples = []; 12 | TagsRunnable? _pendingExampleTags; 13 | Iterable get examples => _examples; 14 | 15 | ScenarioOutlineRunnable( 16 | String name, 17 | RunnableDebugInformation debug, 18 | ) : super(name, debug); 19 | 20 | @override 21 | void addChild(Runnable child) { 22 | switch (child.runtimeType) { 23 | case ExampleRunnable: 24 | if (_pendingExampleTags != null) { 25 | (child as ExampleRunnable).addChild(_pendingExampleTags!); 26 | _pendingExampleTags = null; 27 | } 28 | 29 | _examples.add(child as ExampleRunnable); 30 | break; 31 | case TagsRunnable: 32 | _pendingExampleTags = child as TagsRunnable; 33 | break; 34 | default: 35 | super.addChild(child); 36 | } 37 | } 38 | 39 | @override 40 | void onTagAdded(TagsRunnable tag) { 41 | examples.forEach( 42 | (ex) { 43 | ex.addTag(tag.clone(inherited: true)); 44 | }, 45 | ); 46 | } 47 | 48 | Iterable expandOutlinesIntoScenarios() { 49 | if (examples.isEmpty) { 50 | throw GherkinSyntaxException( 51 | 'Scenario outline `$name` does not contains an example block.'); 52 | } 53 | 54 | final scenarios = []; 55 | examples.forEach( 56 | (example) { 57 | example.table!.asMap().toList(growable: false).asMap().forEach( 58 | (exampleIndex, exampleRow) { 59 | var exampleName = [ 60 | name, 61 | 'Examples:', 62 | if (example.name.isNotEmpty) example.name, 63 | '(${exampleIndex + 1})', 64 | ].join(' '); 65 | 66 | final clonedSteps = steps.map((step) => step.clone()).toList(); 67 | 68 | final scenarioRunnable = 69 | ScenarioExpandedFromOutlineExampleRunnable(exampleName, debug); 70 | 71 | exampleRow.forEach( 72 | (parameterName, value) { 73 | scenarioRunnable.setStepParameter(parameterName, value ?? ''); 74 | clonedSteps.forEach( 75 | (step) => step.setStepParameter(parameterName, value ?? ''), 76 | ); 77 | }, 78 | ); 79 | 80 | [...tags, ...example.tags] 81 | .forEach((t) => scenarioRunnable.addTag(t.clone())); 82 | 83 | clonedSteps.forEach((step) => scenarioRunnable.addChild(step)); 84 | scenarios.add(scenarioRunnable); 85 | }, 86 | ); 87 | }, 88 | ); 89 | 90 | return scenarios; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/hooks/aggregated_hook_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/runnables/scenario.dart'; 3 | import 'package:test/test.dart'; 4 | import '../mocks/hook_mock.dart'; 5 | 6 | void main() { 7 | group('orders hooks', () { 8 | test('executes hooks in correct order', () async { 9 | final executionOrder = []; 10 | final hookOne = HookMock(providedPriority: 0, onBeforeRunCode: () => executionOrder.add(3)); 11 | final hookTwo = HookMock(providedPriority: 10, onBeforeRunCode: () => executionOrder.add(2)); 12 | final hookThree = HookMock(providedPriority: 20, onBeforeRunCode: () => executionOrder.add(1)); 13 | final hookFour = HookMock(providedPriority: -1, onBeforeRunCode: () => executionOrder.add(4)); 14 | 15 | final aggregatedHook = AggregatedHook(); 16 | aggregatedHook.addHooks([hookOne, hookTwo, hookThree, hookFour]); 17 | await aggregatedHook.onBeforeRun(TestConfiguration.DEFAULT([])); 18 | expect(executionOrder, [1, 2, 3, 4]); 19 | expect(hookOne.onBeforeRunInvocationCount, 1); 20 | expect(hookTwo.onBeforeRunInvocationCount, 1); 21 | expect(hookThree.onBeforeRunInvocationCount, 1); 22 | expect(hookFour.onBeforeRunInvocationCount, 1); 23 | await aggregatedHook.onAfterRun(TestConfiguration.DEFAULT([])); 24 | expect(hookOne.onAfterRunInvocationCount, 1); 25 | expect(hookTwo.onAfterRunInvocationCount, 1); 26 | expect(hookThree.onAfterRunInvocationCount, 1); 27 | expect(hookFour.onAfterRunInvocationCount, 1); 28 | await aggregatedHook.onBeforeScenario( 29 | TestConfiguration.DEFAULT([]), 30 | '', 31 | const Iterable.empty(), 32 | ); 33 | expect(hookOne.onBeforeScenarioInvocationCount, 1); 34 | expect(hookTwo.onBeforeScenarioInvocationCount, 1); 35 | expect(hookThree.onBeforeScenarioInvocationCount, 1); 36 | expect(hookFour.onBeforeScenarioInvocationCount, 1); 37 | await aggregatedHook.onBeforeStep(World(), ''); 38 | expect(hookOne.onBeforeStepInvocationCount, 1); 39 | expect(hookTwo.onBeforeStepInvocationCount, 1); 40 | expect(hookThree.onBeforeStepInvocationCount, 1); 41 | expect(hookFour.onBeforeStepInvocationCount, 1); 42 | await aggregatedHook.onAfterScenarioWorldCreated( 43 | World(), 44 | ScenarioRunnable('', RunnableDebugInformation.EMPTY()), 45 | const Iterable.empty(), 46 | ); 47 | expect(hookOne.onAfterScenarioWorldCreatedInvocationCount, 1); 48 | expect(hookTwo.onAfterScenarioWorldCreatedInvocationCount, 1); 49 | expect(hookThree.onAfterScenarioWorldCreatedInvocationCount, 1); 50 | expect(hookFour.onAfterScenarioWorldCreatedInvocationCount, 1); 51 | await aggregatedHook.onAfterStep( 52 | World(), 53 | ScenarioRunnable('', RunnableDebugInformation.EMPTY()), 54 | '', 55 | StepResult( 56 | 0, 57 | StepExecutionResult.skipped, 58 | ), 59 | ); 60 | expect(hookOne.onAfterStepInvocationCount, 1); 61 | expect(hookTwo.onAfterStepInvocationCount, 1); 62 | expect(hookThree.onAfterStepInvocationCount, 1); 63 | expect(hookFour.onAfterStepInvocationCount, 1); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/reporters/test_run_summary_reporter.dart: -------------------------------------------------------------------------------- 1 | import './stdout_reporter.dart'; 2 | import '../gherkin/steps/step_run_result.dart'; 3 | import './message_level.dart'; 4 | import './messages.dart'; 5 | 6 | class TestRunSummaryReporter extends StdoutReporter { 7 | final _timer = Stopwatch(); 8 | final List _ranSteps = []; 9 | final List _ranScenarios = 10 | []; 11 | 12 | @override 13 | Future onScenarioFinished(ScenarioFinishedMessage message) async { 14 | _ranScenarios.add(message); 15 | } 16 | 17 | @override 18 | Future onStepFinished(StepFinishedMessage message) async { 19 | _ranSteps.add(message); 20 | } 21 | 22 | @override 23 | Future message(String message, MessageLevel level) async { 24 | // ignore messages 25 | } 26 | 27 | @override 28 | Future onTestRunStarted() async { 29 | _timer.start(); 30 | } 31 | 32 | @override 33 | Future onTestRunFinished() async { 34 | _timer.stop(); 35 | printMessageLine( 36 | "${_ranScenarios.length} scenario${_ranScenarios.length > 1 ? "s" : ""} (${_collectScenarioSummary(_ranScenarios)})"); 37 | printMessageLine( 38 | "${_ranSteps.length} step${_ranSteps.length > 1 ? "s" : ""} (${_collectStepSummary(_ranSteps)})"); 39 | printMessageLine('${Duration(milliseconds: _timer.elapsedMilliseconds)}'); 40 | } 41 | 42 | @override 43 | Future dispose() async { 44 | if (_timer.isRunning) { 45 | _timer.stop(); 46 | } 47 | } 48 | 49 | String _collectScenarioSummary(Iterable scenarios) { 50 | final summaries = []; 51 | if (scenarios.any((s) => s.passed)) { 52 | summaries.add( 53 | '${StdoutReporter.PASS_COLOR}${scenarios.where((s) => s.passed).length} passed${StdoutReporter.RESET_COLOR}'); 54 | } 55 | 56 | if (scenarios.any((s) => !s.passed)) { 57 | summaries.add( 58 | '${StdoutReporter.FAIL_COLOR}${scenarios.where((s) => !s.passed).length} failed${StdoutReporter.RESET_COLOR}'); 59 | } 60 | 61 | return summaries.join(', '); 62 | } 63 | 64 | String _collectStepSummary(Iterable steps) { 65 | final summaries = []; 66 | final passed = 67 | steps.where((s) => s.result.result == StepExecutionResult.pass); 68 | final skipped = 69 | steps.where((s) => s.result.result == StepExecutionResult.skipped); 70 | final failed = steps.where((s) => 71 | s.result.result == StepExecutionResult.error || 72 | s.result.result == StepExecutionResult.fail || 73 | s.result.result == StepExecutionResult.timeout); 74 | if (passed.isNotEmpty) { 75 | summaries.add( 76 | '${StdoutReporter.PASS_COLOR}${passed.length} passed${StdoutReporter.RESET_COLOR}'); 77 | } 78 | 79 | if (skipped.isNotEmpty) { 80 | summaries.add( 81 | '${StdoutReporter.WARN_COLOR}${skipped.length} skipped${StdoutReporter.RESET_COLOR}'); 82 | } 83 | 84 | if (failed.isNotEmpty) { 85 | summaries.add( 86 | '${StdoutReporter.FAIL_COLOR}${failed.length} failed${StdoutReporter.RESET_COLOR}'); 87 | } 88 | 89 | return summaries.join(', '); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /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 | expect( 21 | syntax.isMatch( 22 | '```', 23 | EnDialectMock(), 24 | ), 25 | true); 26 | expect( 27 | syntax.isMatch( 28 | "'''", 29 | EnDialectMock(), 30 | ), 31 | true); 32 | }); 33 | 34 | test('does not match', () { 35 | final syntax = MultilineStringSyntax(); 36 | expect( 37 | syntax.isMatch( 38 | '#"""', 39 | EnDialectMock(), 40 | ), 41 | false); 42 | expect( 43 | syntax.isMatch( 44 | '#```', 45 | EnDialectMock(), 46 | ), 47 | false); 48 | expect( 49 | syntax.isMatch( 50 | "#'''", 51 | EnDialectMock(), 52 | ), 53 | false); 54 | expect( 55 | syntax.isMatch( 56 | '"', 57 | EnDialectMock(), 58 | ), 59 | false); 60 | expect( 61 | syntax.isMatch( 62 | '`', 63 | EnDialectMock(), 64 | ), 65 | false); 66 | expect( 67 | syntax.isMatch( 68 | "'", 69 | EnDialectMock(), 70 | ), 71 | false); 72 | }); 73 | }); 74 | 75 | group('block', () { 76 | test('is block', () { 77 | final syntax = MultilineStringSyntax(); 78 | expect(syntax.isBlockSyntax, true); 79 | }); 80 | 81 | test('continue block if text line string', () { 82 | final syntax = MultilineStringSyntax(); 83 | expect(syntax.hasBlockEnded(TextLineSyntax()), false); 84 | }); 85 | 86 | test('continue block if comment string', () { 87 | final syntax = MultilineStringSyntax(); 88 | expect(syntax.hasBlockEnded(CommentSyntax()), false); 89 | }); 90 | 91 | test('end block if multiline string', () { 92 | final syntax = MultilineStringSyntax(); 93 | expect(syntax.hasBlockEnded(MultilineStringSyntax()), true); 94 | }); 95 | }); 96 | 97 | group('toRunnable', () { 98 | test('creates TextLineRunnable', () { 99 | final syntax = MultilineStringSyntax(); 100 | final runnable = syntax.toRunnable( 101 | "'''", 102 | RunnableDebugInformation.EMPTY(), 103 | EnDialectMock(), 104 | ); 105 | expect(runnable, isNotNull); 106 | expect(runnable, predicate((x) => x is MultilineStringRunnable)); 107 | expect(runnable.lines.length, 0); 108 | }); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/expect/expect_mimic.dart: -------------------------------------------------------------------------------- 1 | import './expect_mimic_utils.dart'; 2 | import 'package:matcher/matcher.dart'; 3 | 4 | /// This is an atrocity but I can't see a way around it at the moment 5 | /// To use the expect() it must be called within a test() or this happens: 6 | /// 7 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart#L95 8 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect_async.dart#L237 9 | /// 10 | /// Unfortunately, I cannot get the test framework to play nicely with dynamically 11 | /// creating and adding tests as the tests framework seems to build the tests before 12 | /// I need it to and this happens: 13 | /// 14 | /// "Can't call test() once tests have begun running." 15 | /// 16 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/backend/declarer.dart#L274 17 | /// 18 | /// We still want to be able to use the Matchers are we can't expect people not to use them 19 | /// So we are stuck here using smoke and mirrors and mimicking the expect / expectAsync methods in our step class 20 | /// 21 | /// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart 22 | class ExpectMimic { 23 | /// Assert that [actual] matches [matcher]. 24 | /// 25 | /// This is the main assertion function. [reason] is optional and is typically 26 | /// not supplied, as a reason is generated from [matcher]; if [reason] 27 | /// is included it is appended to the reason generated by the matcher. 28 | /// 29 | /// [matcher] can be a value in which case it will be wrapped in an 30 | /// [equals] matcher. 31 | /// 32 | /// If the assertion fails a [TestFailure] is thrown. 33 | /// 34 | /// If [skip] is a String or `true`, the assertion is skipped. The arguments are 35 | /// still evaluated, but [actual] is not verified to match [matcher]. If 36 | /// [actual] is a [Future], the test won't complete until the future emits a 37 | /// value. 38 | /// 39 | /// Certain matchers, like [completion] and [throwsA], either match or fail 40 | /// asynchronously. When you use [expect] with these matchers, it ensures that 41 | /// the test doesn't complete until the matcher has either matched or failed. If 42 | /// you want to wait for the matcher to complete before continuing the test, you 43 | /// can call [expectLater] instead and `await` the result. 44 | void expect( 45 | actualValue, 46 | matcher, { 47 | String? reason, 48 | }) { 49 | final matchState = {}; 50 | matcher = wrapMatcher(matcher); 51 | final result = matcher.matches(actualValue, matchState); 52 | final formatter = (actual, matcher, reason, matchState, verbose) { 53 | final mismatchDescription = StringDescription(); 54 | matcher.describeMismatch( 55 | actual, mismatchDescription, matchState, verbose); 56 | 57 | return formatFailure(matcher, actual, mismatchDescription.toString(), 58 | reason: reason); 59 | }; 60 | 61 | if (!result) { 62 | throw GherkinTestFailure(formatter( 63 | actualValue, matcher as Matcher, reason, matchState, false)); 64 | } 65 | } 66 | } 67 | 68 | class GherkinTestFailure { 69 | final String message; 70 | 71 | GherkinTestFailure(this.message); 72 | 73 | @override 74 | String toString() => message; 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:gherkin/src/reporters/json/json_tag.dart'; 4 | 5 | import '../messages.dart'; 6 | import '../reporter.dart'; 7 | import '../serializable_reporter.dart'; 8 | import 'json_feature.dart'; 9 | import 'json_scenario.dart'; 10 | import 'json_step.dart'; 11 | 12 | class JsonReporter extends Reporter implements SerializableReporter { 13 | final List _features = []; 14 | final String path; 15 | final Future Function(String jsonReport, String path)? writeReport; 16 | 17 | JsonReporter({ 18 | this.path = './report.json', 19 | this.writeReport, 20 | }); 21 | 22 | @override 23 | Future onFeatureStarted(StartedMessage message) async { 24 | _features.add(JsonFeature.from(message)); 25 | } 26 | 27 | @override 28 | Future onScenarioStarted(StartedMessage message) async { 29 | _getCurrentFeature().add(JsonScenario.from(message)); 30 | } 31 | 32 | @override 33 | Future onStepStarted(StepStartedMessage message) async { 34 | _getCurrentFeature().currentScenario().add(JsonStep.from(message)); 35 | } 36 | 37 | @override 38 | Future onStepFinished(StepFinishedMessage message) async { 39 | _getCurrentFeature().currentScenario().currentStep().onFinish(message); 40 | } 41 | 42 | @override 43 | Future onException(Object exception, StackTrace stackTrace) async { 44 | _getCurrentFeature() 45 | .currentScenario() 46 | .currentStep() 47 | .onException(exception, stackTrace); 48 | } 49 | 50 | @override 51 | Future onTestRunFinished() async { 52 | await _generateReport(path); 53 | } 54 | 55 | Future onSaveReport(String jsonReport, String path) async { 56 | final file = File(path); 57 | await file.writeAsString(jsonReport); 58 | } 59 | 60 | Future _generateReport(String path) async { 61 | try { 62 | final report = toJson(); 63 | if (writeReport != null) { 64 | await writeReport!(report, path); 65 | } else { 66 | await onSaveReport(report, path); 67 | } 68 | } catch (e) { 69 | print('Failed to generate json report: $e'); 70 | } 71 | } 72 | 73 | JsonFeature _getCurrentFeature() { 74 | if (_features.isEmpty) { 75 | final feature = JsonFeature() 76 | ..name = 'Unnamed feature' 77 | ..description = 78 | 'An unnamed feature is possible if something is logged before any feature has started to execute' 79 | ..scenarios = [ 80 | JsonScenario() 81 | ..target = Target.scenario 82 | ..name = 'Unnamed' 83 | ..description = 84 | 'An unnamed scenario is possible if something is logged before any feature has started to execute' 85 | ..line = 0 86 | ..tags = [] 87 | ..steps = [ 88 | JsonStep() 89 | ..name = 'Unnamed' 90 | ..line = 0 91 | ] 92 | ] 93 | ..line = 0 94 | ..tags = [] 95 | ..uri = 'unknown'; 96 | 97 | _features.add(feature); 98 | } 99 | 100 | return _features.last; 101 | } 102 | 103 | @override 104 | String toJson() { 105 | return json.encode(_features); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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('', debugInfo); 15 | runnable.addChild(EmptyLineRunnable(debugInfo)); 16 | }); 17 | 18 | test('can add StepRunnable', () { 19 | final runnable = ScenarioOutlineRunnable('', 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('', 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('', 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', 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: , ', debugInfo); 55 | final example = ExampleRunnable('', debugInfo); 56 | final exampleTable = TableRunnable(debugInfo); 57 | 58 | exampleTable.rows 59 | ..add('| i | j | k |') 60 | ..add('| 1 | 2 | 3 |') 61 | ..add('| text | 4.5 | false |'); 62 | example.addChild(exampleTable); 63 | runnable.addChild(example); 64 | 65 | final expandedScenarios = runnable.expandOutlinesIntoScenarios(); 66 | final expandedScenario1 = expandedScenarios.elementAt(0); 67 | final expandedScenario2 = expandedScenarios.elementAt(1); 68 | 69 | expect(expandedScenario1.name, 70 | equals('Scenario outline with parameters: 1, 3 Examples: (1)')); 71 | expect( 72 | expandedScenario2.name, 73 | equals( 74 | 'Scenario outline with parameters: text, false Examples: (2)')); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /test/reporters/test_run_summary_reporter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gherkin/gherkin.dart'; 2 | import 'package:gherkin/src/gherkin/steps/step_run_result.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | class TestableTestRunSummaryReporter extends TestRunSummaryReporter { 6 | final output = []; 7 | @override 8 | void printMessageLine( 9 | String message, [ 10 | String? colour, 11 | ]) { 12 | output.add(message); 13 | } 14 | } 15 | 16 | void main() { 17 | group('report', () { 18 | test('provides correct output', () async { 19 | final reporter = TestableTestRunSummaryReporter(); 20 | 21 | await reporter.onStepFinished( 22 | StepFinishedMessage( 23 | '', 24 | RunnableDebugInformation.EMPTY(), 25 | StepResult(0, StepExecutionResult.pass), 26 | ), 27 | ); 28 | await reporter.onStepFinished( 29 | StepFinishedMessage( 30 | '', 31 | RunnableDebugInformation.EMPTY(), 32 | StepResult(0, StepExecutionResult.fail), 33 | ), 34 | ); 35 | await reporter.onStepFinished( 36 | StepFinishedMessage( 37 | '', 38 | RunnableDebugInformation.EMPTY(), 39 | StepResult(0, StepExecutionResult.skipped), 40 | ), 41 | ); 42 | await reporter.onStepFinished( 43 | StepFinishedMessage( 44 | '', 45 | RunnableDebugInformation.EMPTY(), 46 | StepResult(0, StepExecutionResult.skipped), 47 | ), 48 | ); 49 | await reporter.onStepFinished( 50 | StepFinishedMessage( 51 | '', 52 | RunnableDebugInformation.EMPTY(), 53 | StepResult(0, StepExecutionResult.pass), 54 | ), 55 | ); 56 | await reporter.onStepFinished( 57 | StepFinishedMessage( 58 | '', 59 | RunnableDebugInformation.EMPTY(), 60 | StepResult(0, StepExecutionResult.error), 61 | ), 62 | ); 63 | await reporter.onStepFinished( 64 | StepFinishedMessage( 65 | '', 66 | RunnableDebugInformation.EMPTY(), 67 | StepResult(0, StepExecutionResult.pass), 68 | ), 69 | ); 70 | await reporter.onStepFinished( 71 | StepFinishedMessage( 72 | '', 73 | RunnableDebugInformation.EMPTY(), 74 | StepResult(0, StepExecutionResult.timeout), 75 | ), 76 | ); 77 | 78 | await reporter.onScenarioFinished(ScenarioFinishedMessage( 79 | '', 80 | RunnableDebugInformation.EMPTY(), 81 | true, 82 | )); 83 | await reporter.onScenarioFinished(ScenarioFinishedMessage( 84 | '', 85 | RunnableDebugInformation.EMPTY(), 86 | false, 87 | )); 88 | await reporter.onScenarioFinished(ScenarioFinishedMessage( 89 | '', 90 | RunnableDebugInformation.EMPTY(), 91 | false, 92 | )); 93 | await reporter.onScenarioFinished(ScenarioFinishedMessage( 94 | '', 95 | RunnableDebugInformation.EMPTY(), 96 | true, 97 | )); 98 | 99 | await reporter.onTestRunFinished(); 100 | expect(reporter.output, [ 101 | '4 scenarios (\x1B[33;32m2 passed\x1B[33;0m, \x1B[33;31m2 failed\x1B[33;0m)', 102 | '8 steps (\x1B[33;32m3 passed\x1B[33;0m, \x1B[33;10m2 skipped\x1B[33;0m, \x1B[33;31m3 failed\x1B[33;0m)', 103 | '0:00:00.000000' 104 | ]); 105 | }); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/reporters/json/json_step.dart: -------------------------------------------------------------------------------- 1 | import 'json_embedding.dart'; 2 | import 'json_row.dart'; 3 | import '../messages.dart'; 4 | import '../../gherkin/steps/step_run_result.dart'; 5 | 6 | class JsonStep { 7 | late final String name; 8 | late final int line; 9 | List? rows; 10 | List? embeddings; 11 | String? error; 12 | String? docString; 13 | String? file; 14 | String? keyword; 15 | int duration = 0; 16 | String status = 'failed'; 17 | 18 | static JsonStep from(StepStartedMessage message) { 19 | final step = JsonStep(); 20 | 21 | final index = message.name.indexOf(' '); 22 | final keyword = message.name.substring(0, index + 1); 23 | final name = message.name.substring(index + 1, message.name.length); 24 | 25 | step.keyword = keyword; 26 | step.name = name; 27 | step.line = message.context.nonZeroAdjustedLineNumber; 28 | step.file = message.context.filePath; 29 | step.docString = message.multilineString; 30 | 31 | if (message.table?.rows != null && message.table!.rows.isNotEmpty) { 32 | step.rows = message.table!.rows 33 | .map( 34 | (r) => JsonRow( 35 | r.columns.toList( 36 | growable: false, 37 | ), 38 | ), 39 | ) 40 | .toList(); 41 | 42 | step.rows!.insert( 43 | 0, 44 | JsonRow( 45 | message.table!.header!.columns.toList( 46 | growable: false, 47 | ), 48 | ), 49 | ); 50 | } 51 | 52 | return step; 53 | } 54 | 55 | void onFinish(StepFinishedMessage message) { 56 | duration = message.result.elapsedMilliseconds * 1000000; // nano seconds. 57 | 58 | switch (message.result.result) { 59 | case StepExecutionResult.pass: 60 | status = 'passed'; 61 | break; 62 | case StepExecutionResult.skipped: 63 | status = 'skipped'; 64 | break; 65 | default: 66 | break; 67 | } 68 | 69 | if (message.attachments.isNotEmpty) { 70 | embeddings = message.attachments 71 | .map((attachment) => JsonEmbedding() 72 | ..data = attachment.data 73 | ..mimeType = attachment.mimeType) 74 | .toList(); 75 | } 76 | 77 | _trackError(message.result.resultReason); 78 | } 79 | 80 | void onException( 81 | Object exception, 82 | StackTrace stackTrace, 83 | ) { 84 | _trackError( 85 | exception.toString(), 86 | stackTrace.toString(), 87 | ); 88 | } 89 | 90 | void _trackError( 91 | String? error, [ 92 | String? stacktrace, 93 | ]) { 94 | if (error != null && error.isNotEmpty) { 95 | this.error = 96 | '$error${stacktrace != null ? '\n\n$stacktrace' : ''}'.trim(); 97 | } 98 | } 99 | 100 | Map toJson() { 101 | final result = { 102 | 'keyword': keyword, 103 | 'name': name, 104 | 'line': line, 105 | 'match': { 106 | 'location': '$file:$line', 107 | }, 108 | 'result': { 109 | 'status': status, 110 | 'duration': duration, 111 | } 112 | }; 113 | 114 | if (docString != null && docString!.isNotEmpty) { 115 | result['docString'] = { 116 | 'content_type': '', 117 | 'value': docString, 118 | 'line': line + 1, 119 | }; 120 | } 121 | 122 | if (embeddings != null && embeddings!.isNotEmpty) { 123 | result['embeddings'] = embeddings!.toList(); 124 | } 125 | 126 | if (error != null && error!.isNotEmpty) { 127 | (result['result'] as dynamic)['error_message'] = error; 128 | } 129 | 130 | if (rows != null && rows!.isNotEmpty) { 131 | result['rows'] = rows!.toList(); 132 | } 133 | 134 | return result; 135 | } 136 | } 137 | --------------------------------------------------------------------------------