feat(steps): update lib to use new step function syntax

This commit is contained in:
Jon Samwell 2020-07-19 11:28:52 +10:00
parent b2cef2e4ec
commit 9ba831a695
26 changed files with 316 additions and 379 deletions

View File

@ -1,3 +1,6 @@
## [1.1.8+3] - 19/07/2020
* Updated Gherkin library version to allow for function step step implementation; updated docs to match.
## [1.1.8+2] - 11/05/2020 ## [1.1.8+2] - 11/05/2020
* Fixed issue where the connection attempt of Flutter driver would not retry before throwing a connection error. This was causing an error on some machines trying to connect to an Android emulator (x86 & x86_64) that runs the googleapis (see https://github.com/flutter/flutter/issues/42433) * Fixed issue where the connection attempt of Flutter driver would not retry before throwing a connection error. This was causing an error on some machines trying to connect to an Android emulator (x86 & x86_64) that runs the googleapis (see https://github.com/flutter/flutter/issues/42433)
* Added a before `onBeforeFlutterDriverConnect` and after `onAfterFlutterDriverConnect` Flutter driver connection method property to the test configuration `FlutterTestConfiguration` to enable custom logic before and after a driver connection attempt. * Added a before `onBeforeFlutterDriverConnect` and after `onAfterFlutterDriverConnect` Flutter driver connection method property to the test configuration `FlutterTestConfiguration` to enable custom logic before and after a driver connection attempt.

239
README.md
View File

@ -104,7 +104,6 @@ void main() {
// are interested in testing. // are interested in testing.
runApp(MyApp()); runApp(MyApp());
} }
``` ```
All this code does is enable the Flutter driver extension which is required to be able to automate the app and then runs your application. All this code does is enable the Flutter driver extension which is required to be able to automate the app and then runs your application.
@ -134,30 +133,26 @@ import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class TapButtonNTimesStep extends When2WithWorld<String, int, FlutterWorld> { StepDefinitionGeneric TapButtonNTimesStep() {
TapButtonNTimesStep() return when2<String, int, FlutterWorld>(
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10)); 'I tap the {string} button {int} times',
(key, count, context) async {
@override final locator = find.byValueKey(key);
Future<void> executeStep(String input1, int input2) async { for (var i = 0; i < count; i += 1) {
final locator = find.byValueKey(input1); await FlutterDriverUtils.tap(context.world.driver, locator);
for (var i = 0; i < input2; i += 1) { }
await FlutterDriverUtils.tap(world.driver, locator, timeout: timeout); },
} );
}
@override
RegExp get pattern => RegExp(r"I tap the {string} button {int} times");
} }
``` ```
As you can see the class inherits from `When2WithWorld` and specifies the types of the two input parameters. The third type `FlutterWorld` is a special Flutter context object that allow access to the Flutter driver instance within the step. If you did not need this you could inherit from `When2` which does not type the world context object but still provides two input parameters. As you can see the `when2` method is invoked specifying two input parameters. The third type `FlutterWorld` is a special world context object that allow access from the context object to the Flutter driver that allows you to interact with your app. If you did not need a custom world object or strongly typed parameters you can omit the type arguments completely.
The input parameters are retrieved via the pattern regex from well know parameter types `{string}` and `{int}` [explained below](#well-known-step-parameters). They are just special syntax to indicate you are expecting a string and an integer at those points in the step text. Therefore, when the step to execute is `When I tap the "increment" button 10 times` the parameters "increment" and 10 will be passed into the step as the correct types. Note that in the pattern you can use any regex capture group to indicate any input parameter. For example the regex ` ` ` RegExp(r"When I tap the {string} (button|icon) {int} times") ` ` ` indicates 3 parameters and would match to either of the below step text. The input parameters are retrieved via the pattern regex from well know parameter types `{string}` and `{int}` [explained below](#well-known-step-parameters). They are just special syntax to indicate you are expecting a string and an integer at those points in the step text. Therefore, when the step to execute is `When I tap the "increment" button 10 times` the parameters "increment" and 10 will be passed into the step as the correct types. Note that in the pattern you can use any regex capture group to indicate any input parameter. For example the regex ` ` ` RegExp(r"When I tap the {string} (button|icon) {int} times") ` ` ` indicates 3 parameters and would match to either of the below step text.
``` dart ``` dart
When I tap the "increment" button 10 times // passes 3 parameters "increment", "button" & 10 When I tap the "increment" button 10 times // passes 3 parameters "increment", "button" & 10
When I tap the "increment" icon 2 times // passes 3 parameters "increment", "icon" & 2 When I tap the "plus" icon 2 times // passes 3 parameters "plus", "icon" & 2
``` ```
It is worth noting that this library *does not* rely on mirrors (reflection) for many reasons but most prominently for ease of maintenance and to fall inline with the principles of Flutter not allowing reflection. All in all this make for a much easier to understand and maintain code base as well as much easier debugging for the user. The downside is that we have to be slightly more explicit by providing instances of custom code such as step definition, hook, reporters and custom parameters. It is worth noting that this library *does not* rely on mirrors (reflection) for many reasons but most prominently for ease of maintenance and to fall inline with the principles of Flutter not allowing reflection. All in all this make for a much easier to understand and maintain code base as well as much easier debugging for the user. The downside is that we have to be slightly more explicit by providing instances of custom code such as step definition, hook, reporters and custom parameters.
@ -225,14 +220,12 @@ An infix boolean expression which defines the features and scenarios to run base
#### order #### order
Defaults to `ExecutionOrder.random` Defaults to `ExecutionOrder.random`
The order by which scenarios will be run. Running an a random order may highlight any inter-test dependencies that should be fixed. The order by which scenarios will be run. Running an a random order may highlight any inter-test dependencies that should be fixed.
#### stepDefinitions #### stepDefinitions
Defaults to `Iterable<StepDefinitionBase>` Defaults to `Iterable<StepDefinitionBase>`
Place instances of any custom step definition classes `Given` , `Then` , `When` , `And` , `But` that match to any custom steps defined in your feature files. Place instances of any custom step definition classes `Given` , `Then` , `When` , `And` , `But` that match to any custom steps defined in your feature files.
``` dart ``` dart
@ -253,13 +246,11 @@ Future<void> main() {
..exitAfterTestRun = true; // set to false if debugging to exit cleanly ..exitAfterTestRun = true; // set to false if debugging to exit cleanly
return GherkinRunner().execute(config); return GherkinRunner().execute(config);
} }
``` ```
#### defaultLanguage #### defaultLanguage
Defaults to `en` Defaults to `en`
This specifies the default language the feature files are written in. See https://cucumber.io/docs/gherkin/reference/#overview for supported languages. This specifies the default language the feature files are written in. See https://cucumber.io/docs/gherkin/reference/#overview for supported languages.
Note that this can be overridden in the feature itself by the use of a language block. Note that this can be overridden in the feature itself by the use of a language block.
@ -333,7 +324,7 @@ Attachment are pieces of data you can attach to a running scenario. This could
Attachments would typically be attached via a `Hook` for example `onAfterStep` . Attachments would typically be attached via a `Hook` for example `onAfterStep` .
``` ``` dart
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class AttachScreenshotOnFailedStepHook extends Hook { class AttachScreenshotOnFailedStepHook extends Hook {
@ -346,14 +337,13 @@ class AttachScreenshotOnFailedStepHook extends Hook {
} }
} }
} }
``` ```
##### screenshot ##### screenshot
To take a screenshot on a step failing you can used the pre-defined hook `AttachScreenshotOnFailedStepHook` and include it in the hook configuration of the tests config. This hook will take a screenshot and add it as an attachment to the scenario. If the `JsonReporter` is being used the screenshot will be embedded in the report which can be used to generate a HTML report which will ultimately display the screenshot under the failed step. To take a screenshot on a step failing you can used the pre-defined hook `AttachScreenshotOnFailedStepHook` and include it in the hook configuration of the tests config. This hook will take a screenshot and add it as an attachment to the scenario. If the `JsonReporter` is being used the screenshot will be embedded in the report which can be used to generate a HTML report which will ultimately display the screenshot under the failed step.
``` ``` dart
import 'dart:async'; import 'dart:async';
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
@ -442,14 +432,12 @@ Future<void> main() {
#### logFlutterProcessOutput #### logFlutterProcessOutput
Defaults to `false` Defaults to `false`
If `true` the output from the flutter process is logged to the stdout / stderr streams. Useful when debugging app build or start failures If `true` the output from the flutter process is logged to the stdout / stderr streams. Useful when debugging app build or start failures
#### flutterBuildTimeout #### flutterBuildTimeout
Defaults to `90 seconds` Defaults to `90 seconds`
Specifies the period of time to wait for the Flutter build to complete and the app to be installed and in a state to be tested. Slower machines may need longer than the default 90 seconds to complete this process. Specifies the period of time to wait for the Flutter build to complete and the app to be installed and in a state to be tested. Slower machines may need longer than the default 90 seconds to complete this process.
#### onBeforeFlutterDriverConnect #### onBeforeFlutterDriverConnect
@ -462,20 +450,17 @@ An async method that is called after a successful attempt by Flutter driver to c
#### flutterDriverMaxConnectionAttempts #### flutterDriverMaxConnectionAttempts
Defaults to `3` Defaults to `3`
Specifies the number of Flutter driver connection attempts to a running app before the test is aborted Specifies the number of Flutter driver connection attempts to a running app before the test is aborted
#### flutterDriverReconnectionDelay #### flutterDriverReconnectionDelay
Defaults to `2 seconds` Defaults to `2 seconds`
Specifies the amount of time to wait after a failed Flutter driver connection attempt to the running app Specifies the amount of time to wait after a failed Flutter driver connection attempt to the running app
#### exitAfterTestRun #### exitAfterTestRun
Defaults to `true` Defaults to `true`
True to exit the program after all tests have run. You may want to set this to false during debugging. True to exit the program after all tests have run. You may want to set this to false during debugging.
### Flutter specific configuration options ### Flutter specific configuration options
@ -490,15 +475,13 @@ To avoid tests starting on an app changed by a previous test it is suggested tha
#### targetAppPath #### targetAppPath
Defaults to `lib/test_driver/app.dart` Defaults to `lib/test_driver/app.dart`
This should point to the *testable* application that enables the Flutter driver extensions and thus is able to be automated. This application wil be started when the test run is started and restarted if the `restartAppBetweenScenarios` configuration property is set to true. This should point to the *testable* application that enables the Flutter driver extensions and thus is able to be automated. This application wil be started when the test run is started and restarted if the `restartAppBetweenScenarios` configuration property is set to true.
#### build #### build
Defaults to `true` Defaults to `true`
This optional argument lets you specify if the target application should be built prior to running the first test. This defaults to `true`
This optional argument lets you specify if the target application should be built prior to running the first test. This defaults to `true`
#### buildFlavor #### buildFlavor
@ -515,7 +498,7 @@ This optional argument lets you specify device target id as `flutter run --devic
#### runningAppProtocolEndpointUri #### runningAppProtocolEndpointUri
An observatory url that the test runner can connect to instead of creating a new running instance of the target application An observatory url that the test runner can connect to instead of creating a new running instance of the target application
The url takes the form of `http://127.0.0.1:51540/EM72VtRsUV0=/` and usually printed to stdout in the form `Connecting to service protocol: http://127.0.0.1:51540/EM72VtRsUV0=/` The url takes the form of `http://127.0.0.1:51540/EM72VtRsUV0=/` and usually printed to stdout in the form `Connecting to service protocol: http://127.0.0.1:51540/EM72VtRsUV0=/`
You will have to add the `--verbose` flag to the command to start your flutter app to see this output and ensure `enableFlutterDriverExtension()` is called by the running app You will have to add the `--verbose` flag to the command to start your flutter app to see this output and ensure `enableFlutterDriverExtension()` is called by the running app
## Features Files ## Features Files
@ -537,7 +520,7 @@ However, the domain language you choose will influence what keyword works best i
`Given` steps are used to describe the initial state of a system. The execution of a `Given` step will usually put the system into well defined state. `Given` steps are used to describe the initial state of a system. The execution of a `Given` step will usually put the system into well defined state.
To implement a `Given` step you can inherit from the ` ` ` Given ` ` ` class. To implement a `Given` step you can inherit from the ` ` ` Given ` ` ` class.
``` dart ``` dart
Given Bob has logged in Given Bob has logged in
@ -548,14 +531,13 @@ Would be implemented like so:
``` dart ``` dart
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class GivenWellKnownUserIsLoggedIn extends Given1<String> { StepDefinitionGeneric GivenWellKnownUserIsLoggedIn() {
@override return given1(
Future<void> executeStep(String wellKnownUsername) async { RegExp(r'(Bob|Mary|Emma|Jon) has logged in'),
// implement your code (wellKnownUsername, context) async {
} // implement your code
},
@override );
RegExp get pattern => RegExp(r"(Bob|Mary|Emma|Jon) has logged in");
} }
``` ```
@ -579,16 +561,15 @@ Would be implemented like so:
``` dart ``` dart
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class ThenExpectAppleCount extends Then1<int> { StepDefinitionGeneric ThenExpectAppleCount() {
@override return then1(
Future<void> executeStep(int count) async { 'I expect {int} apple(s)',
// example code (count, context) async {
final actualCount = await _getActualCount(); // example code
expectMatch(actualCount, count); final actualCount = await _getActualCount();
} context.expectMatch(actualCount, count);
},
@override );
RegExp get pattern => RegExp(r"I expect {int} apple(s)");
} }
``` ```
@ -602,23 +583,19 @@ For example, the below sets the step's timeout to 10 seconds.
``` dart ``` dart
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:gherkin/gherkin.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
class TapButtonNTimesStep extends When2WithWorld<String, int, FlutterWorld> { StepDefinitionGeneric TapButtonNTimesStep() {
TapButtonNTimesStep() return given2<String, int, FlutterWorld>(
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10)); 'I tap the {string} button {int} times',
(key, count, context) async {
@override final locator = find.byValueKey(key);
Future<void> executeStep(String input1, int input2) async { for (var i = 0; i < count; i += 1) {
final locator = find.byValueKey(input1); await FlutterDriverUtils.tap(context.world.driver, locator);
for (var i = 0; i < input2; i += 1) { }
await world.driver.tap(locator, timeout: timeout); },
} );
}
@override
RegExp get pattern => RegExp(r"I tap the {string} button {int} times");
} }
``` ```
@ -648,16 +625,14 @@ The matching step definition would then be:
``` dart ``` dart
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class GivenIProvideAComment extends Given2<String, String> { StepDefinitionGeneric GivenTheMultiLineComment() {
@override return given1(
Future<void> executeStep(String commentType, String comment) async { 'I provide the following {string} comment',
// TODO: implement executeStep (comment, context) async {
} // implement step
},
@override );
RegExp get pattern => RegExp(r"I provide the following {string} comment");
} }
``` ```
#### Data tables #### Data tables
@ -669,28 +644,27 @@ import 'package:gherkin/gherkin.dart';
/// ///
/// For example: /// For example:
/// ///
/// `Given I add the users` /// `When I add the users`
/// | Firstname | Surname | Age | Gender | /// | Firstname | Surname | Age | Gender |
/// | Woody | Johnson | 28 | Male | /// | Woody | Johnson | 28 | Male |
/// | Edith | Summers | 23 | Female | /// | Edith | Summers | 23 | Female |
/// | Megan | Hill | 83 | Female | /// | Megan | Hill | 83 | Female |
class GivenIAddTheUsers extends Given1<Table> { StepDefinitionGeneric WhenIAddTheUsers() {
@override return when1(
Future<void> executeStep(Table dataTable) async { 'I add the users',
for (var row in dataTable.rows) { (Table dataTable, context) async {
// do something with row for (var row in dataTable.rows) {
row.columns.forEach((columnValue) => print(columnValue)); // do something with row
} row.columns.forEach((columnValue) => print(columnValue));
}
// or get the table as a map (column values keyed by the header) // or get the table as a map (column values keyed by the header)
final columns = dataTable.asMap(); final columns = dataTable.asMap();
final personOne = columns.elementAt(0); final personOne = columns.elementAt(0);
final personOneName = personOne["Firstname"]; final personOneName = personOne["Firstname"];
print('Name of first user: `$personOneName` '); print('Name of first user: `$personOneName` ');
} },
);
@override
RegExp get pattern => RegExp(r"I add the users");
} }
``` ```
@ -707,7 +681,7 @@ In most scenarios theses parameters will be enough for you to write quite advanc
| {int} | Matches an integer | {int}, {Int} | int | `Given I see {int} worm(s)` would match `Given I see 6 worms` | | {int} | Matches an integer | {int}, {Int} | int | `Given I see {int} worm(s)` would match `Given I see 6 worms` |
| {num} | Matches an number | {num}, {Num}, {float}, {Float} | num | `Given I see {num} worm(s)` would match `Given I see 0.75 worms` | | {num} | Matches an number | {num}, {Num}, {float}, {Float} | num | `Given I see {num} worm(s)` would match `Given I see 0.75 worms` |
Note that you can combine there well known parameters in any step. For example `Given I {word} {int} worm(s)` would match `Given I "see" 6 worms` and also match `Given I "eat" 1 worm` Note that you can combine there well known parameters in any step. For example `Given I {word} {int} worm(s)` would match `Given I "see" 6 worms` and also match `Given I "eat" 1 worm`
#### Pluralization #### Pluralization
@ -747,14 +721,13 @@ The step definition would then use this custom parameter like so:
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
import 'colour_parameter.dart'; import 'colour_parameter.dart';
class GivenIPickAColour extends Given1<Colour> { StepDefinitionGeneric GivenIAddTheUsers() {
@override return given1<Colour>(
Future<void> executeStep(Colour input1) async { 'I pick the colour {colour}',
print("The picked colour was: '$input1'"); (colour, _) async {
} print("The picked colour was: '$colour'");
},
@override );
RegExp get pattern => RegExp(r"I pick the colour {colour}");
} }
``` ```
@ -770,18 +743,13 @@ Tags are a great way of organizing your features and marking them with filterabl
You can filter the scenarios by providing a tag expression to your configuration file. Tag expression are simple infix expressions such as: You can filter the scenarios by providing a tag expression to your configuration file. Tag expression are simple infix expressions such as:
`@smoke` `@smoke`
`@smoke and @perf`
`@smoke and @perf` `@billing or @onboarding`
`@smoke and not @ignore`
`@billing or @onboarding`
`@smoke and not @ignore`
You can even us brackets to ensure the order of precedence You can even us brackets to ensure the order of precedence
`@smoke and not (@ignore or @todo)` `@smoke and not (@ignore or @todo)`
You can use the usual boolean statement "and", "or", "not" You can use the usual boolean statement "and", "or", "not"
Also see <https://docs.cucumber.io/cucumber/api/#tags> Also see <https://docs.cucumber.io/cucumber/api/#tags>
@ -792,7 +760,7 @@ In order to allow features to be written in a number of languages, you can now w
You can set the default language of feature files in your project via the configuration setting see [defaultLanguage](#defaultLanguage) You can set the default language of feature files in your project via the configuration setting see [defaultLanguage](#defaultLanguage)
For example these two features are the same the keywords are just written in different languages. Note the ` ` ` # language: de ` ` ` on the second feature. English is the default language. For example these two features are the same the keywords are just written in different languages. Note the ` ` ` # language: de ` ` ` on the second feature. English is the default language.
``` ```
Feature: Calculator Feature: Calculator
@ -918,18 +886,17 @@ A reporter is a class that is able to report on the progress of the test run. In
You can create your own custom reporter by inheriting from the base `Reporter` class and overriding the one or many of the methods to direct the output message. The `Reporter` defines the following methods that can be overridden. All methods must return a `Future<void>` and can be async. You can create your own custom reporter by inheriting from the base `Reporter` class and overriding the one or many of the methods to direct the output message. The `Reporter` defines the following methods that can be overridden. All methods must return a `Future<void>` and can be async.
* `onTestRunStarted` * `onTestRunStarted`
* `onTestRunFinished` * `onTestRunFinished`
* `onFeatureStarted` * `onFeatureStarted`
* `onFeatureFinished` * `onFeatureFinished`
* `onScenarioStarted` * `onScenarioStarted`
* `onScenarioFinished` * `onScenarioFinished`
* `onStepStarted` * `onStepStarted`
* `onStepFinished` * `onStepFinished`
* `onException` * `onException`
* `message` * `message`
* `dispose` * `dispose`
Once you have created your custom reporter don't forget to add it to the `reporters` configuration file property. Once you have created your custom reporter don't forget to add it to the `reporters` configuration file property.
*Note*: PR's of new reporters are *always* welcome. *Note*: PR's of new reporters are *always* welcome.

View File

@ -8,26 +8,20 @@ import 'steps/given_I_pick_a_colour_step.dart';
import 'steps/tap_button_n_times_step.dart'; import 'steps/tap_button_n_times_step.dart';
Future<void> main() { Future<void> main() {
final config = FlutterTestConfiguration() final steps = [
..features = [Glob('features//**.feature')] TapButtonNTimesStep(),
..reporters = [ GivenIPickAColour(),
ProgressReporter(), ];
TestRunSummaryReporter(),
JsonReporter(path: './report.json'), final config = FlutterTestConfiguration.DEFAULT(
FlutterDriverReporter( steps,
logErrorMessages: true, featurePath: 'features//**.feature',
logInfoMessages: true, targetAppPath: 'test_driver/app.dart',
logWarningMessages: true, )
),
] // you can include the "StdoutReporter()" without the message level parameter for verbose log information
..hooks = [ ..hooks = [
HookExample(), HookExample(),
// AttachScreenshotOnFailedStepHook(), // takes a screenshot of each step failure and attaches it to the world object // AttachScreenshotOnFailedStepHook(), // takes a screenshot of each step failure and attaches it to the world object
] ]
..stepDefinitions = [
TapButtonNTimesStep(),
GivenIPickAColour(),
]
..customStepParameterDefinitions = [ ..customStepParameterDefinitions = [
ColourParameter(), ColourParameter(),
] ]

View File

@ -2,8 +2,10 @@ Feature: Startup
Scenario: should increment counter Scenario: should increment counter
Given I expect the "counter" to be "0" Given I expect the "counter" to be "0"
#! profile "action speed"
When I tap the "increment" button When I tap the "increment" button
And I tap the "increment" button And I tap the "increment" button
#! end
Then I expect the "counter" to be "2" Then I expect the "counter" to be "2"
Scenario: counter should reset when app is restarted Scenario: counter should reset when app is restarted

View File

@ -1,7 +1,6 @@
Feature: Counter Feature: Counter
The counter should be incremented when the button is pressed. The counter should be incremented when the button is pressed.
@smoke
Scenario: Counter increases when the button is pressed Scenario: Counter increases when the button is pressed
Given I pick the colour red Given I pick the colour red
Given I expect the "counter" to be "0" Given I expect the "counter" to be "0"

View File

@ -2,7 +2,6 @@
Fonctionnalité: Counter Fonctionnalité: Counter
The counter should be incremented when the button is pressed. The counter should be incremented when the button is pressed.
@smoke
Scénario: Counter increases when the button is pressed Scénario: Counter increases when the button is pressed
Etant donné que I pick the colour red Etant donné que I pick the colour red
Et I expect the "counter" to be "0" Et I expect the "counter" to be "0"

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
/// This step expects a multiline string proceeding it /// This step expects a data table
/// ///
/// For example: /// For example:
/// ///
@ -9,16 +9,14 @@ import 'package:gherkin/gherkin.dart';
/// | Woody | Johnson | 28 | Male | /// | Woody | Johnson | 28 | Male |
/// | Edith | Summers | 23 | Female | /// | Edith | Summers | 23 | Female |
/// | Megan | Hill | 83 | Female | /// | Megan | Hill | 83 | Female |
class GivenIAddTheUsers extends Given1<Table> { StepDefinitionGeneric WhenIAddTheUsers() {
@override return when1(
Future<void> executeStep(Table dataTable) async { 'I add the users',
// implement executeStep (Table dataTable, context) async {
for (var row in dataTable.rows) { for (var row in dataTable.rows) {
// do something with row // do something with row
row.columns.forEach((columnValue) => print(columnValue)); row.columns.forEach((columnValue) => print(columnValue));
} }
} },
);
@override
RegExp get pattern => RegExp(r'I add the users');
} }

View File

@ -1,12 +1,11 @@
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
import 'colour_parameter.dart'; import 'colour_parameter.dart';
class GivenIPickAColour extends Given1<Colour> { StepDefinitionGeneric GivenIPickAColour() {
@override return given1(
Future<void> executeStep(Colour input1) async { 'I pick the colour {colour}',
print("The picked colour was: '$input1'"); (Colour colour, _) async {
} print("The picked colour was: '$colour'");
},
@override );
RegExp get pattern => RegExp(r'I pick the colour {colour}');
} }

View File

@ -1,6 +1,6 @@
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
/// This step expects a multiline string proceeding it /// This step expects a multi-line string proceeding it
/// ///
/// For example: /// For example:
/// ///
@ -8,12 +8,11 @@ import 'package:gherkin/gherkin.dart';
/// """ /// """
/// Some comment /// Some comment
/// """ /// """
class GivenIProvideAComment extends Given2<String, String> { StepDefinitionGeneric GivenMultiLineString() {
@override return given2(
Future<void> executeStep(String commentType, String comment) async { 'I provide the following {string} comment',
// implement executeStep (commentType, comment, _) async {
} // implement step
},
@override );
RegExp get pattern => RegExp(r'I provide the following {string} comment');
} }

View File

@ -2,18 +2,14 @@ import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class TapButtonNTimesStep extends When2WithWorld<String, int, FlutterWorld> { StepDefinitionGeneric TapButtonNTimesStep() {
TapButtonNTimesStep() return given2<String, int, FlutterWorld>(
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10)); 'I tap the {string} button {int} times',
(key, count, context) async {
@override final locator = find.byValueKey(key);
Future<void> executeStep(String input1, int input2) async { for (var i = 0; i < count; i += 1) {
final locator = find.byValueKey(input1); await FlutterDriverUtils.tap(context.world.driver, locator);
for (var i = 0; i < input2; i += 1) { }
await FlutterDriverUtils.tap(world.driver, locator, timeout: timeout); },
} );
}
@override
RegExp get pattern => RegExp(r'I tap the {string} button {int} times');
} }

