From 29eeb73f39b36c04a0843dd61b8ffdad913f0b50 Mon Sep 17 00:00:00 2001 From: Jon Samwell Date: Wed, 10 Nov 2021 11:14:17 +1100 Subject: [PATCH] - Fix: #165: Empty .feature files causing void functions which get compiled out at runtime and cause errors - Fix: #162: Incorrect feature name in HTML reports - many thanks to @AFASbart for suggesting the cause and fix. - Fix: #159: Swipe step is not working due to bad '??' statement - Fix: #155: Ensure stdout reporter only add ascii colour code when the target supports it --- .flutter-plugins-dependencies | 2 +- CHANGELOG.md | 7 + example_with_integration_test/README.md | 2 +- .../integration_test/features/create.feature | 58 ++--- .../integration_test/features/swiping.feature | 10 + .../gherkin_suite_test.g.dart | 133 ++++++++-- example_with_integration_test/lib/main.dart | 29 ++- .../lib/widgets/views/home_view.dart | 238 ++++++++++-------- example_with_integration_test/pubspec.lock | 4 +- .../widget_tester_app_driver_adapter.dart | 2 +- .../gherkin_suite_test_generator.dart | 49 ++-- lib/src/flutter/steps/swipe_step.dart | 4 +- pubspec.lock | 2 +- pubspec.yaml | 4 +- 14 files changed, 348 insertions(+), 196 deletions(-) create mode 100644 example_with_integration_test/integration_test/features/swiping.feature diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 04aaefa..ca81364 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"C:\\\\Google\\\\flutter\\\\packages\\\\integration_test\\\\","dependencies":[]}],"android":[{"name":"integration_test","path":"C:\\\\Google\\\\flutter\\\\packages\\\\integration_test\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2021-10-27 20:12:58.071212","version":"2.5.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"C:\\\\Google\\\\flutter\\\\packages\\\\integration_test\\\\","dependencies":[]}],"android":[{"name":"integration_test","path":"C:\\\\Google\\\\flutter\\\\packages\\\\integration_test\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2021-11-10 11:13:27.428521","version":"2.5.3"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c807601..fefd2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [3.0.0-rc.7] - 10/11/2021 + +- Fix: #165: Empty .feature files causing void functions which get compiled out at runtime and cause errors +- Fix: #162: Incorrect feature name in HTML reports - many thanks to @AFASbart for suggesting the cause and fix. +- Fix: #159: Swipe step is not working due to bad '??' statement +- Fix: #155: Ensure stdout reporter only add ascii colour code when the target supports it + ## [3.0.0-rc.6] - 27/10/2021 - BREAKING CHANGE: Made `appMainFunction` return a `Future` so it can be async diff --git a/example_with_integration_test/README.md b/example_with_integration_test/README.md index 596f079..bf2e37d 100644 --- a/example_with_integration_test/README.md +++ b/example_with_integration_test/README.md @@ -1,6 +1,6 @@ ``` # generate the test suite -flutter pub run build_runner build +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/features/create.feature b/example_with_integration_test/integration_test/features/create.feature index 0ca8041..c372ef2 100644 --- a/example_with_integration_test/integration_test/features/create.feature +++ b/example_with_integration_test/integration_test/features/create.feature @@ -23,32 +23,32 @@ Feature: Creating todos | Buy apples | | Buy carrots | Given I wait 5 seconds for the animation to complete -# Given I have item with data -# """ -# { -# "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" -# } -# } -# } -# } -# } -# """ -# When I test the default step timeout is not applied to step with custom timeout \ No newline at end of file + Given I have item with data + """ + { + "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" + } + } + } + } + } + """ + # When I test the default step timeout is not applied to step with custom timeout \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/swiping.feature b/example_with_integration_test/integration_test/features/swiping.feature new file mode 100644 index 0000000..efcb2d6 --- /dev/null +++ b/example_with_integration_test/integration_test/features/swiping.feature @@ -0,0 +1,10 @@ +@tag +Feature: Swiping + + @debug + Scenario: User can swipe cards left and right + Given I swipe right by 250 pixels on the "scrollable cards"` + Then Then I expect the text "Page 2" to be present + + Given I swipe left by 250 pixels on the "scrollable cards"` + Then Then I expect the text "Page 1" to be present \ No newline at end of file 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 d63af46..c3df1c8 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 @@ -16,9 +16,113 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { void onRun() { testFeature0(); testFeature1(); + testFeature2(); } void testFeature0() { + runFeature( + 'Swiping:', + ['@tag'], + () { + runScenario( + 'User can swipe cards left and right', + ['@tag', '@debug'], + (TestDependencies dependencies) async { + await runStep( + 'Given I swipe right by 250 pixels on the "scrollable cards"`', + [], + null, + dependencies, + ); + + await runStep( + 'Then Then I expect the text "Page 2" to be present', + [], + null, + dependencies, + ); + + await runStep( + 'Given I swipe left by 250 pixels on the "scrollable cards"`', + [], + null, + dependencies, + ); + + await runStep( + 'Then Then I expect the text "Page 1" to be present', + [], + null, + dependencies, + ); + }, + onBefore: () async => onBeforeRunFeature( + 'Swiping', + ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + 'Swiping', + ), + ); + }, + ); + } + + void testFeature1() { + runFeature( + 'Checking data:', + ['@tag'], + () { + runScenario( + 'User can have data', + ['@tag', '@tag1'], + (TestDependencies dependencies) async { + await runStep( + 'Given I have item with data', + [ + """{ + "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" + } + } + } + } +}""" + ], + null, + dependencies, + ); + }, + onBefore: () async => onBeforeRunFeature( + 'Checking data', + ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + 'Checking data', + ), + ); + }, + ); + } + + void testFeature2() { runFeature( 'Creating todos:', ['@tag'], @@ -49,8 +153,8 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { ); }, onBefore: () async => onBeforeRunFeature( - 'User can create a new todo item', - ['@tag', '@tag1', '@tag_two'], + 'Creating todos', + ['@tag'], ), onAfter: null, ); @@ -115,25 +219,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { null, dependencies, ); - }, - onBefore: null, - onAfter: () async => onAfterRunFeature( - 'User can create multiple new todo items', - ), - ); - }, - ); - } - void testFeature1() { - runFeature( - 'Checking data:', - ['@tag'], - () { - runScenario( - 'User can have data', - ['@tag', '@tag1'], - (TestDependencies dependencies) async { await runStep( 'Given I have item with data', [ @@ -167,12 +253,9 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { dependencies, ); }, - onBefore: () async => onBeforeRunFeature( - 'User can have data', - ['@tag', '@tag1'], - ), + onBefore: null, onAfter: () async => onAfterRunFeature( - 'User can have data', + 'Creating todos', ), ); }, diff --git a/example_with_integration_test/lib/main.dart b/example_with_integration_test/lib/main.dart index 60dce68..3358d54 100644 --- a/example_with_integration_test/lib/main.dart +++ b/example_with_integration_test/lib/main.dart @@ -26,22 +26,29 @@ class TodoApp extends StatelessWidget { @override Widget build(BuildContext context) { - final app = MaterialApp( - title: 'Todo App', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: injector.get(), - ); - // if the app is in test mode and an ExternalApplicationManager is passed in // let the app respond to external changes return externalApplicationManager == null - ? app + ? MaterialApp( + title: 'Todo App', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: injector.get(), + ) : StreamBuilder( stream: externalApplicationManager!.applicationReset, - builder: (ctx, _) => app, + builder: (ctx, _) { + return MaterialApp( + title: 'Todo App', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: injector.get(), + ); + }, ); } } 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 3557eba..fbbaf8c 100644 --- a/example_with_integration_test/lib/widgets/views/home_view.dart +++ b/example_with_integration_test/lib/widgets/views/home_view.dart @@ -33,115 +33,147 @@ class _HomeViewState extends State with ViewUtilsMixin { ), ), body: SafeArea( - child: Column( - children: [ - SizedBox( - height: 100, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: AddTodoComponent( - todo: bloc.newModel, - onAdded: (todo) { - subscribeOnce(bloc.add(todo)); - }, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 100, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: AddTodoComponent( + todo: bloc.newModel, + onAdded: (todo) { + subscribeOnce(bloc.add(todo)); + }, + ), ), ), - ), - SizedBox( - height: 16, - ), - StreamBuilder>( - stream: bloc.todos, - builder: (_, snapshot) { - 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, + SizedBox( + height: 16, + ), + StreamBuilder>( + stream: bloc.todos, + builder: (_, snapshot) { + 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}', + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } else { + return ListView.builder( + shrinkWrap: true, + itemCount: data.length, + itemBuilder: (ctx, index) { + final todo = data.elementAt(index); + return Dismissible( + background: Container( + color: Colors.red, + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Icon( + Icons.delete, + color: Colors.white, + ), + Icon( + Icons.delete, + color: Colors.white, + ), + ], + ), + ), + key: Key(todo.action!), + onDismissed: (direction) { + subscribeOnce( + bloc.remove(todo), + onDone: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Todo deleted'), + ), + ); + }, + ); + }, + child: ListTile( + title: Text( + todo.action!, + style: Theme.of(context) + .textTheme + .bodyText1! + .copyWith( + decoration: + todo.status == TodoStatus.complete + ? TextDecoration.lineThrough + : null, + ), + ), + ), + ); + }, + ); + } } else { - return ListView.builder( - shrinkWrap: true, - itemCount: data.length, - itemBuilder: (ctx, index) { - final todo = data.elementAt(index); - return Dismissible( - background: Container( - color: Colors.red, - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon( - Icons.delete, - color: Colors.white, - ), - Icon( - Icons.delete, - color: Colors.white, - ), - ], - ), - ), - key: Key(todo.action!), - onDismissed: (direction) { - subscribeOnce( - bloc.remove(todo), - onDone: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Todo deleted'), - ), - ); - }, - ); - }, - child: ListTile( - title: Text( - todo.action!, - style: Theme.of(context) - .textTheme - .bodyText1! - .copyWith( - decoration: - todo.status == TodoStatus.complete - ? TextDecoration.lineThrough - : null, - ), - ), - ), - ); - }, + return Center( + child: Text('Loading...'), ); } - } else { - return Center( - child: Text('Loading...'), - ); - } - }, - ), - ], + }, + ), + ], + ), ), ), ); diff --git a/example_with_integration_test/pubspec.lock b/example_with_integration_test/pubspec.lock index 47db601..3dd4db1 100644 --- a/example_with_integration_test/pubspec.lock +++ b/example_with_integration_test/pubspec.lock @@ -213,7 +213,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0-rc.6" + version: "3.0.0-rc.7" flutter_simple_dependency_injection: dependency: "direct main" description: @@ -249,7 +249,7 @@ packages: name: gherkin url: "https://pub.dartlang.org" source: hosted - version: "2.0.5+1" + version: "2.0.7" glob: dependency: transitive description: 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 7c7057a..06b7792 100644 --- a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart +++ b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart @@ -151,7 +151,7 @@ class WidgetTesterAppDriverAdapter }) async { final scrollableFinder = findByDescendant( finder, - find.byType(Scrollable).first, + find.byType(Scrollable), matchRoot: true, ); final state = nativeDriver.state(scrollableFinder) as ScrollableState; 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 d53e645..b9203a2 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 @@ -149,10 +149,10 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { ); '''; static const String ON_BEFORE_SCENARIO_RUN = ''' - () async => onBeforeRunFeature('{{scenario_name}}', {{tags}},) + () async => onBeforeRunFeature('{{feature_name}}', {{feature_tags}},) '''; static const String ON_AFTER_SCENARIO_RUN = ''' - () async => onAfterRunFeature('{{scenario_name}}',) + () async => onAfterRunFeature('{{feature_name}}',) '''; final StringBuffer _buffer = StringBuffer(); @@ -188,26 +188,31 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { String name, String? description, Iterable tags, + int childScenarioCount, ) async { - _currentFeatureCode = _replaceVariable( - FUNCTION_TEMPLATE, - 'feature_number', - _id.toString(), - ); - _currentFeatureCode = _replaceVariable( - _currentFeatureCode!, - 'feature_name', - _escapeText(name), - ); - _currentFeatureCode = _replaceVariable( - _currentFeatureCode!, - 'tags', - '[${tags.map((e) => "'$e'").join(', ')}]', - ); + if (childScenarioCount > 0) { + _currentFeatureCode = _replaceVariable( + FUNCTION_TEMPLATE, + 'feature_number', + _id.toString(), + ); + _currentFeatureCode = _replaceVariable( + _currentFeatureCode!, + 'feature_name', + _escapeText(name), + ); + _currentFeatureCode = _replaceVariable( + _currentFeatureCode!, + 'tags', + '[${tags.map((e) => "'$e'").join(', ')}]', + ); + } } @override Future visitScenario( + String featureName, + Iterable featureTags, String name, Iterable tags, bool isFirst, @@ -224,6 +229,16 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { 'onAfter', isLast ? ON_AFTER_SCENARIO_RUN : 'null', ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'feature_name', + _escapeText(featureName), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'feature_tags', + '[${featureTags.map((e) => "'$e'").join(', ')}]', + ); _currentScenarioCode = _replaceVariable( _currentScenarioCode!, 'scenario_name', diff --git a/lib/src/flutter/steps/swipe_step.dart b/lib/src/flutter/steps/swipe_step.dart index 36f0b89..badecc7 100644 --- a/lib/src/flutter/steps/swipe_step.dart +++ b/lib/src/flutter/steps/swipe_step.dart @@ -18,7 +18,6 @@ mixin _SwipeHelper await world.appDriver.scroll( finder, dx: offset.toDouble(), - dy: 0, duration: Duration(milliseconds: 500), timeout: timeout, ); @@ -28,7 +27,6 @@ mixin _SwipeHelper await world.appDriver.scroll( finder, - dx: 0, dy: offset.toDouble(), duration: Duration(milliseconds: 500), timeout: timeout, @@ -58,7 +56,7 @@ class SwipeOnKeyStep @override RegExp get pattern => - RegExp(r'I swipe {swipe_direction} by {int} pixels on the {string}$'); + RegExp(r'I swipe {swipe_direction} by {int} pixels on the {string}'); } /// Swipes in a cardinal direction on a widget discovered by its test. diff --git a/pubspec.lock b/pubspec.lock index 6fb59a7..a03e544 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -160,7 +160,7 @@ packages: name: gherkin url: "https://pub.dartlang.org" source: hosted - version: "2.0.5+1" + version: "2.0.7" glob: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index de8ba8e..885c3d0 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.6 +version: 3.0.0-rc.7 homepage: https://github.com/jonsamwell/flutter_gherkin environment: @@ -18,7 +18,7 @@ dependencies: sdk: flutter analyzer: ">=1.7.1 <3.0.0" collection: ^1.15.0 - gherkin: ^2.0.5+1 + gherkin: ^2.0.7 source_gen: ^1.1.1 build: ^2.1.1 glob: ^2.0.2