From 1f63ecc858b7e31dfad58b7057ddee2e840f859e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 24 Jun 2022 12:26:45 +1000 Subject: [PATCH] - Fix #231 - Removed the use of explicitly calling `pumpAndSettle` in the pre-defined steps in favour of the implicit `pumpAndSettle` calls used in the `WidgetTesterAppDriverAdapter`. - Added ability to add a `appLifecyclePumpHandler` to override the default handler that determines how the app is pumped during lifecycle events. Useful if your app has a long splash screen etc. Parameter is on `executeTestSuite`. - Added ability to ensure feature paths are relative when generating reports `useAbsolutePaths` on the `GherkinTestSuite` attribute * BREAKING CHANGE: The parameters on `executeTestSuite` are now keyed to allow for the above changes --- CHANGELOG.md | 7 + example_with_integration_test/README.md | 4 + .../gherkin/steps/expect_todos_step.dart | 2 + .../integration_test/gherkin_suite_test.dart | 9 +- .../gherkin_suite_test.g.dart | 298 ++++++++---------- .../lib/widgets/views/home_view.dart | 89 +++--- .../widget_tester_app_driver_adapter.dart | 27 +- .../gherkin_full_test_suite_annotation.dart | 4 + .../gherkin_suite_test_generator.dart | 52 ++- .../flutter_driver_test_configuration.dart | 4 +- .../flutter_test_configuration.dart | 9 +- .../gherkin_integration_test_runner.dart | 57 +++- .../steps/given_i_open_the_drawer_step.dart | 2 - .../steps/tap_text_within_widget_step.dart | 1 - .../steps/tap_widget_of_type_step.dart | 1 - .../steps/tap_widget_of_type_within_step.dart | 1 - .../steps/tap_widget_with_text_step.dart | 1 - .../flutter/steps/when_fill_field_step.dart | 2 - .../steps/when_tap_the_back_button_step.dart | 1 - .../flutter/steps/when_tap_widget_step.dart | 5 - pubspec.yaml | 2 +- 21 files changed, 295 insertions(+), 283 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88e5896..a98b387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [3.0.0-rc.11] - 24/06/2022 + - Fix #231 - Removed the use of explicitly calling `pumpAndSettle` in the pre-defined steps in favour of the implicit `pumpAndSettle` calls used in the `WidgetTesterAppDriverAdapter`. + - Added ability to add a `appLifecyclePumpHandler` to override the default handler that determines how the app is pumped during lifecycle events. Useful if your app has a long splash screen etc. Parameter is on `executeTestSuite`. + - Added ability to ensure feature paths are relative when generating reports `useAbsolutePaths` on the `GherkinTestSuite` attribute + +* BREAKING CHANGE: The parameters on `executeTestSuite` are now keyed to allow for the above changes + ## [3.0.0-rc.10] - 23/06/2022 - Fix #195: Adding missing export for `wait_until_key_exists_step.dart` diff --git a/example_with_integration_test/README.md b/example_with_integration_test/README.md index bf2e37d..97254b5 100644 --- a/example_with_integration_test/README.md +++ b/example_with_integration_test/README.md @@ -2,6 +2,10 @@ # generate the test suite flutter pub run build_runner build --delete-conflicting-outputs +# re-generate +flutter pub run build_runner clean +flutter pub run build_runner build --delete-conflicting-outputs + # run the tests flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart ``` diff --git a/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart b/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart index d2357a9..f914127 100644 --- a/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart +++ b/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart @@ -9,6 +9,8 @@ final thenIExpectTheTodos = then1( expect(context.configuration.timeout, isNotNull); expect(context.configuration.timeout!.inSeconds, 5); + await context.world.appDriver.waitForAppToSettle(); + // get the parent list final listTileFinder = context.world.appDriver.findBy( ListTile, diff --git a/example_with_integration_test/integration_test/gherkin_suite_test.dart b/example_with_integration_test/integration_test/gherkin_suite_test.dart index 802a40c..3965037 100644 --- a/example_with_integration_test/integration_test/gherkin_suite_test.dart +++ b/example_with_integration_test/integration_test/gherkin_suite_test.dart @@ -1,14 +1,17 @@ import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:gherkin/gherkin.dart'; import 'gherkin/configuration.dart'; part 'gherkin_suite_test.g.dart'; -@GherkinTestSuite() +@GherkinTestSuite( + useAbsolutePaths: false, +) void main() { executeTestSuite( - gherkinTestConfiguration, - appInitializationFn, + appMainFunction: appInitializationFn, + configuration: gherkinTestConfiguration, ); } diff --git a/example_with_integration_test/integration_test/gherkin_suite_test.g.dart b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart index a925da4..35a4738 100644 --- a/example_with_integration_test/integration_test/gherkin_suite_test.g.dart +++ b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart @@ -7,10 +7,17 @@ part of 'gherkin_suite_test.dart'; // ************************************************************************** class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { - _CustomGherkinIntegrationTestRunner( - FlutterTestConfiguration configuration, - Future Function(World) appMainFunction, - ) : super(configuration, appMainFunction); + _CustomGherkinIntegrationTestRunner({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + required Timeout scenarioExecutionTimeout, + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + }) : super( + configuration: configuration, + appMainFunction: appMainFunction, + scenarioExecutionTimeout: scenarioExecutionTimeout, + appLifecyclePumpHandler: appLifecyclePumpHandler, + ); @override void onRun() { @@ -21,34 +28,20 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { void testFeature0() { runFeature( - name: 'Swiping:', + name: 'Creating todos:', tags: ['@tag'], run: () { runScenario( - name: 'User can swipe cards left and right', - path: - 'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\swiping.feature', - tags: ['@tag', '@debug'], + name: 'User can create single todo item', + path: '.\\integration_test\\features\\create.feature', + tags: ['@tag'], steps: [ ( TestDependencies dependencies, bool skip, ) async { return await runStep( - name: - 'Given I swipe right by 250 pixels on the "scrollable cards"`', - multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Then I expect the text "Page 2" to be present', + name: 'Given I fill the "todo" field with "Buy spinach"', multiLineStrings: [], table: null, dependencies: dependencies, @@ -60,8 +53,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: - 'Given I swipe left by 250 pixels on the "scrollable cards"`', + name: 'When I tap the "add" button', multiLineStrings: [], table: null, dependencies: dependencies, @@ -73,107 +65,24 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Then I expect the text "Page 1" to be present', + name: 'Then I expect the todo list', multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ], - onBefore: () async => onBeforeRunFeature( - name: 'Swiping', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\swiping.feature', - tags: ['@tag'], - ), - onAfter: () async => onAfterRunFeature( - name: 'Swiping', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\swiping.feature', - tags: ['@tag'], - ), - ); - }, - ); - } - - void testFeature1() { - runFeature( - name: 'Checking data:', - tags: ['@tag'], - run: () { - runScenario( - name: 'User can have data', - path: - 'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\check.feature', - tags: ['@tag', '@tag1'], - steps: [ - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Given I have item with data', - multiLineStrings: [ - """{ - "glossary": { - "title": "example glossary", - "GlossDiv": { - "title": "S", - "GlossList": { - "GlossEntry": { - "ID": "SGML", - "SortAs": "SGML", - "GlossTerm": "Standard Generalized Markup Language", - "Acronym": "SGML", - "Abbrev": "ISO 8879:1986", - "GlossDef": { - "para": "A meta-markup language, used to create markup languages such as DocBook.", - "GlossSeeAlso": [ - "GML", - "XML" - ] - }, - "GlossSee": "markup" - } - } - } - } -}""" - ], - table: null, + table: GherkinTable.fromJson('[{"Todo":"Buy spinach"}]'), dependencies: dependencies, skip: skip, ); }, ], onBefore: () async => onBeforeRunFeature( - name: 'Checking data', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\check.feature', - tags: ['@tag'], - ), - onAfter: () async => onAfterRunFeature( - name: 'Checking data', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\check.feature', + name: 'Creating todos', + path: r'.\\integration_test\\features\\create.feature', tags: ['@tag'], ), ); - }, - ); - } - void testFeature2() { - runFeature( - name: 'Creating todos:', - tags: ['@tag'], - run: () { runScenario( - name: 'User can create single todo item', - path: - 'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\create.feature', + name: 'User can create multiple new todo items', + path: '.\\integration_test\\features\\create.feature', tags: ['@tag'], steps: [ ( @@ -181,7 +90,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Given I fill the "todo" field with "Buy spinach"', + name: 'Given I fill the "todo" field with "Buy carrots"', multiLineStrings: [], table: null, dependencies: dependencies, @@ -205,34 +114,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Then I expect the todo list', - multiLineStrings: [], - table: GherkinTable.fromJson('[{"Todo":"Buy spinach"}]'), - dependencies: dependencies, - skip: skip, - ); - }, - ], - onBefore: () async => onBeforeRunFeature( - name: 'Creating todos', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\create.feature', - tags: ['@tag'], - ), - ); - - runScenario( - name: 'User can create multiple new todo items', - path: - 'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\create.feature', - tags: ['@tag'], - steps: [ - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Given I fill the "todo" field with "Buy spinach"', + name: 'And I fill the "todo" field with "Buy apples"', multiLineStrings: [], table: null, dependencies: dependencies, @@ -256,9 +138,9 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Then I expect the todo list', + name: 'And I fill the "todo" field with "Buy blueberries"', multiLineStrings: [], - table: GherkinTable.fromJson('[{"Todo":"Buy spinach"}]'), + table: null, dependencies: dependencies, skip: skip, ); @@ -268,7 +150,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Given I fill the "todo" field with "Buy carrots"', + name: 'When I tap the "add" button', multiLineStrings: [], table: null, dependencies: dependencies, @@ -280,9 +162,10 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'When I tap the "add" button', + name: 'Then I expect the todo list', multiLineStrings: [], - table: null, + table: GherkinTable.fromJson( + '[{"Todo":"Buy blueberries"},{"Todo":"Buy apples"},{"Todo":"Buy carrots"}]'), dependencies: dependencies, skip: skip, ); @@ -292,7 +175,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'And I fill the "todo" field with "Buy apples"', + name: 'Given I wait 5 seconds for the animation to complete', multiLineStrings: [], table: null, dependencies: dependencies, @@ -304,19 +187,67 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'When I tap the "add" button', - multiLineStrings: [], + name: 'Given I have item with data', + multiLineStrings: [ + """{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } +}""" + ], table: null, dependencies: dependencies, skip: skip, ); }, + ], + onAfter: () async => onAfterRunFeature( + name: 'Creating todos', + path: r'.\\integration_test\\features\\create.feature', + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature1() { + runFeature( + name: 'Swiping:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can swipe cards left and right', + path: '.\\integration_test\\features\\swiping.feature', + tags: ['@tag', '@debug'], + steps: [ ( TestDependencies dependencies, bool skip, ) async { return await runStep( - name: 'And I fill the "todo" field with "Buy blueberries"', + name: + 'Given I swipe right by 250 pixels on the "scrollable cards"`', multiLineStrings: [], table: null, dependencies: dependencies, @@ -328,7 +259,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'When I tap the "add" button', + name: 'Then I expect the text "Page 2" to be present', multiLineStrings: [], table: null, dependencies: dependencies, @@ -340,10 +271,10 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Then I expect the todo list', + name: + 'Given I swipe left by 250 pixels on the "scrollable cards"`', multiLineStrings: [], - table: GherkinTable.fromJson( - '[{"Todo":"Buy blueberries"},{"Todo":"Buy apples"},{"Todo":"Buy carrots"}]'), + table: null, dependencies: dependencies, skip: skip, ); @@ -353,13 +284,39 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { bool skip, ) async { return await runStep( - name: 'Given I wait 5 seconds for the animation to complete', + name: 'Then I expect the text "Page 1" to be present', multiLineStrings: [], table: null, dependencies: dependencies, skip: skip, ); }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Swiping', + path: r'.\\integration_test\\features\\swiping.feature', + tags: ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Swiping', + path: r'.\\integration_test\\features\\swiping.feature', + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature2() { + runFeature( + name: 'Checking data:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can have data', + path: '.\\integration_test\\features\\check.feature', + tags: ['@tag', '@tag1'], + steps: [ ( TestDependencies dependencies, bool skip, @@ -399,10 +356,14 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { ); }, ], + onBefore: () async => onBeforeRunFeature( + name: 'Checking data', + path: r'.\\integration_test\\features\\check.feature', + tags: ['@tag'], + ), onAfter: () async => onAfterRunFeature( - name: 'Creating todos', - path: - r'C:\\Development\\github\\flutter_gherkin\\example_with_integration_test\\.\\integration_test\\features\\create.feature', + name: 'Checking data', + path: r'.\\integration_test\\features\\check.feature', tags: ['@tag'], ), ); @@ -411,9 +372,16 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { } } -void executeTestSuite( - FlutterTestConfiguration configuration, - Future Function(World) appMainFunction, -) { - _CustomGherkinIntegrationTestRunner(configuration, appMainFunction).run(); +void executeTestSuite({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + Timeout scenarioExecutionTimeout = const Timeout(Duration(minutes: 10)), + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, +}) { + _CustomGherkinIntegrationTestRunner( + configuration: configuration, + appMainFunction: appMainFunction, + appLifecyclePumpHandler: appLifecyclePumpHandler, + scenarioExecutionTimeout: scenarioExecutionTimeout, + ).run(); } diff --git a/example_with_integration_test/lib/widgets/views/home_view.dart b/example_with_integration_test/lib/widgets/views/home_view.dart index 7318f1d..ce05cd6 100644 --- a/example_with_integration_test/lib/widgets/views/home_view.dart +++ b/example_with_integration_test/lib/widgets/views/home_view.dart @@ -58,53 +58,10 @@ class _HomeViewState extends State with ViewUtilsMixin { if (snapshot.hasData) { final data = snapshot.data!; if (data.isEmpty) { - return Center( - child: Column( - children: [ - Icon( - Icons.list, - size: 64, - color: Colors.black26, - ), - Padding( - padding: const EdgeInsets.all(16), - // child: Text( - // 'No todos!', - // key: const Key('empty'), - // style: Theme.of(context).textTheme.headline6, - // ), - child: SizedBox( - key: const Key('scrollable cards'), - width: 300, - height: 250, - child: PageView.builder( - itemCount: 3, - itemBuilder: (ctx, index) { - return Container( - margin: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: index == 0 - ? Colors.amber - : Colors.blueAccent, - borderRadius: BorderRadius.circular(10), - ), - child: SizedBox( - key: Key('Page ${index + 1}'), - width: 200, - height: 200, - child: Center( - child: Text( - 'Page ${index + 1}', - ), - ), - ), - ); - }, - ), - ), - ), - ], - ), + return const Icon( + Icons.list, + size: 64, + color: Colors.black26, ); } else { return ListView.builder( @@ -171,6 +128,44 @@ class _HomeViewState extends State with ViewUtilsMixin { } }, ), + Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + key: const Key('scrollable cards'), + width: 300, + height: 250, + child: PageView.builder( + itemCount: 3, + itemBuilder: (ctx, index) { + return Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: index == 0 + ? Colors.amber + : Colors.blueAccent, + borderRadius: BorderRadius.circular(10), + ), + child: SizedBox( + key: Key('Page ${index + 1}'), + width: 200, + height: 200, + child: Center( + child: Text( + 'Page ${index + 1}', + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), ], ), ), diff --git a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart index 84d753c..98e9488 100644 --- a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart +++ b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart @@ -23,33 +23,31 @@ class WidgetTesterAppDriverAdapter Duration? duration = const Duration(milliseconds: 100), Duration? timeout = const Duration(seconds: 30), }) async { - try { - final pumps = await nativeDriver.pumpAndSettle( - duration ?? const Duration(milliseconds: 100), - EnginePhase.sendSemanticsUpdate, - timeout ?? const Duration(seconds: 30), - ); - - return pumps; - } catch (_) { - return 1; - } + return _implicitWait( + duration: duration, + timeout: timeout, + force: true, + ); } - Future _implicitWait({ + Future _implicitWait({ Duration? duration = const Duration(milliseconds: 100), Duration? timeout = const Duration(seconds: 30), bool? force, }) async { if (waitImplicitlyAfterAction || force == true) { try { - await nativeDriver.pumpAndSettle( + return await nativeDriver.pumpAndSettle( duration ?? const Duration(milliseconds: 100), EnginePhase.sendSemanticsUpdate, timeout ?? const Duration(seconds: 30), ); - } catch (_) {} + } catch (_) { + return 0; + } } + + return 0; } @override @@ -256,7 +254,6 @@ class WidgetTesterAppDriverAdapter Duration? timeout = const Duration(seconds: 30), }) async { await nativeDriver.ensureVisible(finder); - await waitForAppToSettle(); // must force a pump and settle to ensure the scroll is performed await _implicitWait( diff --git a/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart b/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart index f898ebe..fb5b487 100644 --- a/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart +++ b/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart @@ -12,9 +12,13 @@ class GherkinTestSuite { /// The default feature language final String featureDefaultLanguage; + /// True (the default) to use absolute file paths for reporters + final bool useAbsolutePaths; + const GherkinTestSuite({ this.executionOrder = ExecutionOrder.random, this.featureDefaultLanguage = 'en', this.featurePaths = const ['integration_test/features/**.feature'], + this.useAbsolutePaths = true, }); } diff --git a/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart b/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart index 6a2d650..90384da 100644 --- a/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart +++ b/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart @@ -29,10 +29,17 @@ class GherkinSuiteTestGenerator extends GeneratorForAnnotation { static const String template = ''' class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { - _CustomGherkinIntegrationTestRunner( - FlutterTestConfiguration configuration, - Future Function(World) appMainFunction, - ) : super(configuration, appMainFunction); + _CustomGherkinIntegrationTestRunner({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + required Timeout scenarioExecutionTimeout, + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + }) : super( + configuration: configuration, + appMainFunction: appMainFunction, + scenarioExecutionTimeout: scenarioExecutionTimeout, + appLifecyclePumpHandler: appLifecyclePumpHandler, + ); @override void onRun() { @@ -42,11 +49,18 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { {{feature_functions}} } -void executeTestSuite( - FlutterTestConfiguration configuration, - Future Function(World) appMainFunction, -) { - _CustomGherkinIntegrationTestRunner(configuration, appMainFunction).run(); +void executeTestSuite({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + Timeout scenarioExecutionTimeout = const Timeout(const Duration(minutes: 10)), + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, +}) { + _CustomGherkinIntegrationTestRunner( + configuration: configuration, + appMainFunction: appMainFunction, + appLifecyclePumpHandler: appLifecyclePumpHandler, + scenarioExecutionTimeout: scenarioExecutionTimeout, + ).run(); } '''; final _reporter = NoOpReporter(); @@ -74,6 +88,8 @@ void executeTestSuite( .map((glob) => glob.listSync().map((entity) => File(entity.path)).toList()) .reduce((value, element) => value..addAll(element)); + final useAbsolutePaths = + annotation.read('useAbsolutePaths').objectValue.toBoolValue(); if (executionOrder == ExecutionOrder.random) { featureFiles.shuffle(); @@ -87,8 +103,8 @@ void executeTestSuite( for (var featureFile in featureFiles) { final code = await generator.generate( id++, - featureFile.readAsStringSync(), - featureFile.absolute.path, + await featureFile.readAsString(), + useAbsolutePaths ?? true ? featureFile.absolute.path : featureFile.path, _languageService, _reporter, ); @@ -224,9 +240,15 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { } @override - Future visitScenario(String featureName, Iterable featureTags, - String name, Iterable tags, String path, - {required bool isFirst, required bool isLast}) async { + Future visitScenario( + String featureName, + Iterable featureTags, + String name, + Iterable tags, + String path, { + required bool isFirst, + required bool isLast, + }) async { _flushScenario(); _currentScenarioCode = _replaceVariable( scenarioTemplate, @@ -315,6 +337,8 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { 'steps', _steps.join(','), ); + + _steps.clear(); } _scenarioBuffer.writeln(_currentScenarioCode); diff --git a/lib/src/flutter/configuration/flutter_driver_test_configuration.dart b/lib/src/flutter/configuration/flutter_driver_test_configuration.dart index 0917800..cb440ef 100644 --- a/lib/src/flutter/configuration/flutter_driver_test_configuration.dart +++ b/lib/src/flutter/configuration/flutter_driver_test_configuration.dart @@ -47,9 +47,7 @@ class FlutterDriverTestConfiguration extends FlutterTestConfiguration { ); /// Provide a configuration object with default settings such as the reports and feature file location - /// Additional setting on the configuration object can be set on the returned instance. - // ignore: non_constant_identifier_names - static FlutterDriverTestConfiguration DEFAULT( + static FlutterDriverTestConfiguration standard( Iterable> steps, { String featurePath = 'features/*.*.feature', String targetAppPath = 'test_driver/app.dart', diff --git a/lib/src/flutter/configuration/flutter_test_configuration.dart b/lib/src/flutter/configuration/flutter_test_configuration.dart index c8b3035..0fc4b80 100644 --- a/lib/src/flutter/configuration/flutter_test_configuration.dart +++ b/lib/src/flutter/configuration/flutter_test_configuration.dart @@ -45,13 +45,10 @@ class FlutterTestConfiguration extends TestConfiguration { /// Defaults to false final bool waitImplicitlyAfterAction; - /// Provide a configuration object with default settings such as the reports and feature file location - /// Additional setting on the configuration object can be set on the returned instance. + /// Provide a configuration object with default settings static FlutterTestConfiguration standard( - Iterable> steps, { - String featurePath = 'integration_test/features/*.*.feature', - String targetAppPath = 'test_driver/integration_test_driver.dart', - }) { + Iterable> steps, + ) { return FlutterTestConfiguration( reporters: [ StdoutReporter(MessageLevel.error), diff --git a/lib/src/flutter/runners/gherkin_integration_test_runner.dart b/lib/src/flutter/runners/gherkin_integration_test_runner.dart index afe483d..2a86a21 100644 --- a/lib/src/flutter/runners/gherkin_integration_test_runner.dart +++ b/lib/src/flutter/runners/gherkin_integration_test_runner.dart @@ -5,12 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:collection/collection.dart'; +enum AppLifecyclePhase { + initialisation, + finalisation, +} + typedef StepFn = Future Function( TestDependencies dependencies, bool skip, ); typedef StartAppFn = Future Function(World world); +typedef AppLifecyclePumpHandlerFn = Future Function( + AppLifecyclePhase phase, + WidgetTester tester, +); class TestDependencies { final World world; @@ -27,22 +36,34 @@ abstract class GherkinIntegrationTestRunner { TagExpressionEvaluator(); final FlutterTestConfiguration configuration; final StartAppFn appMainFunction; + final AppLifecyclePumpHandlerFn? appLifecyclePumpHandler; final Timeout scenarioExecutionTimeout; final AggregatedReporter _reporter = AggregatedReporter(); - Hook? _hook; - Iterable? _executableSteps; - Iterable? _customParameters; + late final Iterable? _executableSteps; + late final Iterable? _customParameters; + late final Hook? _hook; late final IntegrationTestWidgetsFlutterBinding _binding; AggregatedReporter get reporter => _reporter; Hook get hook => _hook!; LiveTestWidgetsFlutterBindingFramePolicy? get framePolicy => null; - GherkinIntegrationTestRunner( - this.configuration, - this.appMainFunction, { - this.scenarioExecutionTimeout = const Timeout(Duration(minutes: 10)), + /// A Gherkin test runner that uses [WidgetTester] to instrument the app under test. + /// + /// [configuration] the configuration for the test run. + /// + /// [appMainFunction] a function to start the app under test. + /// + /// [appLifecyclePumpHandler] a function to determine how to pump the app during various lifecycle phases, + /// if null a default handler is used see [_appLifecyclePhasePumper]. + /// + /// [scenarioExecutionTimeout] the default execution timeout for the whole test run. + GherkinIntegrationTestRunner({ + required this.configuration, + required this.appMainFunction, + required this.scenarioExecutionTimeout, + this.appLifecyclePumpHandler, }) { configuration.prepare(); _registerReporters(configuration.reporters); @@ -225,7 +246,10 @@ abstract class GherkinIntegrationTestRunner { } // need to pump so app can finalise - await _pumpAndSettle(tester); + await _appLifecyclePhasePumper( + AppLifecyclePhase.finalisation, + tester, + ); cleanUpScenarioRun(dependencies); } @@ -251,7 +275,7 @@ abstract class GherkinIntegrationTestRunner { await appMainFunction(world); // need to pump so app is initialised - await _pumpAndSettle(tester); + await _appLifecyclePhasePumper(AppLifecyclePhase.initialisation, tester); } @protected @@ -496,11 +520,14 @@ abstract class GherkinIntegrationTestRunner { result == StepExecutionResult.timeout; } - Future _pumpAndSettle(WidgetTester tester) async { - await tester.pumpAndSettle( - const Duration(milliseconds: 200), - EnginePhase.sendSemanticsUpdate, - const Duration(milliseconds: 2000), - ); + Future _appLifecyclePhasePumper( + AppLifecyclePhase phase, + WidgetTester tester, + ) async { + if (appLifecyclePumpHandler != null) { + await appLifecyclePumpHandler!(phase, tester); + } else { + await tester.pumpAndSettle(); + } } } diff --git a/lib/src/flutter/steps/given_i_open_the_drawer_step.dart b/lib/src/flutter/steps/given_i_open_the_drawer_step.dart index bd9c921..f1c5019 100644 --- a/lib/src/flutter/steps/given_i_open_the_drawer_step.dart +++ b/lib/src/flutter/steps/given_i_open_the_drawer_step.dart @@ -37,8 +37,6 @@ StepDefinitionGeneric givenOpenDrawer() { timeout: context.configuration.timeout, ); } - - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/tap_text_within_widget_step.dart b/lib/src/flutter/steps/tap_text_within_widget_step.dart index dc5ffa5..7d51084 100644 --- a/lib/src/flutter/steps/tap_text_within_widget_step.dart +++ b/lib/src/flutter/steps/tap_text_within_widget_step.dart @@ -40,7 +40,6 @@ StepDefinitionGeneric tapTextWithinWidgetStep() { finder, timeout: timeout, ); - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/tap_widget_of_type_step.dart b/lib/src/flutter/steps/tap_widget_of_type_step.dart index cf45fb0..1832a37 100644 --- a/lib/src/flutter/steps/tap_widget_of_type_step.dart +++ b/lib/src/flutter/steps/tap_widget_of_type_step.dart @@ -19,7 +19,6 @@ StepDefinitionGeneric tapWidgetOfTypeStep() { FindType.type, ), ); - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/tap_widget_of_type_within_step.dart b/lib/src/flutter/steps/tap_widget_of_type_within_step.dart index b0d3787..d77953a 100644 --- a/lib/src/flutter/steps/tap_widget_of_type_within_step.dart +++ b/lib/src/flutter/steps/tap_widget_of_type_within_step.dart @@ -17,7 +17,6 @@ StepDefinitionGeneric tapWidgetOfTypeWithinStep() { firstMatchOnly: true, ); await context.world.appDriver.tap(finder); - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/tap_widget_with_text_step.dart b/lib/src/flutter/steps/tap_widget_with_text_step.dart index 157f1bb..3a44fe9 100644 --- a/lib/src/flutter/steps/tap_widget_with_text_step.dart +++ b/lib/src/flutter/steps/tap_widget_with_text_step.dart @@ -16,7 +16,6 @@ StepDefinitionGeneric tapWidgetWithTextStep() { final finder = context.world.appDriver.findBy(input1, FindType.text); await context.world.appDriver.scrollIntoView(finder); await context.world.appDriver.tap(finder); - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/when_fill_field_step.dart b/lib/src/flutter/steps/when_fill_field_step.dart index 814a85a..9cbb62c 100644 --- a/lib/src/flutter/steps/when_fill_field_step.dart +++ b/lib/src/flutter/steps/when_fill_field_step.dart @@ -17,8 +17,6 @@ StepDefinitionGeneric whenFillFieldStep() { finder, value, ); - - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/when_tap_the_back_button_step.dart b/lib/src/flutter/steps/when_tap_the_back_button_step.dart index 385540c..4f4846c 100644 --- a/lib/src/flutter/steps/when_tap_the_back_button_step.dart +++ b/lib/src/flutter/steps/when_tap_the_back_button_step.dart @@ -13,7 +13,6 @@ StepDefinitionGeneric whenTapBackButtonWidget() { RegExp(r'I tap the back (?:button|element|widget|icon|text)$'), (context) async { await context.world.appDriver.pageBack(); - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/lib/src/flutter/steps/when_tap_widget_step.dart b/lib/src/flutter/steps/when_tap_widget_step.dart index 43d35bc..213780e 100644 --- a/lib/src/flutter/steps/when_tap_widget_step.dart +++ b/lib/src/flutter/steps/when_tap_widget_step.dart @@ -21,18 +21,15 @@ StepDefinitionGeneric whenTapWidget() { RegExp( r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'), (key, context) async { - await context.world.appDriver.waitForAppToSettle(); final finder = context.world.appDriver.findBy(key, FindType.key); await context.world.appDriver.scrollIntoView( finder, ); - await context.world.appDriver.waitForAppToSettle(); await context.world.appDriver.tap( finder, timeout: context.configuration.timeout, ); - await context.world.appDriver.waitForAppToSettle(); }, ); } @@ -48,8 +45,6 @@ StepDefinitionGeneric whenTapWidgetWithoutScroll() { await context.world.appDriver.tap( finder, ); - - await context.world.appDriver.waitForAppToSettle(); }, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 48767e2..68b59ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_gherkin description: A Gherkin / Cucumber parser and test runner for Dart and Flutter -version: 3.0.0-rc.10 +version: 3.0.0-rc.11 homepage: https://github.com/jonsamwell/flutter_gherkin environment: