Merge branch 'master' into new-core-steps

This commit is contained in:
Jon Samwell 2020-07-19 11:31:17 +10:00 committed by GitHub
commit 859cf7028c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 315 additions and 380 deletions

View File

@ -1,4 +1,5 @@
## Unreleased
## [1.1.8+3] - 19/07/2020
* Updated Gherkin library version to allow for function step step implementation; updated docs to match.
* Add steps `SiblingContainsText`, `SwipeOnKey`, `SwipeOnText`, `TapTextWithinWidget`, `TapWidgetOfType`, `TapWidgetOfTypeWithin`, `TapWidgetWithText`, `TextExists`, `TextExistsWithin`, `WaitUntilKeyExists`, and `WaitUntilTypeExists`.
## [1.1.8+2] - 11/05/2020

239
README.md
View File

@ -104,7 +104,6 @@ void main() {
// are interested in testing.
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.
@ -134,30 +133,26 @@ import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
class TapButtonNTimesStep extends When2WithWorld<String, int, FlutterWorld> {
TapButtonNTimesStep()
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10));
@override
Future<void> executeStep(String input1, int input2) async {
final locator = find.byValueKey(input1);
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");
StepDefinitionGeneric TapButtonNTimesStep() {
return when2<String, int, FlutterWorld>(
'I tap the {string} button {int} times',
(key, count, context) async {
final locator = find.byValueKey(key);
for (var i = 0; i < count; i += 1) {
await FlutterDriverUtils.tap(context.world.driver, locator);
}
},
);
}
```
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
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.
@ -225,14 +220,12 @@ An infix boolean expression which defines the features and scenarios to run base
#### 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.
#### 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.
``` dart
@ -253,13 +246,11 @@ Future<void> main() {
..exitAfterTestRun = true; // set to false if debugging to exit cleanly
return GherkinRunner().execute(config);
}
```
#### 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.
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` .
```
``` dart
import 'package:gherkin/gherkin.dart';
class AttachScreenshotOnFailedStepHook extends Hook {
@ -346,14 +337,13 @@ class AttachScreenshotOnFailedStepHook extends Hook {
}
}
}
```
##### 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.
```
``` dart
import 'dart:async';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
@ -442,14 +432,12 @@ Future<void> main() {
#### 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
#### 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.
#### onBeforeFlutterDriverConnect
@ -462,20 +450,17 @@ An async method that is called after a successful attempt by Flutter driver to c
#### flutterDriverMaxConnectionAttempts
Defaults to `3`
Defaults to `3`
Specifies the number of Flutter driver connection attempts to a running app before the test is aborted
#### 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
#### 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.
### 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
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.
#### build
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`
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
@ -515,7 +498,7 @@ This optional argument lets you specify device target id as `flutter run --devic
#### runningAppProtocolEndpointUri
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
## 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.
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
Given Bob has logged in
@ -548,14 +531,13 @@ Would be implemented like so:
``` dart
import 'package:gherkin/gherkin.dart';
class GivenWellKnownUserIsLoggedIn extends Given1<String> {
@override
Future<void> executeStep(String wellKnownUsername) async {
// implement your code
}
@override
RegExp get pattern => RegExp(r"(Bob|Mary|Emma|Jon) has logged in");
StepDefinitionGeneric GivenWellKnownUserIsLoggedIn() {
return given1(
RegExp(r'(Bob|Mary|Emma|Jon) has logged in'),
(wellKnownUsername, context) async {
// implement your code
},
);
}
```
@ -579,16 +561,15 @@ Would be implemented like so:
``` dart
import 'package:gherkin/gherkin.dart';
class ThenExpectAppleCount extends Then1<int> {
@override
Future<void> executeStep(int count) async {
// example code
final actualCount = await _getActualCount();
expectMatch(actualCount, count);
}
@override
RegExp get pattern => RegExp(r"I expect {int} apple(s)");
StepDefinitionGeneric ThenExpectAppleCount() {
return then1(
'I expect {int} apple(s)',
(count, context) async {
// example code
final actualCount = await _getActualCount();
context.expectMatch(actualCount, count);
},
);
}
```
@ -602,23 +583,19 @@ For example, the below sets the step's timeout to 10 seconds.
``` dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:gherkin/gherkin.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
class TapButtonNTimesStep extends When2WithWorld<String, int, FlutterWorld> {
TapButtonNTimesStep()
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10));
@override
Future<void> executeStep(String input1, int input2) async {
final locator = find.byValueKey(input1);
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");
StepDefinitionGeneric TapButtonNTimesStep() {
return given2<String, int, FlutterWorld>(
'I tap the {string} button {int} times',
(key, count, context) async {
final locator = find.byValueKey(key);
for (var i = 0; i < count; i += 1) {
await FlutterDriverUtils.tap(context.world.driver, locator);
}
},
);
}
```
@ -648,16 +625,14 @@ The matching step definition would then be:
``` dart
import 'package:gherkin/gherkin.dart';
class GivenIProvideAComment extends Given2<String, String> {
@override
Future<void> executeStep(String commentType, String comment) async {
// TODO: implement executeStep
}
@override
RegExp get pattern => RegExp(r"I provide the following {string} comment");
StepDefinitionGeneric GivenTheMultiLineComment() {
return given1(
'I provide the following {string} comment',
(comment, context) async {
// implement step
},
);
}
```
#### Data tables
@ -669,28 +644,27 @@ import 'package:gherkin/gherkin.dart';
///
/// For example:
///
/// `Given I add the users`
/// `When I add the users`
/// | Firstname | Surname | Age | Gender |
/// | Woody | Johnson | 28 | Male |
/// | Edith | Summers | 23 | Female |
/// | Megan | Hill | 83 | Female |
class GivenIAddTheUsers extends Given1<Table> {
@override
Future<void> executeStep(Table dataTable) async {
for (var row in dataTable.rows) {
// do something with row
row.columns.forEach((columnValue) => print(columnValue));
}
StepDefinitionGeneric WhenIAddTheUsers() {
return when1(
'I add the users',
(Table dataTable, context) async {
for (var row in dataTable.rows) {
// do something with row
row.columns.forEach((columnValue) => print(columnValue));
}
// or get the table as a map (column values keyed by the header)
final columns = dataTable.asMap();
final personOne = columns.elementAt(0);
final personOneName = personOne["Firstname"];
print('Name of first user: `$personOneName` ');
}
@override
RegExp get pattern => RegExp(r"I add the users");
// or get the table as a map (column values keyed by the header)
final columns = dataTable.asMap();
final personOne = columns.elementAt(0);
final personOneName = personOne["Firstname"];
print('Name of first user: `$personOneName` ');
},
);
}
```
@ -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` |
| {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
@ -747,14 +721,13 @@ The step definition would then use this custom parameter like so:
import 'package:gherkin/gherkin.dart';
import 'colour_parameter.dart';
class GivenIPickAColour extends Given1<Colour> {
@override
Future<void> executeStep(Colour input1) async {
print("The picked colour was: '$input1'");
}
@override
RegExp get pattern => RegExp(r"I pick the colour {colour}");
StepDefinitionGeneric GivenIAddTheUsers() {
return given1<Colour>(
'I pick the colour {colour}',
(colour, _) async {
print("The picked colour was: '$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:
`@smoke`
`@smoke and @perf`
`@billing or @onboarding`
`@smoke and not @ignore`
`@smoke`
`@smoke and @perf`
`@billing or @onboarding`
`@smoke and not @ignore`
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"
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)
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
@ -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.
* `onTestRunStarted`
* `onTestRunFinished`
* `onFeatureStarted`
* `onFeatureFinished`
* `onScenarioStarted`
* `onScenarioFinished`
* `onStepStarted`
* `onStepFinished`
* `onException`
* `message`
* `dispose`
* `onTestRunStarted`
* `onTestRunFinished`
* `onFeatureStarted`
* `onFeatureFinished`
* `onScenarioStarted`
* `onScenarioFinished`
* `onStepStarted`
* `onStepFinished`
* `onException`
* `message`
* `dispose`
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.

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
Fonctionnalité: Counter
The counter should be incremented when the button is pressed.
@smoke
Scénario: Counter increases when the button is pressed
Etant donné que I pick the colour red
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';
/// This step expects a multiline string proceeding it
/// This step expects a data table
///
/// For example:
///
@ -9,16 +9,14 @@ import 'package:gherkin/gherkin.dart';
/// | Woody | Johnson | 28 | Male |
/// | Edith | Summers | 23 | Female |
/// | Megan | Hill | 83 | Female |
class GivenIAddTheUsers extends Given1<Table> {
@override
Future<void> executeStep(Table dataTable) async {
// implement executeStep
for (var row in dataTable.rows) {
// do something with row
row.columns.forEach((columnValue) => print(columnValue));
}
}
@override
RegExp get pattern => RegExp(r'I add the users');
StepDefinitionGeneric WhenIAddTheUsers() {
return when1(
'I add the users',
(Table dataTable, context) async {
for (var row in dataTable.rows) {
// do something with row
row.columns.forEach((columnValue) => print(columnValue));
}
},
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -24,12 +24,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_driver/flutter_driver.dart';
import 'package:gherkin/gherkin.dart';
import 'package:glob/glob.dart';
import 'steps/then_expect_widget_to_be_present_step.dart';
class FlutterTestConfiguration extends TestConfiguration {
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.
/// Defaults to true to avoid the application being in an invalid state
/// before each test

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,19 +8,17 @@ import 'package:gherkin/gherkin.dart';
/// 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 {
final finder = find.byValueKey(key);
await world.driver.scrollIntoView(finder);
await FlutterDriverUtils.enterText(
world.driver,
finder,
input2,
timeout: timeout * .9,
);
}
@override
RegExp get pattern => RegExp(r'I fill the {string} field with {string}');
StepDefinitionGeneric WhenFillFieldStep() {
return given2<String, String, FlutterWorld>(
'I fill the {string} field with {string}',
(key, value, context) async {
final finder = find.byValueKey(key);
await context.world.driver.scrollIntoView(finder);
await FlutterDriverUtils.enterText(
context.world.driver,
finder,
value,
);
},
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
name: flutter_gherkin
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
environment:
@ -16,7 +16,7 @@ dependencies:
sdk: flutter
glob: ^1.1.7
meta: ">=1.1.6 <2.0.0"
gherkin: ^1.1.8+1
gherkin: ^1.1.8+2
# gherkin:
# path: ../dart_gherkin

View File

@ -1,13 +1,5 @@
import 'package:flutter_gherkin/flutter_gherkin.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 'mocks/step_definition_mock.dart';
@ -31,21 +23,6 @@ void main() {
config.prepare();
expect(config.stepDefinitions, isNotNull);
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', () {
@ -58,8 +35,6 @@ void main() {
expect(config.stepDefinitions.length, 10);
expect(config.stepDefinitions.elementAt(0),
(x) => x is MockStepDefinition);
expect(config.stepDefinitions.elementAt(1),
(x) => x is ThenExpectElementToHaveValue);
});
});
});