feat(steps): add more pre-defined steps and more Flutter driver utility methods

This commit is contained in:
Jon Samwell 2018-10-29 16:29:11 +11:00
parent a99e79938f
commit afb6677eff
16 changed files with 174 additions and 32 deletions

View File

@ -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

View File

@ -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)
<!-- /TOC -->
@ -67,7 +68,7 @@ This implementation of the Gherkin tries to follow as closely as possible other
See <https://docs.cucumber.io/gherkin/> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<Color> {
enum Colour { red, green, blue }
class ColourParameter extends CustomParameter<Colour> {
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<Color> {
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<Color> {
class GivenIPickAColour extends Given1<Colour> {
@override
Future<void> executeStep(Color input1) async {
Future<void> 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 <lib/src/flutter/utils/driver_utils.dart>
### 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!

View File

@ -8,7 +8,7 @@ import 'steps/tap_button_n_times_step.dart';
Future<void> 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()]

View File

@ -203,6 +203,7 @@ class FeatureFileRunner {
}
}
""";
_reporter.message(message, MessageLevel.error);
throw new GherkinStepNotDefinedException(message);
}

View File

@ -16,15 +16,14 @@ class GivenOpenDrawer extends Given1WithWorld<String, FlutterWorld> {
Future<void> 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);
}
}
}

View File

@ -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<void> 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(

View File

@ -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<String, String, FlutterWorld> {
@override
Future<void> 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}");
}

View File

@ -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<int> {
WhenPauseStep()
: super(StepDefinitionConfiguration()..timeout = Duration(minutes: 5));
@override
Future<void> executeStep(int seconds) async {
await Future.delayed(Duration(seconds: seconds));
}
@override
RegExp get pattern => RegExp(r"I pause for {int} seconds");
}

View File

@ -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<String, FlutterWorld> {
@override
Future<void> 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);
}
}

View File

@ -1,13 +1,53 @@
import 'package:flutter_driver/flutter_driver.dart';
class FlutterDriverUtils {
Future<bool> isPresent(SerializableFinder finder, FlutterDriver driver,
static Future<bool> 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<bool> 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<bool> waitForFlutter(FlutterDriver driver,
{Duration timeout = const Duration(seconds: 30)}) async {
try {
await driver.waitUntilNoTransientCallbacks(timeout: timeout);
return true;
} catch (_) {
return false;
}
}
static Future<void> 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<String> 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<void> tap(FlutterDriver driver, SerializableFinder finder,
{Duration timeout = const Duration(seconds: 30)}) async {
await driver.tap(finder, timeout: timeout);
await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout);
}
}

View File

@ -7,6 +7,16 @@ abstract class And extends StepDefinition<World> {
And([StepDefinitionConfiguration configuration]) : super(configuration);
}
abstract class AndWithWorld<TWorld extends World>
extends StepDefinition<TWorld> {
AndWithWorld([StepDefinitionConfiguration configuration])
: super(configuration);
@override
StepDefinitionCode get code => () async => await executeStep();
Future<void> executeStep();
}
abstract class And1WithWorld<TInput1, TWorld extends World>
extends StepDefinition1<TWorld, TInput1> {
And1WithWorld([StepDefinitionConfiguration configuration])

View File

@ -7,6 +7,16 @@ abstract class But extends StepDefinition<World> {
But([StepDefinitionConfiguration configuration]) : super(configuration);
}
abstract class ButWithWorld<TWorld extends World>
extends StepDefinition<TWorld> {
ButWithWorld([StepDefinitionConfiguration configuration])
: super(configuration);
@override
StepDefinitionCode get code => () async => await executeStep();
Future<void> executeStep();
}
abstract class But1WithWorld<TInput1, TWorld extends World>
extends StepDefinition1<TWorld, TInput1> {
But1WithWorld([StepDefinitionConfiguration configuration])

View File

@ -7,6 +7,16 @@ abstract class Then extends StepDefinition<World> {
Then([StepDefinitionConfiguration configuration]) : super(configuration);
}
abstract class ThenWithWorld<TWorld extends World>
extends StepDefinition<TWorld> {
ThenWithWorld([StepDefinitionConfiguration configuration])
: super(configuration);
@override
StepDefinitionCode get code => () async => await executeStep();
Future<void> executeStep();
}
abstract class Then1WithWorld<TInput1, TWorld extends World>
extends StepDefinition1<TWorld, TInput1> {
Then1WithWorld([StepDefinitionConfiguration configuration])

View File

@ -7,6 +7,16 @@ abstract class When extends StepDefinition<World> {
When([StepDefinitionConfiguration configuration]) : super(configuration);
}
abstract class WhenWithWorld<TWorld extends World>
extends StepDefinition<TWorld> {
WhenWithWorld([StepDefinitionConfiguration configuration])
: super(configuration);
@override
StepDefinitionCode get code => () async => await executeStep();
Future<void> executeStep();
}
abstract class When1WithWorld<TInput1, TWorld extends World>
extends StepDefinition1<TWorld, TInput1> {
When1WithWorld([StepDefinitionConfiguration configuration])

View File

@ -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:

View File

@ -18,7 +18,7 @@ class ReporterMock extends Reporter {
OnStepFinished onStepFinishedFn;
Future<void> onTestRunStarted() async => onTestRunStartedInvocationCount += 1;
Future<void> onTestRunfinished() async =>
Future<void> onTestRunFinished() async =>
onTestRunfinishedInvocationCount += 1;
Future<void> onFeatureStarted(StartedMessage message) async =>
onFeatureStartedInvocationCount += 1;