diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb86fc..f5d2ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.0.3] - 29/10/2018 + +* Added more pre-defined flutter step definitions +* Added more Flutter driver util methods to abstract common functionality like entering text into a control and tapping a button. + ## [0.0.2] - 29/10/2018 * Fixed up dependencies diff --git a/README.md b/README.md index 2e0613e..e51190f 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ This implementation of the Gherkin tries to follow as closely as possible other - [Restarting the app before each test](#restarting-the-app-before-each-test) - [Flutter World](#flutter-world) - [Pre-defined Steps](#pre-defined-steps) + - [Flutter Driver Utilities](#flutter-driver-utilities) - [Debugging](#debugging) @@ -67,7 +68,7 @@ This implementation of the Gherkin tries to follow as closely as possible other See for information on the Gherkin syntax and Behaviour Driven Development (BDD). -The first step is to create a version of your app that has flutter driver enabled so that it can be automated. A good guide how to do this is show [here](flutter.io/cookbook/testing/integration-test-introduction/#4-instrument-the-app). However in short, create a folder called `test_driver` and within that create a file called `app.dart` and paste in the below code. +The first step is to create a version of your app that has flutter driver enabled so that it can be automated. A good guide how to do this is show [here](https://flutter.io/cookbook/testing/integration-test-introduction/#4-instrument-the-app). However in short, create a folder called `test_driver` and within that create a file called `app.dart` and paste in the below code. ```dart import '../lib/main.dart'; @@ -145,8 +146,8 @@ import 'package:flutter_gherkin/flutter_gherkin.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] - ..reporters = [StdoutReporter()] + ..features = [Glob(r"test_driver/features/**/*.feature")] + ..reporters = [ProgressReporter()] ..restartAppBetweenScenarios = true ..targetAppPath = "test_driver/app.dart" ..exitAfterTestRun = true; @@ -154,7 +155,7 @@ Future main() { } ``` -This code simple creates a configuration object and calls this library which will then promptly parse your feature files and run the tests. The configuration file is important and explained in further detail below. However, all that is happening is a `Glob` is provide which specifies the path to one or more feature files, it sets the reporter to the `StdoutReporter` report which mean prints to the standard output (console). Finally it specifies the path to the testable app created above `test_driver/app.dart`. This is important as it instructions the library which app to run the tests against. +This code simple creates a configuration object and calls this library which will then promptly parse your feature files and run the tests. The configuration file is important and explained in further detail below. However, all that is happening is a `Glob` is provide which specifies the path to one or more feature files, it sets the reporter to the `ProgressReporter` report which mean prints to the result of a scenario and step to the standard output (console). Finally it specifies the path to the testable app created above `test_driver/app.dart`. This is important as it instructions the library which app to run the tests against. Finally to actually run the tests run the below on the command line: @@ -205,7 +206,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] + ..features = [Glob(r"test_driver/features/**/*.feature")] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..restartAppBetweenScenarios = true @@ -231,7 +232,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] + ..features = [Glob(r"test_driver/features/**/*.feature")] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] @@ -269,7 +270,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] + ..features = [Glob(r"test_driver/features/**/*.feature")] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] @@ -296,7 +297,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] + ..features = [Glob(r"test_driver/features/**/*.feature")] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] @@ -528,20 +529,20 @@ While the well know step parameter will be sufficient in most cases there are ti The below custom parameter defines a regex that matches the words "red", "green" or "blue". The matches word is passed into the function which is then able to convert the string into a Color object. The name of the custom parameter is used to identity the parameter within the step text. In the below example the word "colour" is used. This is combined with the pre / post prefixes (which default to "{" and "}") to match to the custom parameter. ```dart -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; -class ColourParameter extends CustomParameter { +enum Colour { red, green, blue } + +class ColourParameter extends CustomParameter { ColourParameter() - : super("colour", RegExp(r"(red|green|blue)"), (c) { + : super("colour", RegExp(r"red|green|blue", caseSensitive: true), (c) { switch (c.toLowerCase()) { case "red": - return Colors.red; + return Colour.red; case "green": - return Colors.green; + return Colour.green; case "blue": - return Colors.blue; + return Colour.blue; } }); } @@ -550,18 +551,17 @@ class ColourParameter extends CustomParameter { The step definition would then use this custom parameter like so: ```dart -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'colour_parameter.dart'; -class GivenIPickAColour extends Given1 { +class GivenIPickAColour extends Given1 { @override - Future executeStep(Color input1) async { + Future executeStep(Colour input1) async { // TODO: implement executeStep } @override - RegExp get pattern => RegExp(r"I pick the colour {colour}"); + RegExp get pattern => RegExp(r"I pick a {colour}"); } ``` @@ -702,6 +702,21 @@ You might additionally want to do some clean-up of your app after each test by i ### Pre-defined Steps +For convenience the library defines a number of pre-defined steps so you can get going much quicker without having to implement lots of step classes. The pre-defined steps are: + +| Step Text | Description | Examples | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| I tap the {string} [button\|element\|label\|icon\|field\|text\|\widget] | Taps the element with the provided key ( given by the first input parameter) | `When I tap the "login" button`, `Then I tap the "save" icon` | +| I fill the {string} field with {string} | Fills the element with the provided key with the given value (given by the second input parameter) | `When I fill the "email" field with "someone@gmail.com"` | +| I expect the {string} to be {string} | Asserts that the element with the given key has the given string value | `Then I expect the "cost" to be "£10.95"` | +| I (open\|close) the drawer | Opens or closes the application default drawer | `When I open the drawer`, `And I close the drawer` | +| I pause for {int} seconds | Pauses the test execution for the given seconds. Only use in debug scenarios or to inspect the state of the app | `Then I pause for 20 seconds` | + +#### Flutter Driver Utilities + +For convenience the library provides a static `FlutterDriverUtils` class that abstracts away some common Flutter driver functionality like tapping a button, getting and entering text, checking if an element is present or absent. See + + ### Debugging In VSCode simply add add this block to your launch.json file (if you testable app is called `app_test.dart` and within the `test_driver` folder, if not replace that with the correct file path). Don't forget to put a break point somewhere! diff --git a/example/test_driver/app_test.dart b/example/test_driver/app_test.dart index 5184d49..4fbbfce 100644 --- a/example/test_driver/app_test.dart +++ b/example/test_driver/app_test.dart @@ -8,7 +8,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/*.feature")] + ..features = [Glob(r"test_driver/features/**/*.feature")] ..reporters = [ProgressReporter()] ..hooks = [HookExample()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] diff --git a/lib/src/feature_file_runner.dart b/lib/src/feature_file_runner.dart index c1b0553..6cbda94 100644 --- a/lib/src/feature_file_runner.dart +++ b/lib/src/feature_file_runner.dart @@ -203,6 +203,7 @@ class FeatureFileRunner { } } """; + _reporter.message(message, MessageLevel.error); throw new GherkinStepNotDefinedException(message); } 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 c50050d..855425d 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 @@ -16,15 +16,14 @@ class GivenOpenDrawer extends Given1WithWorld { Future executeStep(String action) async { final drawerFinder = find.byType("Drawer"); final isOpen = - await FlutterDriverUtils().isPresent(drawerFinder, world.driver); + await FlutterDriverUtils.isPresent(drawerFinder, world.driver); // https://github.com/flutter/flutter/issues/9002#issuecomment-293660833 if (isOpen && action == "close") { // Swipe to the left across the whole app to close the drawer await world.driver .scroll(drawerFinder, -300.0, 0.0, Duration(milliseconds: 300)); } else if (!isOpen && action == "open") { - final locator = find.byTooltip("Open navigation menu"); - await world.driver.tap(locator, timeout: timeout); + await FlutterDriverUtils.tap(world.driver, find.byTooltip("Open navigation menu"), timeout: timeout); } } } diff --git a/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart b/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart index c023ee3..33b5795 100644 --- a/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart +++ b/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart @@ -1,4 +1,5 @@ import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; import 'package:flutter_gherkin/src/gherkin/steps/then.dart'; import 'package:flutter_gherkin/src/reporters/message_level.dart'; import 'package:flutter_driver/flutter_driver.dart'; @@ -20,9 +21,8 @@ class ThenExpectElementToHaveValue @override Future executeStep(String key, String value) async { - final locator = find.byValueKey(key); try { - final text = await world.driver.getText(locator, timeout: timeout); + final text = await FlutterDriverUtils.getText(world.driver, find.byValueKey(key), timeout: timeout); expect(text, value); } catch (e) { await reporter.message( diff --git a/lib/src/flutter/steps/when_fill_field_step.dart b/lib/src/flutter/steps/when_fill_field_step.dart new file mode 100644 index 0000000..3f35749 --- /dev/null +++ b/lib/src/flutter/steps/when_fill_field_step.dart @@ -0,0 +1,19 @@ +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/when.dart'; + +/// Enters the given text into the widget with the key provided +/// +/// Examples: +/// Then I fill the "email" field with "bob@gmail.com" +/// Then I fill the "name" field with "Woody Johnson" +class WhenFillFieldStep extends When2WithWorld { + @override + Future executeStep(String key, String input2) async { + await FlutterDriverUtils.enterText(world.driver, find.byValueKey(key), input2); + } + + @override + RegExp get pattern => RegExp(r"I fill the {string} field with {string}"); +} diff --git a/lib/src/flutter/steps/when_pause_step.dart b/lib/src/flutter/steps/when_pause_step.dart new file mode 100644 index 0000000..1e365bf --- /dev/null +++ b/lib/src/flutter/steps/when_pause_step.dart @@ -0,0 +1,22 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/when.dart'; + +/// Pauses the execution for the provided number of seconds. +/// Handy when you want to pause to check something. +/// Note: this should only be used during development as having to pause during a test usually indicates timing issues +/// +/// Examples: +/// When I pause for 10 seconds +/// When I pause for 120 seconds +class WhenPauseStep extends When1 { + WhenPauseStep() + : super(StepDefinitionConfiguration()..timeout = Duration(minutes: 5)); + + @override + Future executeStep(int seconds) async { + await Future.delayed(Duration(seconds: seconds)); + } + + @override + RegExp get pattern => RegExp(r"I pause for {int} seconds"); +} diff --git a/lib/src/flutter/steps/when_tap_widget_step.dart b/lib/src/flutter/steps/when_tap_widget_step.dart index f675633..4277ef5 100644 --- a/lib/src/flutter/steps/when_tap_widget_step.dart +++ b/lib/src/flutter/steps/when_tap_widget_step.dart @@ -1,4 +1,5 @@ import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; import 'package:flutter_gherkin/src/gherkin/steps/when.dart'; import 'package:flutter_driver/flutter_driver.dart'; @@ -23,7 +24,7 @@ class WhenTapWidget extends When1WithWorld { @override Future executeStep(String key) async { - final locator = find.byValueKey(key); - await world.driver.tap(locator, timeout: timeout); + await FlutterDriverUtils.tap(world.driver, find.byValueKey(key), + timeout: timeout); } } diff --git a/lib/src/flutter/utils/driver_utils.dart b/lib/src/flutter/utils/driver_utils.dart index 2ad7b19..c62c371 100644 --- a/lib/src/flutter/utils/driver_utils.dart +++ b/lib/src/flutter/utils/driver_utils.dart @@ -1,13 +1,53 @@ import 'package:flutter_driver/flutter_driver.dart'; class FlutterDriverUtils { - Future isPresent(SerializableFinder finder, FlutterDriver driver, + static Future isPresent(SerializableFinder finder, FlutterDriver driver, {Duration timeout = const Duration(seconds: 1)}) async { try { await driver.waitFor(finder, timeout: timeout); return true; - } catch (e) { + } catch (_) { return false; } } + + static Future isAbsent(FlutterDriver driver, SerializableFinder finder, + {Duration timeout = const Duration(seconds: 30)}) async { + try { + await driver.waitForAbsent(finder, timeout: timeout); + return true; + } catch (_) { + return false; + } + } + + static Future waitForFlutter(FlutterDriver driver, + {Duration timeout = const Duration(seconds: 30)}) async { + try { + await driver.waitUntilNoTransientCallbacks(timeout: timeout); + return true; + } catch (_) { + return false; + } + } + + static Future enterText( + FlutterDriver driver, SerializableFinder finder, String text, + {Duration timeout = const Duration(seconds: 30)}) async { + await FlutterDriverUtils.tap(driver, finder, timeout: timeout); + await driver.enterText(text, timeout: timeout); + } + + static Future getText(FlutterDriver driver, SerializableFinder finder, + {Duration timeout = const Duration(seconds: 30)}) async { + await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout); + final text = driver.getText(finder, timeout: timeout); + return text; + } + + static Future tap(FlutterDriver driver, SerializableFinder finder, + {Duration timeout = const Duration(seconds: 30)}) async { + await driver.tap(finder, timeout: timeout); + await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout); + } } diff --git a/lib/src/gherkin/steps/and.dart b/lib/src/gherkin/steps/and.dart index d90d963..4b9b2b1 100644 --- a/lib/src/gherkin/steps/and.dart +++ b/lib/src/gherkin/steps/and.dart @@ -7,6 +7,16 @@ abstract class And extends StepDefinition { And([StepDefinitionConfiguration configuration]) : super(configuration); } +abstract class AndWithWorld + extends StepDefinition { + AndWithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode get code => () async => await executeStep(); + + Future executeStep(); +} + abstract class And1WithWorld extends StepDefinition1 { And1WithWorld([StepDefinitionConfiguration configuration]) diff --git a/lib/src/gherkin/steps/but.dart b/lib/src/gherkin/steps/but.dart index 4f83fe0..7e290b2 100644 --- a/lib/src/gherkin/steps/but.dart +++ b/lib/src/gherkin/steps/but.dart @@ -7,6 +7,16 @@ abstract class But extends StepDefinition { But([StepDefinitionConfiguration configuration]) : super(configuration); } +abstract class ButWithWorld + extends StepDefinition { + ButWithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode get code => () async => await executeStep(); + + Future executeStep(); +} + abstract class But1WithWorld extends StepDefinition1 { But1WithWorld([StepDefinitionConfiguration configuration]) diff --git a/lib/src/gherkin/steps/then.dart b/lib/src/gherkin/steps/then.dart index 47a47cf..918a384 100644 --- a/lib/src/gherkin/steps/then.dart +++ b/lib/src/gherkin/steps/then.dart @@ -7,6 +7,16 @@ abstract class Then extends StepDefinition { Then([StepDefinitionConfiguration configuration]) : super(configuration); } +abstract class ThenWithWorld + extends StepDefinition { + ThenWithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode get code => () async => await executeStep(); + + Future executeStep(); +} + abstract class Then1WithWorld extends StepDefinition1 { Then1WithWorld([StepDefinitionConfiguration configuration]) diff --git a/lib/src/gherkin/steps/when.dart b/lib/src/gherkin/steps/when.dart index eccb06b..9dd2d2d 100644 --- a/lib/src/gherkin/steps/when.dart +++ b/lib/src/gherkin/steps/when.dart @@ -7,6 +7,16 @@ abstract class When extends StepDefinition { When([StepDefinitionConfiguration configuration]) : super(configuration); } +abstract class WhenWithWorld + extends StepDefinition { + WhenWithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode get code => () async => await executeStep(); + + Future executeStep(); +} + abstract class When1WithWorld extends StepDefinition1 { When1WithWorld([StepDefinitionConfiguration configuration]) diff --git a/pubspec.yaml b/pubspec.yaml index 28c37c6..faefc0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: path: ^1.6.2 glob: ^1.1.7 test: ^1.3.0 - matcher: 0.12.3+1 + matcher: ^0.12.3+1 flutter_test: sdk: flutter flutter_driver: diff --git a/test/mocks/reporter_mock.dart b/test/mocks/reporter_mock.dart index aff3414..6432b8f 100644 --- a/test/mocks/reporter_mock.dart +++ b/test/mocks/reporter_mock.dart @@ -18,7 +18,7 @@ class ReporterMock extends Reporter { OnStepFinished onStepFinishedFn; Future onTestRunStarted() async => onTestRunStartedInvocationCount += 1; - Future onTestRunfinished() async => + Future onTestRunFinished() async => onTestRunfinishedInvocationCount += 1; Future onFeatureStarted(StartedMessage message) async => onFeatureStartedInvocationCount += 1;