View File

@ -187,7 +187,7 @@ class FlutterRunProcessHandler extends ProcessHandler {
final completer = Completer<String>(); final completer = Completer<String>();
StreamSubscription sub; StreamSubscription sub;
sub = _processStdoutStream.timeout( sub = _processStdoutStream.timeout(
timeout, timeout ?? const Duration(seconds: 90),
onTimeout: (_) { onTimeout: (_) {
sub?.cancel(); sub?.cancel();
if (!completer.isCompleted) { if (!completer.isCompleted) {

View File

@ -12,12 +12,39 @@ import 'package:flutter_gherkin/src/flutter/steps/when_tap_widget_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/when_tap_the_back_button_step.dart'; import 'package:flutter_gherkin/src/flutter/steps/when_tap_the_back_button_step.dart';
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
import 'package:glob/glob.dart';
import 'steps/then_expect_widget_to_be_present_step.dart'; import 'steps/then_expect_widget_to_be_present_step.dart';
class FlutterTestConfiguration extends TestConfiguration { class FlutterTestConfiguration extends TestConfiguration {
String _observatoryDebuggerUri; String _observatoryDebuggerUri;
/// 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.
static FlutterTestConfiguration DEFAULT(
Iterable<StepDefinitionGeneric<World>> steps, {
String featurePath = 'test_driver/features/**.feature',
String targetAppPath = 'test_driver/app.dart',
}) {
return FlutterTestConfiguration()
..features = [Glob(featurePath)]
..reporters = [
StdoutReporter(MessageLevel.error),
ProgressReporter(),
TestRunSummaryReporter(),
JsonReporter(path: './report.json'),
FlutterDriverReporter(
logErrorMessages: true,
logInfoMessages: false,
logWarningMessages: false,
),
]
..targetAppPath = targetAppPath
..stepDefinitions = steps
..restartAppBetweenScenarios = true
..exitAfterTestRun = true;
}
/// restarts the application under test between each scenario. /// restarts the application under test between each scenario.
/// Defaults to true to avoid the application being in an invalid state /// Defaults to true to avoid the application being in an invalid state
/// before each test /// before each test

View File

@ -13,7 +13,7 @@ class FlutterWorld extends World {
_driver = flutterDriver; _driver = flutterDriver;
} }
void setFlutterProccessHandler( void setFlutterProcessHandler(
FlutterRunProcessHandler flutterRunProcessHandler) { FlutterRunProcessHandler flutterRunProcessHandler) {
_flutterRunProcessHandler = flutterRunProcessHandler; _flutterRunProcessHandler = flutterRunProcessHandler;
} }

View File

@ -56,7 +56,7 @@ class FlutterAppRunnerHook extends Hook {
Iterable<Tag> tags, Iterable<Tag> tags,
) async { ) async {
if (world is FlutterWorld) { if (world is FlutterWorld) {
world.setFlutterProccessHandler(_flutterRunProcessHandler); world.setFlutterProcessHandler(_flutterRunProcessHandler);
} }
} }

View File

@ -8,26 +8,25 @@ import 'package:gherkin/gherkin.dart';
/// Examples: /// Examples:
/// ///
/// `Given I open the drawer` /// `Given I open the drawer`
class GivenOpenDrawer extends Given1WithWorld<String, FlutterWorld> { StepDefinitionGeneric GivenOpenDrawer() {
@override return given1<String, FlutterWorld>(
RegExp get pattern => RegExp(r'I (open|close) the drawer'); RegExp(r'I (open|close) the drawer'),
(action, context) async {
@override final drawerFinder = find.byType('Drawer');
Future<void> executeStep(String action) async { final isOpen = await FlutterDriverUtils.isPresent(
final drawerFinder = find.byType('Drawer'); context.world.driver, drawerFinder);
final isOpen = // https://github.com/flutter/flutter/issues/9002#issuecomment-293660833
await FlutterDriverUtils.isPresent(world.driver, drawerFinder); if (isOpen && action == 'close') {
// https://github.com/flutter/flutter/issues/9002#issuecomment-293660833 // Swipe to the left across the whole app to close the drawer
if (isOpen && action == 'close') { await context.world.driver.scroll(
// Swipe to the left across the whole app to close the drawer drawerFinder, -300.0, 0.0, const Duration(milliseconds: 300));
await world.driver } else if (!isOpen && action == 'open') {
.scroll(drawerFinder, -300.0, 0.0, const Duration(milliseconds: 300)); await FlutterDriverUtils.tap(
} else if (!isOpen && action == 'open') { context.world.driver,
await FlutterDriverUtils.tap( find.byTooltip('Open navigation menu'),
world.driver, timeout: context.configuration?.timeout,
find.byTooltip('Open navigation menu'), );
timeout: timeout, }
); },
} );
}
} }

View File

@ -1,15 +1,13 @@
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart'; import 'package:gherkin/gherkin.dart';
class RestartAppStep extends WhenWithWorld<FlutterWorld> { StepDefinitionGeneric RestartAppStep() {
RestartAppStep() return given<FlutterWorld>(
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 60)); 'I restart the app',
(context) async {
@override await context.world.restartApp(
Future<void> executeStep() async { timeout: context.configuration?.timeout,
await world.restartApp(timeout: timeout); );
} },
);
@override
RegExp get pattern => RegExp(r'I restart the app');
} }

View File

@ -13,22 +13,20 @@ import 'package:gherkin/gherkin.dart';
/// ///
/// `Then I expect the "controlKey" to be "Hello World"` /// `Then I expect the "controlKey" to be "Hello World"`
/// `And I expect the "controlKey" to be "Hello World"` /// `And I expect the "controlKey" to be "Hello World"`
class ThenExpectElementToHaveValue StepDefinitionGeneric ThenExpectElementToHaveValue() {
extends Then2WithWorld<String, String, FlutterWorld> { return given2<String, String, FlutterWorld>(
@override RegExp(r'I expect the {string} to be {string}$'),
RegExp get pattern => RegExp(r'I expect the {string} to be {string}$'); (key, value, context) async {
try {
@override final text = await FlutterDriverUtils.getText(
Future<void> executeStep(String key, String value) async { context.world.driver,
try { find.byValueKey(key),
final text = await FlutterDriverUtils.getText( );
world.driver, find.byValueKey(key), context.expect(text, value);
timeout: timeout * .9); } catch (e) {
expect(text, value); await context.reporter.message('Step error: $e', MessageLevel.error);
} catch (e) { rethrow;
await reporter.message( }
"Step error '${pattern.pattern}': $e", MessageLevel.error); },
rethrow; );
}
}
} }

View File

@ -12,19 +12,17 @@ import 'package:gherkin/gherkin.dart';
/// ///
/// `Then I expect the widget 'notification' to be present within 10 seconds` /// `Then I expect the widget 'notification' to be present within 10 seconds`
/// `Then I expect the button 'save' to be present within 1 second` /// `Then I expect the button 'save' to be present within 1 second`
class ThenExpectWidgetToBePresent StepDefinitionGeneric ThenExpectWidgetToBePresent() {
extends When2WithWorld<String, int, FlutterWorld> { return given2<String, int, FlutterWorld>(
@override RegExp(
RegExp get pattern => RegExp( r'I expect the (?:button|element|label|icon|field|text|widget|dialog|popup) {string} to be present within {int} second(s)$'),
r'I expect the (?:button|element|label|icon|field|text|widget|dialog|popup) {string} to be present within {int} second(s)$'); (key, seconds, context) async {
final isPresent = await FlutterDriverUtils.isPresent(
@override context.world.driver,
Future<void> executeStep(String key, int seconds) async { find.byValueKey(key),
final isPresent = await FlutterDriverUtils.isPresent( timeout: Duration(seconds: seconds),
world.driver, );
find.byValueKey(key), context.expect(isPresent, true);
timeout: Duration(seconds: seconds), },
); );
expect(isPresent, true);
}
} }

View File

@ -8,19 +8,17 @@ import 'package:gherkin/gherkin.dart';
/// Examples: /// Examples:
/// Then I fill the "email" field with "bob@gmail.com" /// Then I fill the "email" field with "bob@gmail.com"
/// Then I fill the "name" field with "Woody Johnson" /// Then I fill the "name" field with "Woody Johnson"
class WhenFillFieldStep extends When2WithWorld<String, String, FlutterWorld> { StepDefinitionGeneric WhenFillFieldStep() {
@override return given2<String, String, FlutterWorld>(
Future<void> executeStep(String key, String input2) async { 'I fill the {string} field with {string}',
final finder = find.byValueKey(key); (key, value, context) async {
await world.driver.scrollIntoView(finder); final finder = find.byValueKey(key);
await FlutterDriverUtils.enterText( await context.world.driver.scrollIntoView(finder);
world.driver, await FlutterDriverUtils.enterText(
finder, context.world.driver,
input2, finder,
timeout: timeout * .9, value,
); );
} },
);
@override
RegExp get pattern => RegExp(r'I fill the {string} field with {string}');
} }

View File

@ -7,16 +7,11 @@ import 'package:gherkin/gherkin.dart';
/// Examples: /// Examples:
/// When I pause for 10 seconds /// When I pause for 10 seconds
/// When I pause for 120 seconds /// When I pause for 120 seconds
class WhenPauseStep extends When1<int> { StepDefinitionGeneric WhenPauseStep() {
WhenPauseStep() return when1(
: super(StepDefinitionConfiguration() 'I pause for {int} second(s)',
..timeout = const Duration(minutes: 5)); (wait, _) async {
await Future.delayed(Duration(seconds: wait));
@override },
Future<void> executeStep(int seconds) async { );
await Future.delayed(Duration(seconds: seconds));
}
@override
RegExp get pattern => RegExp(r'I pause for {int} second(s)');
} }

View File

@ -10,16 +10,14 @@ import 'package:gherkin/gherkin.dart';
/// `When I tap the back button"` /// `When I tap the back button"`
/// `When I tap the back element"` /// `When I tap the back element"`
/// `When I tap the back widget"` /// `When I tap the back widget"`
class WhenTapBackButtonWidget extends WhenWithWorld<FlutterWorld> { StepDefinitionGeneric WhenTapBackButtonWidget() {
@override return when1<String, FlutterWorld>(
RegExp get pattern => RegExp(r'I tap the back (?:button|element|widget)$'); RegExp(r'I tap the back (?:button|element|widget)$'),
(_, context) async {
@override await FlutterDriverUtils.tap(
Future<void> executeStep() async { context.world.driver,
await FlutterDriverUtils.tap( find.pageBack(),
world.driver, );
find.pageBack(), },
timeout: timeout * .9, );
);
}
} }

View File

@ -17,40 +17,35 @@ import 'package:gherkin/gherkin.dart';
/// `When I tap "controlKey" field"` /// `When I tap "controlKey" field"`
/// `When I tap "controlKey" text"` /// `When I tap "controlKey" text"`
/// `When I tap "controlKey" widget"` /// `When I tap "controlKey" widget"`
class WhenTapWidget extends When1WithWorld<String, FlutterWorld> { StepDefinitionGeneric WhenTapWidget() {
@override return when1<String, FlutterWorld>(
RegExp get pattern => RegExp( RegExp(
r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'); r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'),
(key, context) async {
final finder = find.byValueKey(key);
@override await context.world.driver.scrollIntoView(
Future<void> executeStep(String key) async { finder,
final finder = find.byValueKey(key); );
await FlutterDriverUtils.tap(
await world.driver.scrollIntoView( context.world.driver,
finder, finder,
timeout: timeout * .45, );
); },
await FlutterDriverUtils.tap( );
world.driver,
finder,
timeout: timeout * .45,
);
}
} }
class WhenTapWidgetWithoutScroll extends When1WithWorld<String, FlutterWorld> { StepDefinitionGeneric WhenTapWidgetWithoutScroll() {
@override return when1<String, FlutterWorld>(
RegExp get pattern => RegExp( RegExp(
r'I tap the {string} (?:button|element|label|icon|field|text|widget) without scrolling it into view$'); r'I tap the {string} (?:button|element|label|icon|field|text|widget) without scrolling it into view$'),
(key, context) async {
final finder = find.byValueKey(key);
@override await FlutterDriverUtils.tap(
Future<void> executeStep(String key) async { context.world.driver,
final finder = find.byValueKey(key); finder,
);
await FlutterDriverUtils.tap( },
world.driver, );
finder,
timeout: timeout * .45,
);
}
} }

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "5.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.39.8" version: "0.39.13"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -70,7 +70,7 @@ packages:
name: coverage name: coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.13.9" version: "0.13.11"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -118,7 +118,7 @@ packages:
name: gherkin name: gherkin
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.8+1" version: "1.1.8+2"
glob: glob:
dependency: "direct main" dependency: "direct main"
description: description:
@ -139,7 +139,7 @@ packages:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.1" version: "0.12.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -181,7 +181,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.1+1" version: "0.6.2"
json_rpc_2: json_rpc_2:
dependency: transitive dependency: transitive
description: description:
@ -230,21 +230,21 @@ packages:
name: node_interop name: node_interop
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.1.1"
node_io: node_io:
dependency: transitive dependency: transitive
description: description:
name: node_io name: node_io
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
node_preamble: node_preamble:
dependency: transitive dependency: transitive
description: description:
name: node_preamble name: node_preamble
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.8" version: "1.4.12"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -314,7 +314,7 @@ packages:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.5" version: "0.7.7"
shelf_packages_handler: shelf_packages_handler:
dependency: transitive dependency: transitive
description: description:
@ -403,7 +403,7 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.14.3" version: "1.14.4"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
@ -438,7 +438,7 @@ packages:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.3" version: "4.1.0"
vm_service_client: vm_service_client:
dependency: transitive dependency: transitive
description: description:
@ -473,7 +473,7 @@ packages:
name: webkit_inspection_protocol name: webkit_inspection_protocol
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.3" version: "0.7.3"
xml: xml:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,6 +1,6 @@
name: flutter_gherkin name: flutter_gherkin
description: A Gherkin / Cucumber parser and test runner for Dart and Flutter description: A Gherkin / Cucumber parser and test runner for Dart and Flutter
version: 1.1.8+2 version: 1.1.8+3
homepage: https://github.com/jonsamwell/flutter_gherkin homepage: https://github.com/jonsamwell/flutter_gherkin
environment: environment:
@ -16,7 +16,7 @@ dependencies:
sdk: flutter sdk: flutter
glob: ^1.1.7 glob: ^1.1.7
meta: ">=1.1.6 <2.0.0" meta: ">=1.1.6 <2.0.0"
gherkin: ^1.1.8+1 gherkin: ^1.1.8+2
# gherkin: # gherkin:
# path: ../dart_gherkin # path: ../dart_gherkin

View File

@ -1,13 +1,5 @@
import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:flutter_gherkin/src/flutter/hooks/app_runner_hook.dart'; import 'package:flutter_gherkin/src/flutter/hooks/app_runner_hook.dart';
import 'package:flutter_gherkin/src/flutter/steps/given_i_open_the_drawer_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/restart_app_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/then_expect_element_to_have_value_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/then_expect_widget_to_be_present_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/when_fill_field_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/when_pause_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/when_tap_the_back_button_step.dart';
import 'package:flutter_gherkin/src/flutter/steps/when_tap_widget_step.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'mocks/step_definition_mock.dart'; import 'mocks/step_definition_mock.dart';
@ -31,21 +23,6 @@ void main() {
config.prepare(); config.prepare();
expect(config.stepDefinitions, isNotNull); expect(config.stepDefinitions, isNotNull);
expect(config.stepDefinitions.length, 9); expect(config.stepDefinitions.length, 9);
expect(config.stepDefinitions.elementAt(0),
(x) => x is ThenExpectElementToHaveValue);
expect(config.stepDefinitions.elementAt(1), (x) => x is WhenTapWidget);
expect(config.stepDefinitions.elementAt(2),
(x) => x is WhenTapWidgetWithoutScroll);
expect(config.stepDefinitions.elementAt(3),
(x) => x is WhenTapBackButtonWidget);
expect(
config.stepDefinitions.elementAt(4), (x) => x is GivenOpenDrawer);
expect(config.stepDefinitions.elementAt(5), (x) => x is WhenPauseStep);
expect(
config.stepDefinitions.elementAt(6), (x) => x is WhenFillFieldStep);
expect(config.stepDefinitions.elementAt(7),
(x) => x is ThenExpectWidgetToBePresent);
expect(config.stepDefinitions.elementAt(8), (x) => x is RestartAppStep);
}); });
test('common step definition added to existing steps', () { test('common step definition added to existing steps', () {
@ -58,8 +35,6 @@ void main() {
expect(config.stepDefinitions.length, 10); expect(config.stepDefinitions.length, 10);
expect(config.stepDefinitions.elementAt(0), expect(config.stepDefinitions.elementAt(0),
(x) => x is MockStepDefinition); (x) => x is MockStepDefinition);
expect(config.stepDefinitions.elementAt(1),
(x) => x is ThenExpectElementToHaveValue);
}); });
}); });
}); });