diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 5b7a217..078e432 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -34,10 +34,7 @@ jobs: run: dart pub global activate coverage - name: ๐Ÿงช Run tests - run: dart pub global run coverage:test_with_coverage - - - name: ๐ŸŽฏ Check Code Coverage - uses: VeryGoodOpenSource/very_good_coverage@v1 + run: dart pub global run coverage:test_with_coverage --fail-under 100 - name: ๐Ÿฅ‡ Update coverage badge if: github.event_name != 'pull_request' diff --git a/CHANGELOG.md b/CHANGELOG.md index 641556f..88638b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [2.0.1] - Custom headers support + +* Add `customHeaders` configuration option to include custom header lines (imports, comments, etc.) in all generated step files and feature files + ## [2.0.0] - Upgrade dependencies * **BREAKING CHANGE**: The package doesn't provide pre-built steps anymore. Steps will appear in the `step` folder. diff --git a/README.md b/README.md index 613af1b..a7ca5cc 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,33 @@ targets: include: package:/bdd_options.yaml ``` +### How to add custom headers to generated files? + +You can add custom header lines (imports, comments, etc.) to all generated step files **and feature files** using the `customHeaders` option in the `build.yaml` file: +```yaml +targets: + $default: + builders: + bdd_widget_test|featureBuilder: + options: + customHeaders: + - "import 'package:flutter_test/flutter_test.dart';" + - "import 'package:patrol/patrol.dart';" + - "// Custom test utilities" + - "import 'package:my_custom_package/my_helper.dart';" +``` + +This is useful when you need to: +- Use custom test frameworks (like Patrol) instead of the default flutter_test +- Import custom helper classes or utilities in all your generated files +- Add specific packages that your tests will commonly use +- Include custom test utilities, mocks, or constants +- Add custom comments or documentation to generated files + +The custom headers will be added to: +- **Feature files**: After any data table imports, replacing the default flutter/material and flutter_test imports +- **Step files**: After any data table imports, replacing the default flutter_test import + ### How to group steps in a single project? You may create sub-folders (like `common`, `login`, `home`, etc.) in the `step` folder and move generated steps there. The plugin is smart enough to find them (see the `example` folder). @@ -397,6 +424,9 @@ targets: testerName: $ testerType: PatrolIntegrationTester includeIntegrationTestBinding: false + customHeaders: + - "import 'package:flutter_test/flutter_test.dart';" + - "import 'package:patrol/patrol.dart';" ``` Since Patrol version 3.0.0, `IntegrationTestWidgetsFlutterBinding.ensureInitialized` must not be called. Set `includeIntegrationTestBinding` to `false`. diff --git a/example/bdd_options_example.yaml b/example/bdd_options_example.yaml new file mode 100644 index 0000000..abae6c3 --- /dev/null +++ b/example/bdd_options_example.yaml @@ -0,0 +1,14 @@ +# Example bdd_options.yaml file showing how to use customHeaders +# This file can be referenced from build.yaml using the include option + +# Add custom header lines that will be added to all generated step files +customHeaders: + - "import 'package:flutter_test/flutter_test.dart';" + - "import 'package:mockito/mockito.dart';" + - "import 'package:my_project/test_helpers.dart';" + - "import 'package:my_project/mocks/database_mock.dart';" + +# You can also include external steps from other packages +externalSteps: + - package:common_bdd_steps/step/i_wait.dart + - package:common_bdd_steps/step/i_see_loading.dart diff --git a/example/build.yaml b/example/build.yaml index 2cfa42d..67abd38 100644 --- a/example/build.yaml +++ b/example/build.yaml @@ -15,6 +15,9 @@ targets: # addHooks: true # if true, hooks will be added to the test; default is false # hookFolderName: bdd_hooks # include: package:/bdd_options.yaml # you may add defaul external steps with this line + # customHeaders: # add custom header lines to all generated step files + # - "import 'package:flutter_test/flutter_test.dart';" + # - "import 'package:my_project/test_helpers.dart';" externalSteps: # or list only steps that you need # - package:/step/i_see_text.dart # - package:/step/i_dont_see_text.dart diff --git a/lib/src/feature_file.dart b/lib/src/feature_file.dart index 2c4b2d7..e6097e1 100644 --- a/lib/src/feature_file.dart +++ b/lib/src/feature_file.dart @@ -83,6 +83,7 @@ class FeatureFile { includeIntegrationTestBinding, includeIntegrationTestImport, hookFile, + generatorOptions, ); List getStepFiles() => _stepFiles; diff --git a/lib/src/feature_generator.dart b/lib/src/feature_generator.dart index 21c8ea8..d9ebfe6 100644 --- a/lib/src/feature_generator.dart +++ b/lib/src/feature_generator.dart @@ -1,5 +1,6 @@ import 'package:bdd_widget_test/src/bdd_line.dart'; import 'package:bdd_widget_test/src/data_table_parser.dart'; +import 'package:bdd_widget_test/src/generator_options.dart'; import 'package:bdd_widget_test/src/hook_file.dart'; import 'package:bdd_widget_test/src/scenario_generator.dart'; import 'package:bdd_widget_test/src/step_file.dart'; @@ -16,6 +17,7 @@ String generateFeatureDart( bool includeIntegrationTestBinding, bool includeIntegrationTestImport, HookFile? hookFile, + GeneratorOptions generatorOptions, ) { final sb = StringBuffer(); sb.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND'); @@ -59,8 +61,17 @@ String generateFeatureDart( if (hasBddDataTable(lines)) { sb.writeln("import 'package:bdd_widget_test/data_table.dart' as bdd;"); } - sb.writeln("import 'package:flutter/material.dart';"); - sb.writeln("import 'package:flutter_test/flutter_test.dart';"); + + // Use custom headers if provided, otherwise use default imports + if (generatorOptions.customHeaders.isNotEmpty) { + for (final header in generatorOptions.customHeaders) { + sb.writeln(header); + } + } else { + sb.writeln("import 'package:flutter/material.dart';"); + sb.writeln("import 'package:flutter_test/flutter_test.dart';"); + } + if (includeIntegrationTestImport) { sb.writeln("import 'package:integration_test/integration_test.dart';"); } diff --git a/lib/src/generator_options.dart b/lib/src/generator_options.dart index 56ab562..a6a08f0 100644 --- a/lib/src/generator_options.dart +++ b/lib/src/generator_options.dart @@ -20,6 +20,7 @@ class GeneratorOptions { String? hookFolderName, this.include, bool? includeIntegrationTestBinding, + List? customHeaders, }) : stepFolder = stepFolderName ?? _stepFolderName, relativeToTestFolder = relativeToTestFolder ?? true, testMethodName = testMethodName ?? _defaultTestMethodName, @@ -28,24 +29,26 @@ class GeneratorOptions { addHooks = addHooks ?? false, hookFolderName = hookFolderName ?? _hookFolderName, externalSteps = externalSteps ?? const [], - includeIntegrationTestBinding = includeIntegrationTestBinding ?? true; + includeIntegrationTestBinding = includeIntegrationTestBinding ?? true, + customHeaders = customHeaders ?? const []; factory GeneratorOptions.fromMap(Map json) => GeneratorOptions( - testMethodName: json['testMethodName'] as String?, - testerType: json['testerType'] as String?, - testerName: json['testerName'] as String?, - externalSteps: (json['externalSteps'] as List?)?.cast(), - stepFolderName: json['stepFolderName'] as String?, - relativeToTestFolder: json['relativeToTestFolder'] as bool?, - addHooks: json['addHooks'] as bool?, - hookFolderName: json['hookFolderName'] as String?, - include: json['include'] is String - ? [(json['include'] as String)] - : (json['include'] as List?)?.cast(), - includeIntegrationTestBinding: - json['includeIntegrationTestBinding'] as bool?, - ); + testMethodName: json['testMethodName'] as String?, + testerType: json['testerType'] as String?, + testerName: json['testerName'] as String?, + externalSteps: (json['externalSteps'] as List?)?.cast(), + stepFolderName: json['stepFolderName'] as String?, + relativeToTestFolder: json['relativeToTestFolder'] as bool?, + addHooks: json['addHooks'] as bool?, + hookFolderName: json['hookFolderName'] as String?, + include: json['include'] is String + ? [(json['include'] as String)] + : (json['include'] as List?)?.cast(), + includeIntegrationTestBinding: + json['includeIntegrationTestBinding'] as bool?, + customHeaders: + (json['customHeaders'] as List?)?.cast() ?? []); final String stepFolder; final bool relativeToTestFolder; @@ -57,6 +60,7 @@ class GeneratorOptions { final List? include; final List externalSteps; final bool includeIntegrationTestBinding; + final List customHeaders; } Future flattenOptions(GeneratorOptions options) async { @@ -109,4 +113,5 @@ GeneratorOptions merge(GeneratorOptions a, GeneratorOptions b) => include: b.include, includeIntegrationTestBinding: a.includeIntegrationTestBinding || b.includeIntegrationTestBinding, + customHeaders: [...a.customHeaders, ...b.customHeaders], ); diff --git a/lib/src/step/generic_step.dart b/lib/src/step/generic_step.dart index 852a3b4..9f19e33 100644 --- a/lib/src/step/generic_step.dart +++ b/lib/src/step/generic_step.dart @@ -1,3 +1,4 @@ +import 'package:bdd_widget_test/src/generator_options.dart'; import 'package:bdd_widget_test/src/regex.dart'; import 'package:bdd_widget_test/src/step/bdd_step.dart'; import 'package:bdd_widget_test/src/step_generator.dart'; @@ -10,6 +11,7 @@ class GenericStep implements BddStep { this.testerType, this.customTesterName, this.hasDataTable, + this.generatorOptions, ); final String rawLine; @@ -17,18 +19,25 @@ class GenericStep implements BddStep { final String testerType; final String customTesterName; final bool hasDataTable; + final GeneratorOptions generatorOptions; @override - String get content => - '${hasDataTable ? "import 'package:bdd_widget_test/data_table.dart' as bdd;\n" : ''}' - ''' -import 'package:flutter_test/flutter_test.dart'; + String get content { + final hasCustomHeaders = generatorOptions.customHeaders.isNotEmpty; + final headerSection = hasCustomHeaders + ? generatorOptions.customHeaders.join('\n') + : "import 'package:flutter_test/flutter_test.dart';"; + + return '${hasDataTable ? "import 'package:bdd_widget_test/data_table.dart' as bdd;\n" : ''}' + ''' +$headerSection /// Usage: $rawLine Future $methodName($testerType $customTesterName${_getMethodParameters(rawLine, hasDataTable)}) async { throw UnimplementedError(); } '''; + } String _getMethodParameters(String stepLine, bool hadDataTable) { final params = parseRawStepLine(stepLine).skip(1); diff --git a/lib/src/step_file.dart b/lib/src/step_file.dart index ed9d553..ab2dd0f 100644 --- a/lib/src/step_file.dart +++ b/lib/src/step_file.dart @@ -45,6 +45,7 @@ abstract class StepFile { testerTypeTagValue, testerNameTagValue, bddLine.type == LineType.dataTableStep, + generatorOptions, ); } @@ -67,6 +68,7 @@ abstract class StepFile { testerTypeTagValue, testerNameTagValue, bddLine.type == LineType.dataTableStep, + generatorOptions, ); } } @@ -80,6 +82,7 @@ class NewStepFile extends StepFile { this.testerType, this.testerName, this.hasDataTable, + this.generatorOptions, ) : super._(); final String package; @@ -88,12 +91,14 @@ class NewStepFile extends StepFile { final String testerType; final String testerName; final bool hasDataTable; + final GeneratorOptions generatorOptions; String get dartContent => generateStepDart( package, line, testerType, testerName, hasDataTable, + generatorOptions, ); } diff --git a/lib/src/step_generator.dart b/lib/src/step_generator.dart index 4fc604f..752cceb 100644 --- a/lib/src/step_generator.dart +++ b/lib/src/step_generator.dart @@ -1,3 +1,4 @@ +import 'package:bdd_widget_test/src/generator_options.dart'; import 'package:bdd_widget_test/src/regex.dart'; import 'package:bdd_widget_test/src/step/bdd_step.dart'; import 'package:bdd_widget_test/src/step/generic_step.dart'; @@ -52,6 +53,7 @@ String generateStepDart( String testerType, String customTesterName, bool hasDataTable, + GeneratorOptions generatorOptions, ) { final methodName = getStepMethodName(line); @@ -62,6 +64,7 @@ String generateStepDart( testerType, customTesterName, hasDataTable, + generatorOptions, ); return bddStep.content; } @@ -73,6 +76,7 @@ BddStep _getStep( String testerType, String testerName, bool hasDataTable, + GeneratorOptions generatorOptions, ) { //for now, predefined steps don't support testerType final factory = predefinedSteps[methodName] ?? @@ -82,6 +86,7 @@ BddStep _getStep( testerType, testerName, hasDataTable, + generatorOptions, ); return factory(package, line); } diff --git a/pubspec.yaml b/pubspec.yaml index 05ad498..087bb22 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bdd_widget_test description: A BDD-style widget testing library. Generates Flutter widget tests from *.feature files. -version: 2.0.0 +version: 2.0.1 repository: https://github.com/olexale/bdd_widget_test issue_tracker: https://github.com/olexale/bdd_widget_test/issues diff --git a/test/custom_headers_test.dart b/test/custom_headers_test.dart new file mode 100644 index 0000000..d895545 --- /dev/null +++ b/test/custom_headers_test.dart @@ -0,0 +1,83 @@ +import 'package:bdd_widget_test/src/feature_file.dart'; +import 'package:bdd_widget_test/src/generator_options.dart'; +import 'package:bdd_widget_test/src/step_file.dart'; +import 'package:test/test.dart'; + +void main() { + test('Custom imports are added to generated steps', () { + const path = 'test'; + const featureFile = ''' +Feature: Testing feature + Scenario: Testing scenario + When I invoke test + '''; + + const expectedSteps = ''' +import 'package:flutter_test/flutter_test.dart'; +// My custom classes +import 'package:custom_package_a/custom_class_a.dart'; +import 'package:custom_package_b/custom_class_b.dart'; + +/// Usage: I invoke test +Future iInvokeTest(WidgetTester tester) async { + throw UnimplementedError(); +} +'''; + + final feature = FeatureFile( + featureDir: '$path.feature', + package: path, + input: featureFile, + generatorOptions: const GeneratorOptions( + customHeaders: [ + "import 'package:flutter_test/flutter_test.dart';", + '// My custom classes', + "import 'package:custom_package_a/custom_class_a.dart';", + "import 'package:custom_package_b/custom_class_b.dart';", + ], + ), + ); + + expect( + feature.getStepFiles().whereType().single.dartContent, + expectedSteps, + ); + }); + + test('Custom imports with data tables work correctly', () { + const path = 'test'; + const featureFile = ''' +Feature: Testing feature + Scenario: Testing scenario + Given the following data + | column1 | column2 | + | value1 | value2 | + '''; + + const expectedSteps = ''' +import 'package:bdd_widget_test/data_table.dart' as bdd; +import 'package:custom_package/custom_class.dart'; + +/// Usage: the following data +Future theFollowingData(WidgetTester tester, bdd.DataTable dataTable) async { + throw UnimplementedError(); +} +'''; + + final feature = FeatureFile( + featureDir: '$path.feature', + package: path, + input: featureFile, + generatorOptions: const GeneratorOptions( + customHeaders: [ + "import 'package:custom_package/custom_class.dart';", + ], + ), + ); + + expect( + feature.getStepFiles().whereType().single.dartContent, + expectedSteps, + ); + }); +} diff --git a/test/feature_generator_test.dart b/test/feature_generator_test.dart index d320ea8..93a93e5 100644 --- a/test/feature_generator_test.dart +++ b/test/feature_generator_test.dart @@ -99,6 +99,9 @@ stepFolderName: ./scenarios testMethodName: customName addHooks: true hookFolderName: hooksFolder +customHeaders: + - "import 'package:flutter_test/flutter_test.dart';" + - "import 'package:my_project/test_helpers.dart';" '''; fs.file('bdd_options.yaml') ..createSync() @@ -107,8 +110,8 @@ hookFolderName: hooksFolder const expected = '// GENERATED CODE - DO NOT MODIFY BY HAND\n' '// ignore_for_file: type=lint, type=warning\n' '\n' - "import 'package:flutter/material.dart';\n" "import 'package:flutter_test/flutter_test.dart';\n" + "import 'package:my_project/test_helpers.dart';\n" '\n' "import '../../hooksFolder/hooks.dart';\n" "import './scenarios/the_app_is_running.dart';\n" diff --git a/test/feature_test.dart b/test/feature_test.dart index feaea67..be66180 100644 --- a/test/feature_test.dart +++ b/test/feature_test.dart @@ -1,4 +1,5 @@ import 'package:bdd_widget_test/src/feature_file.dart'; +import 'package:bdd_widget_test/src/generator_options.dart'; import 'package:test/test.dart'; import 'util/testing_data.dart'; @@ -107,4 +108,34 @@ void main() { ); expect(feature.dartContent, expectedFeatureDart); }); + test('custom headers replace default imports in feature file', () async { + const expectedFeatureDart = ''' +${expectedComment}import 'package:patrol/patrol.dart'; +// Import flutter_test for compatibility +import 'package:flutter_test/flutter_test.dart'; + +import './step/the_app_is_running.dart'; + +void main() { + group(\'\'\'Testing feature\'\'\', () { + testWidgets(\'\'\'Testing scenario\'\'\', (tester) async { + await theAppIsRunning(tester); + }); + }); +} +'''; + + final feature = FeatureFile( + featureDir: 'test.feature', + package: 'test', + input: minimalFeatureFile, + generatorOptions: const GeneratorOptions( + customHeaders: [ + "import 'package:patrol/patrol.dart';", + '// Import flutter_test for compatibility', + "import 'package:flutter_test/flutter_test.dart';", + ], + )); + expect(feature.dartContent, expectedFeatureDart); + }); }