diff --git a/.gitignore b/.gitignore index 446ed0d..25fbfc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,23 @@ .DS_Store -.dart_tool/ - -.packages -.pub/ -build/ ios/.generated/ ios/Flutter/Generated.xcconfig ios/Runner/GeneratedPluginRegistrant.* + +node_modules +package-lock.json + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5b4b3d9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "dart-code.dart-code", + "dart-code.flutter", + "alexkrechik.cucumberautocomplete" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1478a61..48a0230 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,16 +5,33 @@ "version": "0.2.0", "configurations": [ { - "name": "Flutter", + "name": "Debug integration_test app", "request": "launch", - "type": "dart" + "type": "dart", + "cwd": "example_with_integration_test/", + }, + { + "name": "Debug integration_test example tests", + "program": "test_driver/integration_test_driver.dart", + "cwd": "example_with_integration_test/", + "request": "launch", + "type": "dart", + "args": [ + "--target=integration_test/gherkin_suite_test.dart", + ], }, { - "name": "Debug example tests", - "program": "app_test.dart", - "cwd": "example/test_driver", + "name": "Debug flutter_driver app", "request": "launch", "type": "dart", - } + "cwd": "example_with_flutter_driver/", + }, + { + "name": "Debug flutter_driver example tests", + "program": "test_harness.dart", + "cwd": "example_with_flutter_driver/test_driver", + "request": "launch", + "type": "dart", + }, ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ef0e83b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "cSpell.words": [ + "agnostically", + "analyzer", + "dialog", + "Errored", + "Flavor", + "microtask", + "multiline", + "pubspec", + "rxdart", + "scrollable", + "Serializable", + "todos", + "writeln" + ], + "cSpell.language": "en-GB" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f47bd4..432c9af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,233 @@ +## [3.0.0-rc.17] - 25/07/2022 + - Fix #257 - fixed issue when generating a step with a '$' sign in + - Fix #256 - Ensure all exceptions generated when running a step are logged + - Fix #253 - Ensure features with descriptions that span more than one line are parsed correctly + - Fix #252 - Ensure all async code is awaited + - When taking a screenshot on the web use the render element rather than relying on native code that does not work + +## [3.0.0-rc.16] - 01/07/2022 + - Fix #231 - using local coordinate system when taking a screenshot on Android (thanks to @youssef-t for the solution) + - Fix #216 - ensure step exceptions and `expect` failure results are added as errors to the json report + - Scenarios can now have descriptions which also appear in the json reporter output + +NOTE: Due to the above changes generated files will need to be re-generated + +``` +flutter pub run build_runner clean +flutter pub run build_runner build --delete-conflicting-outputs +``` + +## [3.0.0-rc.15] - 28/06/2022 + - Exposed `frameBindingPolicy` on the test runner when running tests which can affect how frames are painted and the speed of the test run, I've removed the default value which might be responsible for #231 + +## [3.0.0-rc.14] - 28/06/2022 + - Fix #237 - Ensure everything works on the web + +## [3.0.0-rc.13] - 27/06/2022 + - Fix #235 - fix issue taking a screenshot on an Android device + - Resolved #170: Added example code to ensure json report is save to disk even when the test run fails. Also added script to generate a HTML report from a JSON report + +## [3.0.0-rc.12] - 24/06/2022 + - Fix #222 - escape single quotation marks in data tables + +## [3.0.0-rc.11] - 24/06/2022 + - Fix #231 - Removed the use of explicitly calling `pumpAndSettle` in the pre-defined steps in favour of the implicit `pumpAndSettle` calls used in the `WidgetTesterAppDriverAdapter`. + - Added ability to add a `appLifecyclePumpHandler` to override the default handler that determines how the app is pumped during lifecycle events. Useful if your app has a long splash screen etc. Parameter is on `executeTestSuite`. + - Added ability to ensure feature paths are relative when generating reports `useAbsolutePaths` on the `GherkinTestSuite` attribute + +* BREAKING CHANGE: The parameters on `executeTestSuite` are now keyed to allow for the above changes + +## [3.0.0-rc.10] - 23/06/2022 + +- Fix #195: Adding missing export for `wait_until_key_exists_step.dart` +- Fix #226: Allow compatibility with dev and master flutter branches +- Feat #218: Allow retry steps in case of intermittent failure by setting the configuration properties `stepMaxRetries` & `retryDelay` +- Fix #210 & #191: Ability to take screenshots on web +- Fix #198: Allow the use of implicit pumpAndSettle methods in the app driver to be turned off using the configuration property `waitImplicitlyAfterAction`. Off by default + +* BREAKING CHANGE: +- `NEW API FOR REPORTERS`: All reporters implement (do not extend) separated interfaces see https://github.com/jonsamwell/dart_gherkin/blob/master/CHANGELOG.md#300---16052022 + +**Note: this release will soon be promoted the main version** + +## [3.0.0-rc.9] - 18/11/2021 + +- Fix: #172: Fix for the `StdoutReporter` when running against the web + +## [3.0.0-rc.8] - 18/11/2021 + +- Fix: #165: Fix when generating empty feature files - many thanks to @AFASbart for the PR. + +## [3.0.0-rc.7] - 10/11/2021 + +- Fix: #165: Empty .feature files causing void functions which get compiled out at runtime and cause errors +- Fix: #162: Incorrect feature name in HTML reports - many thanks to @AFASbart for suggesting the cause and fix. +- Fix: #159: Swipe step is not working due to bad '??' statement +- Fix: #155: Ensure stdout reporter only add ascii colour code when the target supports it + +## [3.0.0-rc.6] - 27/10/2021 + +- BREAKING CHANGE: Made `appMainFunction` return a `Future` so it can be async +- Fix: #159: Swipe step not working +- Ensure Hook.onBeforeRun is called before the run starts +- Set Frame policy- defaults to `LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive` to slightly improve performance + +## [3.0.0-rc.5] - 22/06/2021 + +- Ensure scenario support files (world etc) as always disposed ensure when test throws error + +## [3.0.0-rc.4] - 21/06/2021 + +- Removed debug code + +## [3.0.0-rc.3] - 21/06/2021 + +- POSSIBLE BREAKING CHANGE: Removed tap call before enterText is invoked in `WidgetTesterAppDriverAdapter` this was due to the fact that it opens the on-screen keyboard which is not closed after the text is entered so it could be blocking further controls from view. +- Fix: #150: Better handling of JSON strings in multiline string segments in feature files +- Fix: #141 & #128: Added example of how to generate html report from json report output and fixed all scenarios ending up in the last feature section of the json report + +## [3.0.0-rc.2] - 21/06/2021 + +- Fixed late initialization error when invoking hooks +- Updated float parameter parser so an exception is not thrown during parsing + +## [3.0.0-rc.1] - 25/05/2021 + + HUGE update so that the library now works and favours the flutter integration_test package over flutter_driver. Unfortunately, this will be breaking change to existing users but it has many benefits such as a huge speed and stability improvements. + +# BREAKING CHANGES + +- `Table` has been renamed to `GherkinTable` to avoid naming clashes + +In order to progress this library and add support for the new integration_test package various things have had to be changed to enable this will still supporting Flutter Driver. The big of which is removing Flutter Driver instance from the `FlutterWorld` instance in favour of an adapter approach whereby driving of the app (whether that is via `flutter_driver` or `WidgetTester`) becomes agnostic see `https://github.com/jonsamwell/flutter_gherkin/blob/f1fb2d4a632362629f5d1a196a0c055f858ad1d7/lib/src/flutter/adapters/app_driver_adapter.dart`. + +- `FlutterDriverUtils` has been removed, use `world.appDriver` instead. You can still access the raw driver if needed via `world.appDriver.nativeDriver` +- If you are using a custom world object and still want to use Flutter Driver it will need to extend `FlutterDriverWorld` instead of `FlutterWorld` this will give you type safety on the `world.appDriver.nativeDriver` property + +The change to use the `integration_test` package is a fundamentally different approach. Where using the `flutter_driver` implementation your app is launch in a different process and then controlled by remote RPC calls from flutter driver in a different process. Using the new `integration_test` package your tests surround your app and become the app themselves. This removes the need for RPC communication from an external process into the app as well as giving you access to the internal state of your app. This is an altogether better approach, one that is quicker, more maintainable, scalable to device testing labs. However, it brings with it, its own set of challenges when trying to make this library work with it. Traditionally this library has evaluated the Gherkin feature files at run time, then used that evaluation to invoke actions against the app under test. However, as the tests need to surround the app in the `integration_test` view of the world the Gherkin tests need to be generated at development time so they can be complied in to a test app. Much like `json_serializable` creates classes that are able to work with json data. + +### Steps to get going + +1. Add the following `dev_dependencies` to your app's `pubspec.yaml` file + - integration_test + - build_runner + - flutter_gherkin +2. Add the following `build.yaml` to the root of your project. This file allows the dart code generator to target files outside of your application's `lib` folder +```yaml +targets: + $default: + sources: + - lib/** + - pubspec.* + - $package$ + # Allows the code generator to target files outside of the lib folder + - integration_test/**.dart +``` +3. Add the following file (and folder) `\test_driver\integration_test_driver.dart`. This file is the entry point to run your tests. See `https://flutter.dev/docs/testing/integration-tests` for more information. +```dart +import 'package:integration_test/integration_test_driver.dart' as integration_test_driver; + +Future main() { + // The Gherkin report data send back to this runner by the app after + // the tests have run will be saved to this directory + integration_test_driver.testOutputsDirectory = 'integration_test/gherkin/reports'; + + return integration_test_driver.integrationDriver( + timeout: Duration(minutes: 90), + ); +} +``` +4. Create a folder call `integration_test` this will eventually contain all your Gherkin feature files and the generated test files. +5. Add the following file (and folder) `integration_test\features\counter.feature` with the following below contents. This is a basic feature file that will be transform in to a test file that can run a test against the sample app. +``` +Feature: Counter + +Scenario: User can increment the counter + Given I expect the "counter" to be "0" + When I tap the "increment" button + Then I expect the "counter" to be "1" +``` +6. Add the following file (and folder) `integration_test\gherkin_suite_test.dart`. Notice the attribute `@GherkinTestSuite()` this indicates to the code generator to create a partial file for this file with the generated Gherkin tests in `part 'gherkin_suite_test.g.dart';`. Don't worry about the initial errors as this will disappear when the tests are generated. +```dart +import 'package:flutter_gherkin/flutter_gherkin.dart'; // notice new import name +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +// The application under test. +import 'package:example_with_integration_test/main.dart' as app; + +part 'gherkin_suite_test.g.dart'; + +@GherkinTestSuite() +void main() { + executeTestSuite( + FlutterTestConfiguration.DEFAULT([]) + ..reporters = [ + StdoutReporter(MessageLevel.error) + ..setWriteLineFn(print) + ..setWriteFn(print), + ProgressReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + TestRunSummaryReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + JsonReporter( + writeReport: (_, __) => Future.value(), + ), + ], + (World world) => app.main(), + ); +} +``` +7. We now need to generate the test by running the builder command from the command line in the root of your project. Much like `json_serializable` this will create a `.g.dart` part file that will contain the Gherkin tests in code format which are able to via using the `integration_test` package. +``` +flutter pub run build_runner build +``` +8. The errors in the `integration_test\gherkin_suite_test.dart` file should have not gone away and it you look in `integration_test\gherkin_suite_test.g.dart` you will see the coded version of the Gherkin tests described in the feature file `integration_test\features\counter.feature`. +9. We can now run the test using the below command from the root of your project. +``` +flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart +``` +10. You can debug the tests by adding a breakpoint to line 12 in `integration_test\gherkin_suite_test.dart` and adding the below to your `.vscode\launch.json` file: +```json +{ + "name": "Debug integration_test", + "program": "test_driver/integration_test_driver.dart", + "cwd": "example_with_integration_test/", + "request": "launch", + "type": "dart", + "args": [ + "--target=integration_test/gherkin_suite_test.dart", + ], +} +``` +11. Custom world need to extend `FlutterWorld` note `FlutterDriverWorld`. +12. If you change any of the feature files you will need to re-generate the tests using the below command +``` +# you might need to run the clean command first if you have just changed feature files +flutter pub run build_runner clean + +flutter pub run build_runner build +``` + +## [2.0.0] - 25/05/2021 + * null-safety migration, thanks to @tshedor + +## [1.2.0] - 02/05/2021 + +* Upgraded to the null-safety version of dart_gherkin, as such there are some breaking changes to be aware of (see https://github.com/jonsamwell/dart_gherkin/blob/master/CHANGELOG.md for the full list): + - BREAKING CHANGE: Table has been renamed to GherkinTable to avoid naming clashes + - BREAKING CHANGE: exitAfterTestRun configuration option has been removed as it depends on importing dart:io which is not available under certain environments (dartjs for example). + - BREAKING CHANGE: Reporter->onException() exception parameter is now an object rather than an exception + - POSSIBLE BREAKING CHANGE: Feature file discovery has been refactored to abstract it from the external Glob dependency. It now support the three native dart Patterns (String, RegExp & Glob). There is potential here for your patterns to not work anymore due as the default IoFeatureFileAccessor assumes the current directory is the working directory to search from. For the most part this simple regex is probably enough to get you going. + + ``` + RegExp('features/*.*.feature') + ``` + +* Allow dart-define to be passed to the Flutter build (thanks @Pholey) + ## [1.1.9] - 24/11/2020 * Fixes #93 & #92 - Error waiting for no transient callbacks from Flutter driver * Added option to leave Flutter app under test running when the tests finish see `keepAppRunningAfterTests` configuration property diff --git a/README.md b/README.md index 685aed2..30cccdc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This implementation of the Gherkin tries to follow as closely as possible other Available as a Dart package https://pub.dartlang.org/packages/flutter_gherkin -``` dart +```dart # Comment Feature: Addition @@ -26,6 +26,126 @@ Available as a Dart package https://pub.dartlang.org/packages/flutter_gherkin Then I end up with 2 ``` +## integration_test package support + +NOTE: This library now favours using the `integration_test` package and code generation over `flutter_driver` and runtime interpretation and as such the `flutter_driver` implementations will eventually be deprecated. + + +### Steps to get going + +1. Add the following `dev_dependencies` to your app's `pubspec.yaml` file + - integration_test + - build_runner + - flutter_gherkin +2. Add the following `build.yaml` to the root of your project. This file allows the dart code generator to target files outside of your application's `lib` folder +```yaml +targets: + $default: + sources: + - lib/** + - pubspec.* + - $package$ + # Allows the code generator to target files outside of the lib folder + - integration_test/**.dart +``` +3. Add the following file (and folder) `\test_driver\integration_test_driver.dart`. This file is the entry point to run your tests. +If you want ot use the flutter test command approach, you will not need this file (and be unused when it is created). +See `https://flutter.dev/docs/testing/integration-tests` for more information. +```dart +import 'package:integration_test/integration_test_driver.dart' as integration_test_driver; + +Future main() { + // The Gherkin report data send back to this runner by the app after + // the tests have run will be saved to this directory + integration_test_driver.testOutputsDirectory = 'integration_test/gherkin/reports'; + + return integration_test_driver.integrationDriver( + timeout: Duration(minutes: 90), + ); +} +``` +4. Create a folder call `integration_test` this will eventually contain all your Gherkin feature files and the generated test files. +5. Add the following file (and folder) `integration_test\features\counter.feature` with the following below contents. This is a basic feature file that will be transform in to a test file that can run a test against the sample app. +``` +Feature: Counter + +Scenario: User can increment the counter + Given I expect the "counter" to be "0" + When I tap the "increment" button + Then I expect the "counter" to be "1" +``` +6. Add the following file (and folder) `integration_test\gherkin_suite_test.dart`. Notice the attribute `@GherkinTestSuite()` this indicates to the code generator to create a partial file for this file with the generated Gherkin tests in `part 'gherkin_suite_test.g.dart';`. Don't worry about the initial errors as this will disappear when the tests are generated. +```dart +import 'package:flutter_gherkin/flutter_gherkin.dart'; // notice new import name +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +// The application under test. +import 'package:example_with_integration_test/main.dart' as app; + +part 'gherkin_suite_test.g.dart'; + +@GherkinTestSuite( + featurePaths: ['integration_test/features/**.feature'], + executionOrder: ExecutionOrder.sequential) +Future main() async { + + var configuration = FlutterTestConfiguration( + reporters: [ + TestRunSummaryReporter(), + JsonReporter( + writeReport: (_, __) => Future.value(), + ), + ], + ); + + await executeTestSuite( + configuration: configuration, + appMainFunction: (World world) async => app.main(), + ); +} +``` +7. We now need to generate the test by running the builder command from the command line in the root of your project. Much like `json_serializable` this will create a `.g.dart` part file that will contain the Gherkin tests in code format which are able to via using the `integration_test` package. +``` +flutter pub run build_runner build +``` +8. The errors in the `integration_test\gherkin_suite_test.dart` file should have not gone away and it you look in `integration_test\gherkin_suite_test.g.dart` you will see the coded version of the Gherkin tests described in the feature file `integration_test\features\counter.feature`. +9. We can now run the test using the below command from the root of your project. +``` +flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart +``` + +If you do not want to use the flutter drive command, but the flutter test command you need to change some aspects. +It is REQUIRED that in `integration_test\gherkin_suite_test.dart` the executeTestSuite is awaited. +And then you can run your test command: +``` +flutter test integration_test/gherkin_suite_test.dart +``` + +10. You can debug the tests by adding a breakpoint to line 12 in `integration_test\gherkin_suite_test.dart` and adding the below to your `.vscode\launch.json` file: +```json +{ + "name": "Debug integration_test", + "program": "test_driver/integration_test_driver.dart", + "cwd": "example_with_integration_test/", + "request": "launch", + "type": "dart", + "args": [ + "--target=integration_test/gherkin_suite_test.dart", + ], +} +``` +11. Custom world need to extend `FlutterWorld` note `FlutterDriverWorld`. +12. If you change any of the feature files you will need to re-generate the tests using the below command +``` +# you might need to run the clean command first if you have just changed feature files +flutter pub run build_runner clean + +flutter pub run build_runner build +``` +## Note - Package upgrades +This package will soon have a major release to support null-safety and then another major release to support running tests using the integration_test package and `WidgetTester`. We will still maintain compatibility for running tests using flutter_driver and do our best so that switching over to using the integration_test package will be seamless. For this to happen we have had to refactor large chunks of the code base so unfortunately there will be some unavoidable breaking changes. + ## Table of Contents @@ -41,12 +161,12 @@ Available as a Dart package https://pub.dartlang.org/packages/flutter_gherkin - [hooks](#hooks) - [reporters](#reporters) - [createWorld](#createworld) - - [exitAfterTestRun](#exitaftertestrun) + [Flutter specific configuration options](#flutter-specific-configuration-options) - [restartAppBetweenScenarios](#restartappbetweenscenarios) - [build](#build) - [buildFlavor](#buildFlavor) - [buildMode](#buildMode) + - [dartDefineArgs](#dartDefineArgs) - [flutterBuildTimeout](#flutterBuildTimeout) - [logFlutterProcessOutput](#logFlutterProcessOutput) - [targetDeviceId](#targetDeviceId) @@ -165,7 +285,6 @@ Now that we have a testable app, a feature file and a custom step definition we import 'dart:async'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:glob/glob.dart'; import 'hooks/hook_example.dart'; import 'steps/colour_parameter.dart'; import 'steps/given_I_pick_a_colour_step.dart'; @@ -173,7 +292,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [ ProgressReporter(), TestRunSummaryReporter(), @@ -183,14 +302,13 @@ Future main() { ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" + ..targetAppPath = "test_driver/app.dart"; // ..tagExpression = "@smoke" // uncomment to see an example of running scenarios based on tag expressions - ..exitAfterTestRun = true; // set to false if debugging to exit cleanly return GherkinRunner().execute(config); } ``` -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 reporters to the `ProgressReporter` report which prints the result of scenarios and steps to the standard output (console). The `TestRunSummaryReporter` prints a summary of the run once all tests have been executed. 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 `RegExp` is provide which specifies the path to one or more feature files, it sets the reporters to the `ProgressReporter` report which prints the result of scenarios and steps to the standard output (console). The `TestRunSummaryReporter` prints a summary of the run once all tests have been executed. 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: @@ -212,7 +330,7 @@ The parameters below can be specified in your configuration file: *Required* -An iterable of `Glob` patterns that specify the location(s) of `*.feature` files to run. See +An iterable of `Pattern` that specify the location(s) of `*.feature` files to run. See #### tagExpression @@ -223,7 +341,7 @@ An infix boolean expression which defines the features and scenarios to run base #### order 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. Running with `ExecutionOrder.sorted` processes the feature files in `filename` order. #### stepDefinitions @@ -234,18 +352,16 @@ Place instances of any custom step definition classes `Given` , `Then` , `When` import 'dart:async'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:glob/glob.dart'; import 'steps/given_I_pick_a_colour_step.dart'; import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; // set to false if debugging to exit cleanly + ..targetAppPath = "test_driver/app.dart"; return GherkinRunner().execute(config); } ``` @@ -298,20 +414,19 @@ Place instances of any custom step parameters that you have defined. These will import 'dart:async'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:glob/glob.dart'; import 'steps/given_I_pick_a_colour_step.dart'; import 'steps/tap_button_n_times_step.dart'; import 'steps/colour_parameter.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; // set to false if debugging to exit cleanly + ..targetAppPath = "test_driver/app.dart"; + return GherkinRunner().execute(config); } ``` @@ -349,7 +464,6 @@ To take a screenshot on a step failing you can used the pre-defined hook `Attach import 'dart:async'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:glob/glob.dart'; import 'hooks/hook_example.dart'; import 'steps/colour_parameter.dart'; import 'steps/given_I_pick_a_colour_step.dart'; @@ -357,7 +471,7 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [ ProgressReporter(), TestRunSummaryReporter(), @@ -367,8 +481,8 @@ Future main() { ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; // set to false if debugging to exit cleanly + ..targetAppPath = "test_driver/app.dart"; + return GherkinRunner().execute(config); } ``` @@ -387,7 +501,6 @@ You should provide at least one reporter in the configuration otherwise it'll be ``` dart import 'dart:async'; -import 'package:glob/glob.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'steps/colour_parameter.dart'; import 'steps/given_I_pick_a_colour_step.dart'; @@ -395,13 +508,13 @@ import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..customStepParameterDefinitions = [ColourParameter()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; + ..targetAppPath = "test_driver/app.dart"; + return GherkinRunner().execute(config); } ``` @@ -414,20 +527,19 @@ While it is not recommended so share state between steps within the same scenari ``` dart import 'dart:async'; -import 'package:glob/glob.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'steps/given_I_pick_a_colour_step.dart'; import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [StdoutReporter()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..createWorld = (TestConfiguration config) async => await createMyWorldInstance(config) ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; + ..targetAppPath = "test_driver/app.dart"; + return GherkinRunner().execute(config); } ``` @@ -460,11 +572,6 @@ Specifies the number of Flutter driver connection attempts to a running app befo 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` -True to exit the program after all tests have run. You may want to set this to false during debugging. - ### Flutter specific configuration options The `FlutterTestConfiguration` will automatically create some default Flutter options such as well know step definitions, the Flutter world context object which provides access to a Flutter driver instance as well as the ability to restart you application under test between scenarios. Most of the time you should use this configuration object if you are testing Flutter applications. @@ -502,6 +609,12 @@ Defaults to `BuildMode.Debug` This optional argument lets you specify which build mode you prefer while compiling your app. Flutter Gherkin supports `--debug` and `--profile` modes. Check [Flutter's build modes](https://flutter.dev/docs/testing/build-modes) documentation for more details. +#### dartDefineArgs + +Defaults to `[]` + +`--dart-define` args to pass into the build parameters. Include the name and value for each. For example, `--dart-define=MY_VAR="true"` becomes `['MY_VAR="true"']` + #### targetDeviceId Defaults to empty string @@ -870,20 +983,18 @@ Finally ensure the hook is added to the hook collection in your configuration fi import 'dart:async'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:glob/glob.dart'; import 'hooks/hook_example.dart'; import 'steps/given_I_pick_a_colour_step.dart'; import 'steps/tap_button_n_times_step.dart'; Future main() { final config = FlutterTestConfiguration() - ..features = [Glob(r"test_driver/features/**.feature")] + ..features = [RegExp('features/*.*.feature')] ..reporters = [ProgressReporter()] ..hooks = [HookExample()] ..stepDefinitions = [TapButtonNTimesStep(), GivenIPickAColour()] ..restartAppBetweenScenarios = true - ..targetAppPath = "test_driver/app.dart" - ..exitAfterTestRun = true; + ..targetAppPath = "test_driver/app.dart"; return GherkinRunner().execute(config); } @@ -1005,3 +1116,78 @@ Setting the configuration property `runningAppProtocolEndpointUri` to the servic NOTE: ensure the app you are trying to connect to calls `enableFlutterDriverExtension()` when it starts up otherwise the Flutter Driver will not be able to connect to it. Also ensure that the `--verbose` flag is set when starting the app to test, this will then log the service protocol endpoint out to the console which is the uri you will need to set this property to. It usually takes the form of `Connecting to service protocol: http://127.0.0.1:51540/EM72VtRsUV0=/` so set the `runningAppProtocolEndpointUri` to `http://127.0.0.1:51540/EM72VtRsUV0=/` and then start the tests. + +##### Interactive debugging +One way to configure your test environment is to run the app under test in a separate terminal and run the gherkin in a different terminal. With this approach you can hot reload the app by entering `R` in the app terminal and run the steps repeatedly in the other terminal **with out** incurring the cost of the app start up. + +For the app under test, in this case `lib/main_test.dart`, it should look similar to this: + +``` +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; +void main() { + enableFlutterDriverExtension(); +runApp(); +``` + +When you start this from the terminal, run like this: + +` flutter run -t lib/main_test.dart --verbose` + +As stated above, with the `--verbose` flag, you will want to find the service protocol endpoint. +You should see similar output as this: + +``` +..... +Connecting to service protocol: http://127.0.0.1:61658/RtsPT2zp_qs=/ +..... +Flutter run key commands. +[ +2 ms] r Hot reload. 🔥🔥🔥 +[ +1 ms] R Hot restart. +[ ] h Repeat this help message. +[ ] d Detach (terminate "flutter run" but leave application running). +[ ] c Clear the screen +[ ] q Quit (terminate the application on the device). +[ ] An Observatory debugger and profiler on iPhone 8 Plus is available at: http://127.0.0.1:61660/xgrsw_qQ9sI=/ +[ ] Running with unsound null safety +[ ] For more information see https://dart.dev/null-safety/unsound-null-safety +``` + +To run the gherkin tests, first update the `test_driver/app_test.dart` to something similar to this: + +``` +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; +Future main(List args) async { +if (args.isEmpty) { + print('please pass in the uri'); + exit(1); +} +final Iterable> steps = []; +final config = FlutterTestConfiguration.DEFAULT( + steps, + featurePath: 'features//**.feature', + targetAppPath: 'test_driver/app.dart', +) + ..restartAppBetweenScenarios = false + ..targetAppWorkingDirectory = '../' + ..runningAppProtocolEndpointUri = args[0]; + return GherkinRunner().execute(config); +} +``` + +Start a new terminal and navigate to the `test_driver` directory. + +Notice the `app_test.dart` expects a parameter. This is to ease the changing uri which will occur each time the app under test is started. If you use the `R` command, the `uri` does not change. + +You can copy the `uri` from the terminal window of the app under test. + +Run the command `dart app_test.dart `. As an example, the app under test has this line: + `Connecting to service protocol: http://127.0.0.1:61658/RtsPT2zp_qs=/` +so you would copy `http://127.0.0.1:61658/RtsPT2zp_qs=/` and paste it as such: + +`dart app_test.dart http://127.0.0.1:59862/luEyFXvK9Qc=/`. + +As you make changes in the app under test, just `R` (reload). In the test window you can rerun the tests and update the Scenarios quickly and easily. diff --git a/analysis_options.yaml b/analysis_options.yaml index 69385a3..839cc64 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,29 @@ -include: package:pedantic/analysis_options.1.9.0.yaml \ No newline at end of file +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..d1eadf1 --- /dev/null +++ b/build.yaml @@ -0,0 +1,10 @@ +# Read about `build.yaml` at https://pub.dev/packages/build_config +builders: + + gherkin_test_suite: + import: "package:flutter_gherkin/src/flutter/code_generation/builders/gherkin_test_suite_builder.dart" + builder_factories: ["gherkinTestSuiteBuilder"] + build_extensions: {".dart": ["gherkin_tests.g.part"]} + auto_apply: dependents + build_to: cache + applies_builders: ["source_gen|combining_builder"] \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index 47e0b4d..0000000 --- a/example/.gitignore +++ /dev/null @@ -1,71 +0,0 @@ -# Miscellaneous -*.class -*.lock -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# Visual Studio Code related -.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.packages -.pub-cache/ -.pub/ -build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 40781e6..0000000 --- a/example/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Running the example - -To run this example: - -1. Ensure dart is accessible in the command line (on your path variable) -2. Ensure an emulator or device is connected -3. In a command prompt (from the root of this library): - ```bash - cd example/test_driver - - dart app_test.dart - ``` -This will run the features files found in the folder `test_driver/features` against this example app. - -## Debugging the example - -To debug this example and step through the library code. - -1. Set a break point in `test_driver/app_test.dart` -2. Set `exitAfterTestRun` on the configuration to false to ensure exiting cleanly as the IDE will handle exiting -3. If you are in VsCode you will simply be able to select `Debug example` from the dropdown in the `debugging tab` as the `launch.json` has been configured. - - otherwise you will need to run a debugging session against `test_driver/app_test.dart`. diff --git a/example/android/app/src/main/java/com/example/example/MainActivity.java b/example/android/app/src/main/java/com/example/example/MainActivity.java deleted file mode 100644 index 84f8920..0000000 --- a/example/android/app/src/main/java/com/example/example/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa441..0000000 --- a/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 5a2f14f..0000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100644 index 863594b..0000000 --- a/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=C:\Google\flutter" -export "FLUTTER_APPLICATION_PATH=C:\github\flutter_gherkin\example" -export "FLUTTER_TARGET=lib\main.dart" -export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build\ios" -export "OTHER_LDFLAGS=$(inherited) -framework Flutter" -export "FLUTTER_FRAMEWORK_DIR=C:\Google\flutter\bin\cache\artifacts\engine\ios" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=false" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.packages" diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bb..0000000 --- a/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e9..0000000 --- a/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index 3d43d11..0000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m deleted file mode 100644 index dff6597..0000000 --- a/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index 89ca0d2..0000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() => runApp(MyApp()); - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Counter App', - home: MyHomePage(title: 'Counter App Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - bool hasLongPressedText = false; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - drawer: Drawer( - key: const Key('drawer'), - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - child: const Text('Drawer Header'), - decoration: BoxDecoration( - color: Colors.blue, - ), - ), - ListTile( - title: const Text('Item 1'), - onTap: () { - // Update the state of the app - // ... - // Then close the drawer - Navigator.pop(context); - }, - ), - ListTile( - title: const Text('Item 2'), - onTap: () { - // Update the state of the app - // ... - // Then close the drawer - Navigator.pop(context); - }, - ), - ], - ), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - // Provide a Key to this specific Text Widget. This allows us - // to identify this specific Widget from inside our test suite and - // read the text. - key: const Key('counter'), - style: Theme.of(context).textTheme.headline4, - ), - FlatButton( - key: Key('openPage2'), - child: Text('Open page 2'), - onLongPress: () { - Future.delayed( - Duration(seconds: 12), - () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => PageTwo()), - )); - }, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => PageTwo()), - ); - }, - ), - GestureDetector( - onLongPress: () { - setState(() { - hasLongPressedText = true; - }); - }, - child: Container( - color: - hasLongPressedText ? Colors.blueGrey : Colors.transparent, - child: Text( - hasLongPressedText - ? 'Text has been long pressed!' - : 'Text that has not been long pressed', - key: const Key('longPressText'), - ), - ), - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - // Provide a Key to this the button. This allows us to find this - // specific button and tap it inside the test suite. - key: const Key('increment'), - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - ); - } -} - -class PageTwo extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - key: Key('pageTwo'), - appBar: AppBar( - automaticallyImplyLeading: true, - title: Text('Page 2'), - ), - body: SafeArea( - child: Center( - child: Text('Contents of page 2'), - ), - ), - ); - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml deleted file mode 100644 index 3afebf0..0000000 --- a/example/pubspec.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: example -description: A new Flutter project. - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# Read more about versioning at semver.org. -version: 1.0.0+1 - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - flutter_gherkin: - path: ../ - path: - glob: - -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages diff --git a/example/report.json b/example/report.json deleted file mode 100644 index e89058b..0000000 --- a/example/report.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"","id":"drawer","keyword":"Feature","line":1,"name":"Drawer","uri":".\\test_driver\\features\\drawer.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"drawer;should open the drawer","name":"should open the drawer","description":"","line":3,"steps":[{"keyword":"Given ","name":"I open the drawer","line":4,"match":{"location":".\\test_driver\\features\\drawer.feature:4"},"result":{"status":"passed","duration":1458000000}},{"keyword":"Given ","name":"I close the drawer","line":5,"match":{"location":".\\test_driver\\features\\drawer.feature:5"},"result":{"status":"passed","duration":451000000}}]}]},{"description":"","id":"custom parameter example","keyword":"Feature","line":1,"name":"Custom Parameter Example","uri":".\\test_driver\\features\\custom_parameter_example.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"custom parameter example;custom colour parameter","name":"Custom colour parameter","description":"","line":4,"steps":[{"keyword":"Given ","name":"I pick the colour red","line":5,"match":{"location":".\\test_driver\\features\\custom_parameter_example.feature:5"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I pick the colour green","line":6,"match":{"location":".\\test_driver\\features\\custom_parameter_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I pick the colour blue","line":7,"match":{"location":".\\test_driver\\features\\custom_parameter_example.feature:7"},"result":{"status":"passed","duration":0}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\test_driver\\features\\counter_increases.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":5,"tags":[{"line":4,"name":"@smoke"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\test_driver\\features\\counter_increases.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\test_driver\\features\\counter_increases.feature:7"},"result":{"status":"passed","duration":40000000}},{"keyword":"When ","name":"I tap the \"increment\" button 10 times","line":8,"match":{"location":".\\test_driver\\features\\counter_increases.feature:8"},"result":{"status":"passed","duration":2481000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"10\"","line":9,"match":{"location":".\\test_driver\\features\\counter_increases.feature:9"},"result":{"status":"passed","duration":27000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":2,"name":"Counter","uri":".\\test_driver\\features\\counter_increases_french.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":6,"tags":[{"line":5,"name":"@smoke"}],"steps":[{"keyword":"Etant ","name":"donné que I pick the colour red","line":7,"match":{"location":".\\test_driver\\features\\counter_increases_french.feature:7"},"result":{"status":"passed","duration":0}},{"keyword":"Et ","name":"I expect the \"counter\" to be \"0\"","line":8,"match":{"location":".\\test_driver\\features\\counter_increases_french.feature:8"},"result":{"status":"passed","duration":41000000}},{"keyword":"Quand ","name":"I tap the \"increment\" button 10 times","line":9,"match":{"location":".\\test_driver\\features\\counter_increases_french.feature:9"},"result":{"status":"passed","duration":2480000000}},{"keyword":"Alors ","name":"I expect the \"counter\" to be \"10\"","line":10,"match":{"location":".\\test_driver\\features\\counter_increases_french.feature:10"},"result":{"status":"passed","duration":26000000}}]}]},{"description":"","id":"startup","keyword":"Feature","line":1,"name":"Startup","uri":".\\test_driver\\features\\counter.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"startup;should increment counter","name":"should increment counter","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\test_driver\\features\\counter.feature:4"},"result":{"status":"passed","duration":34000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":5,"match":{"location":".\\test_driver\\features\\counter.feature:5"},"result":{"status":"passed","duration":297000000}},{"keyword":"And ","name":"I tap the \"increment\" button","line":6,"match":{"location":".\\test_driver\\features\\counter.feature:6"},"result":{"status":"passed","duration":242000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"2\"","line":7,"match":{"location":".\\test_driver\\features\\counter.feature:7"},"result":{"status":"passed","duration":28000000}}]},{"keyword":"Scenario","type":"scenario","id":"startup;counter should reset when app is restarted","name":"counter should reset when app is restarted","description":"","line":9,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":10,"match":{"location":".\\test_driver\\features\\counter.feature:10"},"result":{"status":"passed","duration":37000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":11,"match":{"location":".\\test_driver\\features\\counter.feature:11"},"result":{"status":"passed","duration":303000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":12,"match":{"location":".\\test_driver\\features\\counter.feature:12"},"result":{"status":"passed","duration":24000000}},{"keyword":"When ","name":"I restart the app","line":13,"match":{"location":".\\test_driver\\features\\counter.feature:13"},"result":{"status":"passed","duration":2068000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"0\"","line":14,"match":{"location":".\\test_driver\\features\\counter.feature:14"},"result":{"status":"passed","duration":33000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\test_driver\\features\\sub-features\\counter_increases.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":5,"tags":[{"line":4,"name":"@perf"}],"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":6,"match":{"location":".\\test_driver\\features\\sub-features\\counter_increases.feature:6"},"result":{"status":"passed","duration":34000000}},{"keyword":"When ","name":"I tap the \"increment\" button 20 times","line":7,"match":{"location":".\\test_driver\\features\\sub-features\\counter_increases.feature:7"},"result":{"status":"passed","duration":4930000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"20\"","line":8,"match":{"location":".\\test_driver\\features\\sub-features\\counter_increases.feature:8"},"result":{"status":"passed","duration":24000000}}]}]},{"description":"","id":"startup","keyword":"Feature","line":1,"name":"Startup","uri":".\\test_driver\\features\\app_restart.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"startup;counter should reset when app is restarted","name":"counter should reset when app is restarted","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\test_driver\\features\\app_restart.feature:4"},"result":{"status":"passed","duration":35000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":5,"match":{"location":".\\test_driver\\features\\app_restart.feature:5"},"result":{"status":"passed","duration":297000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":6,"match":{"location":".\\test_driver\\features\\app_restart.feature:6"},"result":{"status":"passed","duration":24000000}},{"keyword":"When ","name":"I restart the app","line":7,"match":{"location":".\\test_driver\\features\\app_restart.feature:7"},"result":{"status":"passed","duration":2068000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"0\"","line":8,"match":{"location":".\\test_driver\\features\\app_restart.feature:8"},"result":{"status":"passed","duration":41000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature","elements":[{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed (example 1)","name":"Counter increases when the button is pressed (Example 1)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":36000000}},{"keyword":"When ","name":"I tap the \"increment\" button 1 times","line":8,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":307000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":9,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":22000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed (example 2)","name":"Counter increases when the button is pressed (Example 2)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":38000000}},{"keyword":"When ","name":"I tap the \"increment\" button 2 times","line":8,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":546000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"2\"","line":9,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":26000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed (example 3)","name":"Counter increases when the button is pressed (Example 3)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":40000000}},{"keyword":"When ","name":"I tap the \"increment\" button 5 times","line":8,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":1303000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"5\"","line":9,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":25000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed (example 4)","name":"Counter increases when the button is pressed (Example 4)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":36000000}},{"keyword":"When ","name":"I tap the \"increment\" button 10 times","line":8,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":2511000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"10\"","line":9,"match":{"location":".\\test_driver\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":29000000}}]}]}] \ No newline at end of file diff --git a/example/test_driver/app.dart b/example/test_driver/app.dart deleted file mode 100644 index aa81d15..0000000 --- a/example/test_driver/app.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore: avoid_relative_lib_imports -import '../lib/main.dart' as app; -import 'package:flutter_driver/driver_extension.dart'; - -void main() { - // This line enables the extension - enableFlutterDriverExtension(); - - // Call the `main()` function of your app or call `runApp` with any widget you - // are interested in testing. - app.main(); -} diff --git a/example/test_driver/app_test.dart b/example/test_driver/app_test.dart deleted file mode 100644 index 98126f4..0000000 --- a/example/test_driver/app_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:async'; -import 'package:flutter_gherkin/flutter_gherkin.dart'; -import 'package:gherkin/gherkin.dart'; -import 'hooks/hook_example.dart'; -import 'steps/colour_parameter.dart'; -import 'steps/given_I_pick_a_colour_step.dart'; -import 'steps/tap_button_n_times_step.dart'; - -Future main() { - 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 - ] - ..customStepParameterDefinitions = [ - ColourParameter(), - ] - ..restartAppBetweenScenarios = true - ..targetAppWorkingDirectory = '../' - ..targetAppPath = 'test_driver/app.dart' - // ..buildFlavor = "staging" // uncomment when using build flavor and check android/ios flavor setup see android file android\app\build.gradle - // ..targetDeviceId = "all" // uncomment to run tests on all connected devices or set specific device target id - // ..tagExpression = '@smoke and not @ignore' // uncomment to see an example of running scenarios based on tag expressions - // ..logFlutterProcessOutput = true // uncomment to see command invoked to start the flutter test app - // ..verboseFlutterProcessLogs = true // uncomment to see the verbose output from the Flutter process - // ..flutterBuildTimeout = Duration(minutes: 3) // uncomment to change the default period that flutter is expected to build and start the app within - // ..runningAppProtocolEndpointUri = - // 'http://127.0.0.1:51540/bkegoer6eH8=/' // already running app observatory / service protocol uri (with enableFlutterDriverExtension method invoked) to test against if you use this set `restartAppBetweenScenarios` to false - ..exitAfterTestRun = true; // set to false if debugging to exit cleanly - - return GherkinRunner().execute(config); -} diff --git a/example/test_driver/features/app_restart.feature b/example/test_driver/features/app_restart.feature deleted file mode 100644 index b07e7e2..0000000 --- a/example/test_driver/features/app_restart.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Startup - - Scenario: counter should reset when app is restarted - Given I expect the "counter" to be "0" - When I tap the "increment" button - Then I expect the "counter" to be "1" - When I restart the app - Then I expect the "counter" to be "0" diff --git a/example/test_driver/features/back_navigation.feature b/example/test_driver/features/back_navigation.feature deleted file mode 100644 index 16305a6..0000000 --- a/example/test_driver/features/back_navigation.feature +++ /dev/null @@ -1,13 +0,0 @@ -Feature: Navigation - - @debug - Scenario: User can navigate back from page two - Given I expect the "counter" to be "0" - When I tap the "increment" button - Then I expect the "counter" to be "1" - - Given I tap the label that contains the text "Open page 2" - Then I expect the text "Contents of page 2" to be present - - Given I tap the back button - Then I expect the "counter" to be "1" \ No newline at end of file diff --git a/example/test_driver/features/counter.feature b/example/test_driver/features/counter.feature deleted file mode 100644 index 2912743..0000000 --- a/example/test_driver/features/counter.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: Startup - - Scenario: should increment counter - Given I expect the "counter" to be "0" - When I tap the "increment" button - And I tap the "increment" button - Then I expect the "counter" to be "2" - - Scenario: counter should reset when app is restarted - Given I expect the "counter" to be "0" - When I tap the "increment" button - Then I expect the "counter" to be "1" - When I restart the app - Then I expect the "counter" to be "0" diff --git a/example/test_driver/features/counter_increases.feature b/example/test_driver/features/counter_increases.feature deleted file mode 100644 index da3de58..0000000 --- a/example/test_driver/features/counter_increases.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Counter - The counter should be incremented when the button is pressed. - - Scenario: Counter increases when the button is pressed - Given I pick the colour red - Given I expect the "counter" to be "0" - When I tap the "increment" button 10 times - Then I expect the "counter" to be "10" \ No newline at end of file diff --git a/example/test_driver/features/counter_increases_french.feature b/example/test_driver/features/counter_increases_french.feature deleted file mode 100644 index fa2285a..0000000 --- a/example/test_driver/features/counter_increases_french.feature +++ /dev/null @@ -1,9 +0,0 @@ -# language: fr -Fonctionnalité: Counter - The counter should be incremented when the button is pressed. - - 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" - Quand I tap the "increment" button 10 times - Alors I expect the "counter" to be "10" \ No newline at end of file diff --git a/example/test_driver/features/counter_increases_scenerio_outline_example.feature b/example/test_driver/features/counter_increases_scenerio_outline_example.feature deleted file mode 100644 index f9f4834..0000000 --- a/example/test_driver/features/counter_increases_scenerio_outline_example.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Counter - The counter should be incremented when the button is pressed. - - @scenario_outline - Scenario Outline: Counter increases when the button is pressed - Given I pick the colour red - Given I expect the "counter" to be "0" - When I tap the "increment" button times - Then I expect the "counter" to be "" - - Examples: - | tap_amount | - | 1 | - | 2 | - | 5 | - | 10 | \ No newline at end of file diff --git a/example/test_driver/features/counter_increases_slowly.feature b/example/test_driver/features/counter_increases_slowly.feature deleted file mode 100644 index dd1872b..0000000 --- a/example/test_driver/features/counter_increases_slowly.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Delayed navigation - - Scenario: User can navigate to page two. Eventually - Given I expect the "counter" to be "0" - When I long press the "openPage2" button - Then I expect the widget "pageTwo" to be present within 15 seconds \ No newline at end of file diff --git a/example/test_driver/features/custom_parameter_example.feature b/example/test_driver/features/custom_parameter_example.feature deleted file mode 100644 index 6840166..0000000 --- a/example/test_driver/features/custom_parameter_example.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Custom Parameter Example - This test just logs the colour defined in the step - - Scenario: Custom colour parameter - Given I pick the colour red - Given I pick the colour green - Given I pick the colour blue \ No newline at end of file diff --git a/example/test_driver/features/drawer.feature b/example/test_driver/features/drawer.feature deleted file mode 100644 index dff575f..0000000 --- a/example/test_driver/features/drawer.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: Drawer - - Scenario: should open the drawer - Given I open the drawer - Given I close the drawer diff --git a/example/test_driver/features/long_press_widget.feature b/example/test_driver/features/long_press_widget.feature deleted file mode 100644 index e21914c..0000000 --- a/example/test_driver/features/long_press_widget.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Interaction - - Scenario: Widget can be long pressed - Given I expect the "longPressText" to be "Text that has not been long pressed" - When I long press the "longPressText" text - Then I expect the "longPressText" to be "Text has been long pressed!" diff --git a/example/test_driver/features/sub-features/counter_increases.feature b/example/test_driver/features/sub-features/counter_increases.feature deleted file mode 100644 index 0bf7d1b..0000000 --- a/example/test_driver/features/sub-features/counter_increases.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Counter - The counter should be incremented when the button is pressed. - - @perf - Scenario: Counter increases when the button is pressed - Given I expect the "counter" to be "0" - When I tap the "increment" button 20 times - Then I expect the "counter" to be "20" \ No newline at end of file diff --git a/example/test_driver/hooks/hook_example.dart b/example/test_driver/hooks/hook_example.dart deleted file mode 100644 index 253564c..0000000 --- a/example/test_driver/hooks/hook_example.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:gherkin/gherkin.dart'; - -class HookExample extends Hook { - /// The priority to assign to this hook. - /// Higher priority gets run first so a priority of 10 is run before a priority of 2 - @override - int get priority => 1; - - /// Run before any scenario in a test run have executed - @override - Future onBeforeRun(TestConfiguration config) async { - print('before run hook'); - } - - /// Run after all scenarios in a test run have completed - @override - Future onAfterRun(TestConfiguration config) async { - print('after run hook'); - } - - /// Run before a scenario and it steps are executed - @override - Future onBeforeScenario( - TestConfiguration config, - String scenario, - Iterable tags, - ) async { - print("running hook before scenario '$scenario'"); - } - - /// Run after a scenario has executed - @override - Future onAfterScenario( - TestConfiguration config, - String scenario, - Iterable tags, - ) async { - print("running hook after scenario '$scenario'"); - } -} diff --git a/example/test_driver/report.json b/example/test_driver/report.json deleted file mode 100644 index fe282cc..0000000 --- a/example/test_driver/report.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"","id":"custom parameter example","keyword":"Feature","line":1,"name":"Custom Parameter Example","uri":".\\features\\custom_parameter_example.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"custom parameter example;custom colour parameter","name":"Custom colour parameter","description":"","line":4,"steps":[{"keyword":"Given ","name":"I pick the colour red","line":5,"match":{"location":".\\features\\custom_parameter_example.feature:5"},"result":{"status":"passed","duration":14000000}},{"keyword":"Given ","name":"I pick the colour green","line":6,"match":{"location":".\\features\\custom_parameter_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I pick the colour blue","line":7,"match":{"location":".\\features\\custom_parameter_example.feature:7"},"result":{"status":"passed","duration":0}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\features\\counter_increases_scenerio_outline_example.feature","elements":[{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed examples: (1)","name":"Counter increases when the button is pressed Examples: (1)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":149000000}},{"keyword":"When ","name":"I tap the \"increment\" button 1 times","line":8,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":345000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":9,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":54000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed examples: (2)","name":"Counter increases when the button is pressed Examples: (2)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":47000000}},{"keyword":"When ","name":"I tap the \"increment\" button 2 times","line":8,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":570000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"2\"","line":9,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":28000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed examples: (3)","name":"Counter increases when the button is pressed Examples: (3)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":79000000}},{"keyword":"When ","name":"I tap the \"increment\" button 5 times","line":8,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":1307000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"5\"","line":9,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":33000000}}]},{"keyword":"Scenario Outline","type":"scenario","id":"counter;counter increases when the button is pressed examples: (4)","name":"Counter increases when the button is pressed Examples: (4)","description":"","line":5,"tags":[{"line":4,"name":"@scenario_outline"}],"steps":[{"keyword":"Given ","name":"I pick the colour red","line":6,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:7"},"result":{"status":"passed","duration":46000000}},{"keyword":"When ","name":"I tap the \"increment\" button 10 times","line":8,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:8"},"result":{"status":"passed","duration":2546000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"10\"","line":9,"match":{"location":".\\features\\counter_increases_scenerio_outline_example.feature:9"},"result":{"status":"passed","duration":30000000}}]}]},{"description":"","id":"startup","keyword":"Feature","line":1,"name":"Startup","uri":".\\features\\counter.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"startup;should increment counter","name":"should increment counter","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\features\\counter.feature:4"},"result":{"status":"passed","duration":51000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":5,"match":{"location":".\\features\\counter.feature:5"},"result":{"status":"passed","duration":337000000}},{"keyword":"And ","name":"I tap the \"increment\" button","line":6,"match":{"location":".\\features\\counter.feature:6"},"result":{"status":"passed","duration":274000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"2\"","line":7,"match":{"location":".\\features\\counter.feature:7"},"result":{"status":"passed","duration":30000000}}]},{"keyword":"Scenario","type":"scenario","id":"startup;counter should reset when app is restarted","name":"counter should reset when app is restarted","description":"","line":9,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":10,"match":{"location":".\\features\\counter.feature:10"},"result":{"status":"passed","duration":45000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":11,"match":{"location":".\\features\\counter.feature:11"},"result":{"status":"passed","duration":334000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":12,"match":{"location":".\\features\\counter.feature:12"},"result":{"status":"passed","duration":34000000}},{"keyword":"When ","name":"I restart the app","line":13,"match":{"location":".\\features\\counter.feature:13"},"result":{"status":"passed","duration":2591000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"0\"","line":14,"match":{"location":".\\features\\counter.feature:14"},"result":{"status":"passed","duration":44000000}}]}]},{"description":"","id":"delayed navigation","keyword":"Feature","line":1,"name":"Delayed navigation","uri":".\\features\\counter_increases_slowly.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"delayed navigation;user can navigate to page two. eventually","name":"User can navigate to page two. Eventually","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\features\\counter_increases_slowly.feature:4"},"result":{"status":"passed","duration":41000000}},{"keyword":"When ","name":"I long press the \"openPage2\" button","line":5,"match":{"location":".\\features\\counter_increases_slowly.feature:5"},"result":{"status":"passed","duration":790000000}},{"keyword":"Then ","name":"I expect the widget \"pageTwo\" to be present within 15 seconds","line":6,"match":{"location":".\\features\\counter_increases_slowly.feature:6"},"result":{"status":"passed","duration":12151000000}}]}]},{"description":"","id":"startup","keyword":"Feature","line":1,"name":"Startup","uri":".\\features\\app_restart.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"startup;counter should reset when app is restarted","name":"counter should reset when app is restarted","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\features\\app_restart.feature:4"},"result":{"status":"passed","duration":42000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":5,"match":{"location":".\\features\\app_restart.feature:5"},"result":{"status":"passed","duration":348000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":6,"match":{"location":".\\features\\app_restart.feature:6"},"result":{"status":"passed","duration":41000000}},{"keyword":"When ","name":"I restart the app","line":7,"match":{"location":".\\features\\app_restart.feature:7"},"result":{"status":"passed","duration":2671000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"0\"","line":8,"match":{"location":".\\features\\app_restart.feature:8"},"result":{"status":"passed","duration":44000000}}]}]},{"description":"","id":"navigation","keyword":"Feature","line":1,"name":"Navigation","uri":".\\features\\back_navigation.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"navigation;user can navigate back from page two","name":"User can navigate back from page two","description":"","line":4,"tags":[{"line":3,"name":"@debug"}],"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":5,"match":{"location":".\\features\\back_navigation.feature:5"},"result":{"status":"passed","duration":43000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":6,"match":{"location":".\\features\\back_navigation.feature:6"},"result":{"status":"passed","duration":326000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":7,"match":{"location":".\\features\\back_navigation.feature:7"},"result":{"status":"passed","duration":31000000}},{"keyword":"Given ","name":"I tap the label that contains the text \"Open page 2\"","line":9,"match":{"location":".\\features\\back_navigation.feature:9"},"result":{"status":"passed","duration":401000000}},{"keyword":"Then ","name":"I expect the text \"Contents of page 2\" to be present","line":10,"match":{"location":".\\features\\back_navigation.feature:10"},"result":{"status":"passed","duration":16000000}},{"keyword":"Given ","name":"I tap the back button","line":12,"match":{"location":".\\features\\back_navigation.feature:12"},"result":{"status":"passed","duration":387000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":13,"match":{"location":".\\features\\back_navigation.feature:13"},"result":{"status":"passed","duration":28000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\features\\counter_increases.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":4,"steps":[{"keyword":"Given ","name":"I pick the colour red","line":5,"match":{"location":".\\features\\counter_increases.feature:5"},"result":{"status":"passed","duration":0}},{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":6,"match":{"location":".\\features\\counter_increases.feature:6"},"result":{"status":"passed","duration":44000000}},{"keyword":"When ","name":"I tap the \"increment\" button 10 times","line":7,"match":{"location":".\\features\\counter_increases.feature:7"},"result":{"status":"passed","duration":2529000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"10\"","line":8,"match":{"location":".\\features\\counter_increases.feature:8"},"result":{"status":"passed","duration":30000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\features\\sub-features\\counter_increases.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":5,"tags":[{"line":4,"name":"@perf"}],"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":6,"match":{"location":".\\features\\sub-features\\counter_increases.feature:6"},"result":{"status":"passed","duration":42000000}},{"keyword":"When ","name":"I tap the \"increment\" button 20 times","line":7,"match":{"location":".\\features\\sub-features\\counter_increases.feature:7"},"result":{"status":"passed","duration":4977000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"20\"","line":8,"match":{"location":".\\features\\sub-features\\counter_increases.feature:8"},"result":{"status":"passed","duration":28000000}}]}]},{"description":"","id":"counter","keyword":"Feature","line":2,"name":"Counter","uri":".\\features\\counter_increases_french.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;counter increases when the button is pressed","name":"Counter increases when the button is pressed","description":"","line":5,"steps":[{"keyword":"Etant ","name":"donné que I pick the colour red","line":6,"match":{"location":".\\features\\counter_increases_french.feature:6"},"result":{"status":"passed","duration":0}},{"keyword":"Et ","name":"I expect the \"counter\" to be \"0\"","line":7,"match":{"location":".\\features\\counter_increases_french.feature:7"},"result":{"status":"passed","duration":45000000}},{"keyword":"Quand ","name":"I tap the \"increment\" button 10 times","line":8,"match":{"location":".\\features\\counter_increases_french.feature:8"},"result":{"status":"passed","duration":2537000000}},{"keyword":"Alors ","name":"I expect the \"counter\" to be \"10\"","line":9,"match":{"location":".\\features\\counter_increases_french.feature:9"},"result":{"status":"passed","duration":30000000}}]}]},{"description":"","id":"drawer","keyword":"Feature","line":1,"name":"Drawer","uri":".\\features\\drawer.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"drawer;should open the drawer","name":"should open the drawer","description":"","line":3,"steps":[{"keyword":"Given ","name":"I open the drawer","line":4,"match":{"location":".\\features\\drawer.feature:4"},"result":{"status":"passed","duration":1477000000}},{"keyword":"Given ","name":"I close the drawer","line":5,"match":{"location":".\\features\\drawer.feature:5"},"result":{"status":"passed","duration":441000000}}]}]},{"description":"","id":"interaction","keyword":"Feature","line":1,"name":"Interaction","uri":".\\features\\long_press_widget.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"interaction;widget can be long pressed","name":"Widget can be long pressed","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"longPressText\" to be \"Text that has not been long pressed\"","line":4,"match":{"location":".\\features\\long_press_widget.feature:4"},"result":{"status":"passed","duration":41000000}},{"keyword":"When ","name":"I long press the \"longPressText\" text","line":5,"match":{"location":".\\features\\long_press_widget.feature:5"},"result":{"status":"passed","duration":683000000}},{"keyword":"Then ","name":"I expect the \"longPressText\" to be \"Text has been long pressed!\"","line":6,"match":{"location":".\\features\\long_press_widget.feature:6"},"result":{"status":"passed","duration":44000000}}]}]}] \ No newline at end of file diff --git a/example/test_driver/steps/colour_parameter.dart b/example/test_driver/steps/colour_parameter.dart deleted file mode 100644 index 90b2e0a..0000000 --- a/example/test_driver/steps/colour_parameter.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:gherkin/gherkin.dart'; - -enum Colour { red, green, blue } - -class ColourParameter extends CustomParameter { - ColourParameter() - : super('colour', RegExp(r'(red|green|blue)', caseSensitive: true), (c) { - switch (c.toLowerCase()) { - case 'red': - return Colour.red; - case 'green': - return Colour.green; - case 'blue': - default: - return Colour.blue; - } - }); -} diff --git a/example/test_driver/steps/data_table_example_step.dart b/example/test_driver/steps/data_table_example_step.dart deleted file mode 100644 index 45440de..0000000 --- a/example/test_driver/steps/data_table_example_step.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:gherkin/gherkin.dart'; - -/// This step expects a data table -/// -/// For example: -/// -/// `Given I add the users` -/// | Firstname | Surname | Age | Gender | -/// | Woody | Johnson | 28 | Male | -/// | Edith | Summers | 23 | Female | -/// | Megan | Hill | 83 | Female | -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)); - } - }, - ); -} diff --git a/example/test_driver/steps/given_I_pick_a_colour_step.dart b/example/test_driver/steps/given_I_pick_a_colour_step.dart deleted file mode 100644 index f8e02e9..0000000 --- a/example/test_driver/steps/given_I_pick_a_colour_step.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:gherkin/gherkin.dart'; -import 'colour_parameter.dart'; - -StepDefinitionGeneric GivenIPickAColour() { - return given1( - 'I pick the colour {colour}', - (Colour colour, _) async { - print("The picked colour was: '$colour'"); - }, - ); -} diff --git a/example/test_driver/steps/multiline_string_example_step.dart b/example/test_driver/steps/multiline_string_example_step.dart deleted file mode 100644 index 542a4cd..0000000 --- a/example/test_driver/steps/multiline_string_example_step.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:gherkin/gherkin.dart'; - -/// This step expects a multi-line string proceeding it -/// -/// For example: -/// -/// `Given I provide the following "review" comment` -/// """ -/// Some comment -/// """ -StepDefinitionGeneric GivenMultiLineString() { - return given2( - 'I provide the following {string} comment', - (commentType, comment, _) async { - // implement step - }, - ); -} diff --git a/example/test_driver/steps/tap_button_n_times_step.dart b/example/test_driver/steps/tap_button_n_times_step.dart deleted file mode 100644 index 4fdda0f..0000000 --- a/example/test_driver/steps/tap_button_n_times_step.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:flutter_gherkin/flutter_gherkin.dart'; -import 'package:gherkin/gherkin.dart'; - -StepDefinitionGeneric TapButtonNTimesStep() { - return given2( - '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); - } - }, - ); -} diff --git a/example_with_flutter_driver/.gitignore b/example_with_flutter_driver/.gitignore new file mode 100644 index 0000000..9d532b1 --- /dev/null +++ b/example_with_flutter_driver/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/example/.metadata b/example_with_flutter_driver/.metadata similarity index 70% rename from example/.metadata rename to example_with_flutter_driver/.metadata index 39581c9..182ccca 100644 --- a/example/.metadata +++ b/example_with_flutter_driver/.metadata @@ -4,5 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: f37c235c32fc15babe6dc7b7bc2ee4387e5ecf92 - channel: beta + revision: 78910062997c3a836feee883712c241a5fd22983 + channel: stable + +project_type: app diff --git a/example_with_flutter_driver/README.md b/example_with_flutter_driver/README.md new file mode 100644 index 0000000..f6d5655 --- /dev/null +++ b/example_with_flutter_driver/README.md @@ -0,0 +1,16 @@ +# example_with_flutter_driver + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example_with_flutter_driver/android/.gitignore b/example_with_flutter_driver/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/example_with_flutter_driver/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example/android/app/build.gradle b/example_with_flutter_driver/android/app/build.gradle similarity index 68% rename from example/android/app/build.gradle rename to example_with_flutter_driver/android/app/build.gradle index fb1b2a9..b014181 100644 --- a/example/android/app/build.gradle +++ b/example_with_flutter_driver/android/app/build.gradle @@ -22,10 +22,15 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } lintOptions { disable 'InvalidPackage' @@ -33,12 +38,11 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.example" + applicationId "com.example.example_with_flutter_driver" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -48,19 +52,6 @@ android { signingConfig signingConfigs.debug } } - - /// uncomment to see example of using flavors - // flavorDimensions "env" - // productFlavors { - // staging { - // dimension "env" - // applicationIdSuffix ".dev" - // versionNameSuffix "-dev" - // } - // production { - // dimension "env" - // } - // } } flutter { @@ -68,7 +59,5 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example_with_flutter_driver/android/app/src/debug/AndroidManifest.xml b/example_with_flutter_driver/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..de0f838 --- /dev/null +++ b/example_with_flutter_driver/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example_with_flutter_driver/android/app/src/main/AndroidManifest.xml b/example_with_flutter_driver/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fd6acc8 --- /dev/null +++ b/example_with_flutter_driver/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + diff --git a/example_with_flutter_driver/android/app/src/main/kotlin/com/example/example_with_flutter_driver/MainActivity.kt b/example_with_flutter_driver/android/app/src/main/kotlin/com/example/example_with_flutter_driver/MainActivity.kt new file mode 100644 index 0000000..a8152d7 --- /dev/null +++ b/example_with_flutter_driver/android/app/src/main/kotlin/com/example/example_with_flutter_driver/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example_with_flutter_driver + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example_with_flutter_driver/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to example_with_flutter_driver/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example_with_flutter_driver/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to example_with_flutter_driver/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example_with_flutter_driver/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to example_with_flutter_driver/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example_with_flutter_driver/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to example_with_flutter_driver/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example_with_flutter_driver/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to example_with_flutter_driver/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example_with_flutter_driver/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to example_with_flutter_driver/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example_with_flutter_driver/android/app/src/main/res/values/styles.xml b/example_with_flutter_driver/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..1f83a33 --- /dev/null +++ b/example_with_flutter_driver/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example_with_flutter_driver/android/app/src/profile/AndroidManifest.xml b/example_with_flutter_driver/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..de0f838 --- /dev/null +++ b/example_with_flutter_driver/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example_with_flutter_driver/android/build.gradle similarity index 70% rename from example/android/build.gradle rename to example_with_flutter_driver/android/build.gradle index 6de3728..5e29d98 100644 --- a/example/android/build.gradle +++ b/example_with_flutter_driver/android/build.gradle @@ -1,11 +1,13 @@ buildscript { + ext.kotlin_version = '1.6.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle.properties b/example_with_flutter_driver/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to example_with_flutter_driver/android/gradle.properties index 38c8d45..a673820 100644 --- a/example/android/gradle.properties +++ b/example_with_flutter_driver/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true +android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example_with_flutter_driver/android/gradle/wrapper/gradle-wrapper.properties similarity index 70% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to example_with_flutter_driver/android/gradle/wrapper/gradle-wrapper.properties index 8c0b75f..fe015b5 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example_with_flutter_driver/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Dec 18 20:00:38 AEDT 2019 +#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +org.gradle.jvmargs=-Xmx4608m diff --git a/example_with_flutter_driver/android/settings.gradle b/example_with_flutter_driver/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example_with_flutter_driver/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example_with_flutter_driver/ios/.gitignore b/example_with_flutter_driver/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/example_with_flutter_driver/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example_with_flutter_driver/ios/Flutter/AppFrameworkInfo.plist similarity index 94% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to example_with_flutter_driver/ios/Flutter/AppFrameworkInfo.plist index 9367d48..6b4c0f7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example_with_flutter_driver/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier diff --git a/example/ios/Flutter/Debug.xcconfig b/example_with_flutter_driver/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to example_with_flutter_driver/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/example_with_flutter_driver/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to example_with_flutter_driver/ios/Flutter/Release.xcconfig diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example_with_flutter_driver/ios/Runner.xcodeproj/project.pbxproj similarity index 76% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to example_with_flutter_driver/ios/Runner.xcodeproj/project.pbxproj index bdbe25e..90dec47 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example_with_flutter_driver/ios/Runner.xcodeproj/project.pbxproj @@ -8,15 +8,8 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -29,8 +22,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,17 +31,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -62,8 +49,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -73,10 +58,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -90,7 +72,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, ); sourceTree = ""; }; @@ -105,27 +86,18 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -155,17 +127,18 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; - ORGANIZATIONNAME = "The Chromium Authors"; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -188,9 +161,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -210,7 +181,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -233,8 +204,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -261,9 +231,84 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithFlutterDriver; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -275,12 +320,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -307,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +364,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -329,12 +375,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -355,9 +403,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -368,6 +418,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -380,8 +431,11 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithFlutterDriver; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -391,6 +445,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -403,8 +458,10 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithFlutterDriver; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -417,6 +474,7 @@ buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -426,6 +484,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example_with_flutter_driver/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example_with_flutter_driver/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 96% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to example_with_flutter_driver/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac8..a28140c 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example_with_flutter_driver/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -67,7 +65,7 @@ + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_with_flutter_driver/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_with_flutter_driver/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example_with_flutter_driver/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example_with_flutter_driver/ios/Runner/AppDelegate.swift b/example_with_flutter_driver/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example_with_flutter_driver/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to example_with_flutter_driver/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example_with_flutter_driver/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to example_with_flutter_driver/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example_with_flutter_driver/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to example_with_flutter_driver/ios/Runner/Base.lproj/Main.storyboard diff --git a/example/ios/Runner/Info.plist b/example_with_flutter_driver/ios/Runner/Info.plist similarity index 94% rename from example/ios/Runner/Info.plist rename to example_with_flutter_driver/ios/Runner/Info.plist index 0513117..07fd842 100644 --- a/example/ios/Runner/Info.plist +++ b/example_with_flutter_driver/ios/Runner/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +11,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - example + example_with_flutter_driver CFBundlePackageType APPL CFBundleShortVersionString diff --git a/example_with_flutter_driver/ios/Runner/Runner-Bridging-Header.h b/example_with_flutter_driver/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example_with_flutter_driver/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example_with_flutter_driver/lib/main.dart b/example_with_flutter_driver/lib/main.dart new file mode 100644 index 0000000..a351836 --- /dev/null +++ b/example_with_flutter_driver/lib/main.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: MyHomePage('Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage(this.title) : super(); + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + key: const Key('counter'), + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + key: const Key('increment'), + child: Icon(Icons.add), + ), + ); + } +} diff --git a/example_with_flutter_driver/pubspec.lock b/example_with_flutter_driver/pubspec.lock new file mode 100644 index 0000000..44679c9 --- /dev/null +++ b/example_with_flutter_driver/pubspec.lock @@ -0,0 +1,344 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "31.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.11" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_gherkin: + dependency: "direct dev" + description: + path: ".." + relative: true + source: path + version: "3.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + gherkin: + dependency: transitive + description: + name: gherkin + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+1" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + integration_test: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.2.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.2.0" diff --git a/example_with_flutter_driver/pubspec.yaml b/example_with_flutter_driver/pubspec.yaml new file mode 100644 index 0000000..6106ae6 --- /dev/null +++ b/example_with_flutter_driver/pubspec.yaml @@ -0,0 +1,23 @@ +name: example_with_flutter_driver +description: A new Flutter project. + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: '>=2.17.0 <3.0.0' + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_gherkin: + path: ../ + +flutter: + uses-material-design: true diff --git a/example_with_flutter_driver/test_driver/app.dart b/example_with_flutter_driver/test_driver/app.dart new file mode 100644 index 0000000..b03fad7 --- /dev/null +++ b/example_with_flutter_driver/test_driver/app.dart @@ -0,0 +1,8 @@ +import 'package:example_with_flutter_driver/main.dart' as app; +import 'package:flutter_driver/driver_extension.dart'; + +void main() { + enableFlutterDriverExtension(); + + app.main(); +} diff --git a/example_with_flutter_driver/test_driver/features/counter.feature b/example_with_flutter_driver/test_driver/features/counter.feature new file mode 100644 index 0000000..14b8cdc --- /dev/null +++ b/example_with_flutter_driver/test_driver/features/counter.feature @@ -0,0 +1,6 @@ +Feature: Counter + + Scenario: User can increment the counter + Given I expect the "counter" to be "0" + When I tap the "increment" button + Then I expect the "counter" to be "1" \ No newline at end of file diff --git a/example_with_flutter_driver/test_driver/report.json b/example_with_flutter_driver/test_driver/report.json new file mode 100644 index 0000000..1c3441a --- /dev/null +++ b/example_with_flutter_driver/test_driver/report.json @@ -0,0 +1 @@ +[{"description":"","id":"counter","keyword":"Feature","line":1,"name":"Counter","uri":".\\features\\counter.feature","elements":[{"keyword":"Scenario","type":"scenario","id":"counter;user can increment the counter","name":"User can increment the counter","description":"","line":3,"steps":[{"keyword":"Given ","name":"I expect the \"counter\" to be \"0\"","line":4,"match":{"location":".\\features\\counter.feature:4"},"result":{"status":"passed","duration":66000000}},{"keyword":"When ","name":"I tap the \"increment\" button","line":5,"match":{"location":".\\features\\counter.feature:5"},"result":{"status":"passed","duration":347000000}},{"keyword":"Then ","name":"I expect the \"counter\" to be \"1\"","line":6,"match":{"location":".\\features\\counter.feature:6"},"result":{"status":"passed","duration":34000000}}]}]}] \ No newline at end of file diff --git a/example_with_flutter_driver/test_driver/test_harness.dart b/example_with_flutter_driver/test_driver/test_harness.dart new file mode 100644 index 0000000..0455808 --- /dev/null +++ b/example_with_flutter_driver/test_driver/test_harness.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart'; +import 'package:gherkin/gherkin.dart'; + +Future main() { + final config = FlutterDriverTestConfiguration( + features: [RegExp('features/**.feature')], + targetAppPath: 'test_driver/app.dart', + targetAppWorkingDirectory: '../', + buildFlavour: + "staging", // uncomment when using build flavor and check android/ios flavor setup see android file android\app\build.gradle + targetDeviceId: + "all", // uncomment to run tests on all connected devices or set specific device target id + tagExpression: + '@smoke and not @ignore', // uncomment to see an example of running scenarios based on tag expressions + logFlutterProcessOutput: + true, // uncomment to see command invoked to start the flutter test app + verboseFlutterProcessLogs: + true, // uncomment to see the verbose output from the Flutter process + flutterBuildTimeout: Duration( + minutes: + 3), // uncomment to change the default period that flutter is expected to build and start the app within + runningAppProtocolEndpointUri: + 'http://127.0.0.1:51540/bkegoer6eH8=/', // already running app observatory / service protocol uri (with enableFlutterDriverExtension method invoked) to test against if you use this set `restartAppBetweenScenarios` to false + ); + + return GherkinRunner().execute(config); +} diff --git a/example_with_integration_test/.gitignore b/example_with_integration_test/.gitignore new file mode 100644 index 0000000..9d532b1 --- /dev/null +++ b/example_with_integration_test/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/example_with_integration_test/.metadata b/example_with_integration_test/.metadata new file mode 100644 index 0000000..182ccca --- /dev/null +++ b/example_with_integration_test/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 78910062997c3a836feee883712c241a5fd22983 + channel: stable + +project_type: app diff --git a/example_with_integration_test/README.md b/example_with_integration_test/README.md new file mode 100644 index 0000000..97254b5 --- /dev/null +++ b/example_with_integration_test/README.md @@ -0,0 +1,11 @@ +``` +# generate the test suite +flutter pub run build_runner build --delete-conflicting-outputs + +# re-generate +flutter pub run build_runner clean +flutter pub run build_runner build --delete-conflicting-outputs + +# run the tests +flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart +``` diff --git a/example_with_integration_test/android/.gitignore b/example_with_integration_test/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/example_with_integration_test/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example_with_integration_test/android/app/build.gradle b/example_with_integration_test/android/app/build.gradle new file mode 100644 index 0000000..14f7512 --- /dev/null +++ b/example_with_integration_test/android/app/build.gradle @@ -0,0 +1,63 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example_with_integration_test" + minSdkVersion 23 + targetSdkVersion 31 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/example_with_integration_test/android/app/src/debug/AndroidManifest.xml b/example_with_integration_test/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..77cd5b3 --- /dev/null +++ b/example_with_integration_test/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example_with_integration_test/android/app/src/main/AndroidManifest.xml similarity index 52% rename from example/android/app/src/main/AndroidManifest.xml rename to example_with_integration_test/android/app/src/main/AndroidManifest.xml index 1b515f8..21dd7f8 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example_with_integration_test/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,39 @@ - - - - + package="com.example.example_with_integration_test"> - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + + diff --git a/example_with_integration_test/android/app/src/main/kotlin/com/example/example_with_integration_test/MainActivity.kt b/example_with_integration_test/android/app/src/main/kotlin/com/example/example_with_integration_test/MainActivity.kt new file mode 100644 index 0000000..d314eb0 --- /dev/null +++ b/example_with_integration_test/android/app/src/main/kotlin/com/example/example_with_integration_test/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example_with_integration_test + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example_with_integration_test/android/app/src/main/res/drawable/launch_background.xml b/example_with_integration_test/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example_with_integration_test/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example_with_integration_test/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example_with_integration_test/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example_with_integration_test/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example_with_integration_test/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example_with_integration_test/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example_with_integration_test/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example_with_integration_test/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example_with_integration_test/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example_with_integration_test/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example_with_integration_test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example_with_integration_test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example_with_integration_test/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example_with_integration_test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example_with_integration_test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example_with_integration_test/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example_with_integration_test/android/app/src/main/res/values/styles.xml b/example_with_integration_test/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..1f83a33 --- /dev/null +++ b/example_with_integration_test/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example_with_integration_test/android/app/src/profile/AndroidManifest.xml b/example_with_integration_test/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..77cd5b3 --- /dev/null +++ b/example_with_integration_test/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example_with_integration_test/android/build.gradle b/example_with_integration_test/android/build.gradle new file mode 100644 index 0000000..5e29d98 --- /dev/null +++ b/example_with_integration_test/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example_with_integration_test/android/gradle.properties b/example_with_integration_test/android/gradle.properties new file mode 100644 index 0000000..a673820 --- /dev/null +++ b/example_with_integration_test/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/example_with_integration_test/android/gradle/wrapper/gradle-wrapper.properties b/example_with_integration_test/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fe015b5 --- /dev/null +++ b/example_with_integration_test/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +org.gradle.jvmargs=-Xmx4608m diff --git a/example_with_integration_test/android/settings.gradle b/example_with_integration_test/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example_with_integration_test/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example_with_integration_test/build.yaml b/example_with_integration_test/build.yaml new file mode 100644 index 0000000..ae61e5f --- /dev/null +++ b/example_with_integration_test/build.yaml @@ -0,0 +1,8 @@ +targets: + $default: + sources: + - lib/** + - pubspec.* + - $package$ + # Allows the code generator to target files outside of the lib folder + - integration_test/**.dart \ No newline at end of file diff --git a/example_with_integration_test/generate-cucumber-html-report.js b/example_with_integration_test/generate-cucumber-html-report.js new file mode 100644 index 0000000..c1c3fcc --- /dev/null +++ b/example_with_integration_test/generate-cucumber-html-report.js @@ -0,0 +1,18 @@ +var fs = require("fs"); +var reporter = require('cucumber-html-reporter'); +const reportRootDir = 'integration_test/gherkin/reports/' +const jsonReportPath = `${reportRootDir}json_report.json`; +const htmlReportPath = `${reportRootDir}cucumber_report.html`; +const reportFile = fs.readFileSync(`${reportRootDir}integration_response_data.json`); +const jsonReport = JSON.parse(JSON.parse(reportFile).gherkin_reports)[0]; +fs.writeFileSync(jsonReportPath, JSON.stringify(jsonReport)); + +var options = { + theme: 'bootstrap', + jsonFile: jsonReportPath, + output: htmlReportPath, + reportSuiteAsScenarios: true, + launchReport: false, +}; + +reporter.generate(options); \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/check.feature b/example_with_integration_test/integration_test/features/check.feature new file mode 100644 index 0000000..ce71e4a --- /dev/null +++ b/example_with_integration_test/integration_test/features/check.feature @@ -0,0 +1,32 @@ +@tag +Feature: Checking data + @tag1 + Scenario: User can have data + Given I have item with data + """ + { + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } + } + """ \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/create.feature b/example_with_integration_test/integration_test/features/create.feature new file mode 100644 index 0000000..63c8a9f --- /dev/null +++ b/example_with_integration_test/integration_test/features/create.feature @@ -0,0 +1,53 @@ +@tag +Feature: Creating todos + + Scenario: User can create single todo item + Given I fill the "todo" field with "Buy spinach" + When I tap the "add" button + Then I expect the todo list + | Todo | + | Buy spinach | + When I take a screenshot called 'Johnson' + + Scenario: User can create multiple new todo items + Given I fill the "todo" field with "Buy carrots" + When I tap the "add" button + And I fill the "todo" field with "Buy hannah's apples" + When I tap the "add" button + And I fill the "todo" field with "Buy blueberries" + When I tap the "add" button + Then I expect the todo list + | Todo | + | Buy blueberries | + | Buy hannah's apples | + | Buy carrots | + Given I wait 5 seconds for the animation to complete + Given I have item with data + """ + { + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } + } + """ + # When I test the default step timeout is not applied to step with custom timeout \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/failure.feature b/example_with_integration_test/integration_test/features/failure.feature new file mode 100644 index 0000000..76f7858 --- /dev/null +++ b/example_with_integration_test/integration_test/features/failure.feature @@ -0,0 +1,13 @@ +Feature: Expect failures + Ensure that when a test fails the exception or test failure is reported + + @failure-expected + Scenario: Exception should be added to json report + When I tap the "button is not here but exception should be logged in report" button + + @failure-expected + Scenario: Failed expect() should be added to json report + Description for this scenario! + When I tap the "add" button + And I fill the "todo" field with "Buy hannah's apples" + Then I expect a failure \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/parsing.feature b/example_with_integration_test/integration_test/features/parsing.feature new file mode 100644 index 0000000..0b93e5c --- /dev/null +++ b/example_with_integration_test/integration_test/features/parsing.feature @@ -0,0 +1,9 @@ +@debug +Feature: Parsing + Complex description: + - Line "one". + - Line two, more text + - Line three + + Scenario: Parsing a + Given the text "^[A-Z]{3}\\d{5}\$" \ No newline at end of file diff --git a/example_with_integration_test/integration_test/features/swiping.feature b/example_with_integration_test/integration_test/features/swiping.feature new file mode 100644 index 0000000..207abd2 --- /dev/null +++ b/example_with_integration_test/integration_test/features/swiping.feature @@ -0,0 +1,9 @@ +@tag +Feature: Swiping + + Scenario: User can swipe cards left and right + Given I swipe right by 250 pixels on the "scrollable cards"` + Then I expect the text "Page 2" to be present + + Given I swipe left by 250 pixels on the "scrollable cards"` + Then I expect the text "Page 1" to be present \ No newline at end of file diff --git a/example_with_integration_test/integration_test/gherkin/configuration.dart b/example_with_integration_test/integration_test/gherkin/configuration.dart new file mode 100644 index 0000000..a1d71a2 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/configuration.dart @@ -0,0 +1,62 @@ +import 'package:example_with_integration_test/main.dart'; +import 'package:example_with_integration_test/services/external_application_manager.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:gherkin/gherkin.dart'; + +import 'hooks/reset_app_hook.dart'; +import 'steps/expect_failure.dart'; +import 'steps/expect_todos_step.dart'; +import 'steps/given_text.dart'; +import 'steps/multiline_string_with_formatted_json.dart'; +import 'steps/when_await_animation.dart'; +import 'steps/when_step_has_timeout.dart'; +import 'world/custom_world.dart'; + +FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration( + tagExpression: '@debug2', // can be used to limit the tests that are run + stepDefinitions: [ + thenIExpectTheTodos, + whenAnAnimationIsAwaited, + whenStepHasTimeout, + givenTheData, + givenTheText, + thenIExpectFailure, + ], + hooks: [ + ResetAppHook(), + AttachScreenshotOnFailedStepHook(), + // AttachScreenshotAfterStepHook(), + ], + reporters: [ + StdoutReporter(MessageLevel.error) + ..setWriteLineFn(print) + ..setWriteFn(print), + ProgressReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + TestRunSummaryReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + JsonReporter( + writeReport: (_, __) => Future.value(), + ), + ], + createWorld: (config) => Future.value(CustomWorld()), +); + +Future Function(World) appInitializationFn = (World world) async { + // ensure a new injector instance is created each time + final injector = Injector(DateTime.now().microsecondsSinceEpoch.toString()); + final externalApplicationManager = ExternalApplicationManager(injector); + (world as CustomWorld) + .setExternalApplicationManager(externalApplicationManager); + + runApp( + TodoApp( + injector: injector, + externalApplicationManager: externalApplicationManager, + ), + ); +}; diff --git a/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart b/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart new file mode 100644 index 0000000..ceed4f8 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +class AttachScreenshotAfterStepHook extends Hook { + @override + Future onAfterStep( + World world, + String step, + StepResult stepResult, + ) async { + try { + final screenshotData = await takeScreenshot(world); + world.attach(screenshotData, 'image/png', step); + } catch (e, st) { + world.attach('Failed to take screenshot\n$e\n$st', 'text/plain', step); + } + + return super.onAfterStep(world, step, stepResult); + } +} + +Future takeScreenshot(World world) async { + final bytes = await (world as FlutterWorld).appDriver.screenshot(); + + return base64Encode(bytes); +} diff --git a/example_with_integration_test/integration_test/gherkin/hooks/reset_app_hook.dart b/example_with_integration_test/integration_test/gherkin/hooks/reset_app_hook.dart new file mode 100644 index 0000000..b09f024 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/hooks/reset_app_hook.dart @@ -0,0 +1,20 @@ +import 'package:gherkin/gherkin.dart'; + +import '../world/custom_world.dart'; + +class ResetAppHook extends Hook { + @override + int get priority => 100; + + /// Resets the application state before the test is run to ensure no test side effects + @override + Future onAfterScenarioWorldCreated( + World world, + String scenario, + Iterable tags, + ) async { + if (world is CustomWorld) { + await world.externalApplicationManager.resetApplication(); + } + } +} diff --git a/example_with_integration_test/integration_test/gherkin/reports/package.json b/example_with_integration_test/integration_test/gherkin/reports/package.json new file mode 100644 index 0000000..0143abd --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/reports/package.json @@ -0,0 +1,36 @@ +{ + "name": "reports", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "cucumber-html-reporter": "^5.5.0" + }, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.2", + "color-convert": "^1.9.3", + "color-name": "^1.1.3", + "escape-string-regexp": "^1.0.5", + "find": "^0.3.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.2.10", + "has-flag": "^3.0.0", + "is-wsl": "^1.1.0", + "js-base64": "^2.6.4", + "jsonfile": "^5.0.0", + "lodash": "^4.17.21", + "node-emoji": "^1.11.0", + "open": "^6.4.0", + "supports-color": "^5.5.0", + "traverse-chain": "^0.1.0", + "universalify": "^0.1.2", + "uuid": "^3.4.0" + }, + "description": "" +} diff --git a/example_with_integration_test/integration_test/gherkin/reports/report.html b/example_with_integration_test/integration_test/gherkin/reports/report.html new file mode 100644 index 0000000..25a9fa6 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/reports/report.html @@ -0,0 +1,990 @@ + + + + Cucumber Feature Report + + + + + + + + +
+ +
Mon Jul 25 2022 16:30:33 GMT+1000 (Australian Eastern Standard Time)
+ +
+
+
+
+ + + + + + +
+ + +
+ +
+
+ +
+
+ +
An unnamed feature is possible if something is logged before any feature has started to execute
+ + + +
+ +
+
+
+ +
An unnamed scenario is possible if something is logged before any feature has started to execute +
+ + + +

+

+ + + + + + + + Unnamed + + + + < 1ms + + + + + + + + + + + + + + + + +
+

+ + +
+
+
+ + +
+ +
+
+
+ + + +

+

+ + + + + + + Given + I fill the "todo" field with "Buy carrots" + + + + 327ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + When + I tap the "add" button + + + + 584ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + And + I fill the "todo" field with "Buy hannah's apples" + + + + 507ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + When + I tap the "add" button + + + + 232ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + And + I fill the "todo" field with "Buy blueberries" + + + + 255ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + When + I tap the "add" button + + + + 229ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + Then + I expect the todo list + + + + 276ms + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Todo
+ Buy blueberries +
+ Buy hannah's apples +
+ Buy carrots +
+
+ +
+ + + + + + + + + + +
+

+ + + +

+

+ + + + + + + Given + I wait 5 seconds for the animation to complete + + + + 112ms + + + + + + + + + + + + + + + + +
+

+ + + +

+

+ + + + + + + Given + I have item with data + + + + < 1ms + + + + + + + + + + + + + + + + +
+

+ + +
+
+
+ +
+
+
+
+ +
+ +
+ + + +
+ + + + + + + + + + + diff --git a/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat b/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat new file mode 100644 index 0000000..8b84588 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat @@ -0,0 +1,5 @@ +call npm init -y + +call npm install --save-dev cucumber-html-reporter + +node -e "require('cucumber-html-reporter').generate({theme: 'bootstrap', jsonFile: '{REPORT_NAME}.json', output: 'report.html', reportSuiteAsScenarios: true, launchReport: false});" \ No newline at end of file diff --git a/example_with_integration_test/integration_test/gherkin/steps/expect_failure.dart b/example_with_integration_test/integration_test/gherkin/steps/expect_failure.dart new file mode 100644 index 0000000..0c49182 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/expect_failure.dart @@ -0,0 +1,10 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +final thenIExpectFailure = then( + 'I expect a failure', + (context) async { + expect([1, 2, 3], equals([1, 2])); + }, +); diff --git a/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart b/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart new file mode 100644 index 0000000..f914127 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/expect_todos_step.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +final thenIExpectTheTodos = then1( + 'I expect the todo list', + (table, context) async { + expect(context.configuration.timeout, isNotNull); + expect(context.configuration.timeout!.inSeconds, 5); + + await context.world.appDriver.waitForAppToSettle(); + + // get the parent list + final listTileFinder = context.world.appDriver.findBy( + ListTile, + FindType.type, + ); + + for (final row in table.rows) { + final todoText = row.columns.elementAt(0); + final listTileTextFinder = context.world.appDriver.findBy( + todoText, + FindType.text, + ); + // find the todo by the expected text + final finder = await context.world.appDriver + .findByDescendant(listTileFinder, listTileTextFinder); + + final text = await context.world.appDriver.getText(finder); + + context.expect(todoText, text); + } + }, + configuration: StepDefinitionConfiguration() + ..timeout = const Duration(seconds: 5), +); diff --git a/example_with_integration_test/integration_test/gherkin/steps/given_text.dart b/example_with_integration_test/integration_test/gherkin/steps/given_text.dart new file mode 100644 index 0000000..19a9271 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/given_text.dart @@ -0,0 +1,9 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +final givenTheText = given1( + 'the text {String}', + (text, world) async { + print(text); + }, +); diff --git a/example_with_integration_test/integration_test/gherkin/steps/multiline_string_with_formatted_json.dart b/example_with_integration_test/integration_test/gherkin/steps/multiline_string_with_formatted_json.dart new file mode 100644 index 0000000..c477124 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/multiline_string_with_formatted_json.dart @@ -0,0 +1,10 @@ +import 'package:gherkin/gherkin.dart'; + +final givenTheData = given1( + 'I have item with data', + (jsonString, context) async { + // print(jsonString); + }, + configuration: StepDefinitionConfiguration() + ..timeout = const Duration(seconds: 5), +); diff --git a/example_with_integration_test/integration_test/gherkin/steps/when_await_animation.dart b/example_with_integration_test/integration_test/gherkin/steps/when_await_animation.dart new file mode 100644 index 0000000..81c7ec7 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/when_await_animation.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Shows an example of using the `WidgetTester` from the `World` context rather than +/// using the implementation agnostic `appDriver` +final whenAnAnimationIsAwaited = when1( + 'I wait {int} seconds for the animation to complete', + (duration, context) async { + final tester = context.world.rawAppDriver; + + try { + await tester.pumpAndSettle( + const Duration(milliseconds: 100), + EnginePhase.sendSemanticsUpdate, + Duration(seconds: duration), + ); + // ignore: avoid_catching_errors + } on FlutterError { + // pump for 2 seconds and stop + await tester.pump(const Duration(seconds: 2)); + } + }, + configuration: StepDefinitionConfiguration() + ..timeout = const Duration(minutes: 5), +); diff --git a/example_with_integration_test/integration_test/gherkin/steps/when_step_has_timeout.dart b/example_with_integration_test/integration_test/gherkin/steps/when_step_has_timeout.dart new file mode 100644 index 0000000..616d659 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/steps/when_step_has_timeout.dart @@ -0,0 +1,13 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final whenStepHasTimeout = when( + 'I test the default step timeout is not applied to step with custom timeout', + (_) async { + // this should fail as the timeout of the test is 15 seconds but the below waits for 30 seconds + await Future.delayed(const Duration(seconds: 30)); + }, + configuration: StepDefinitionConfiguration() + ..timeout = const Duration(seconds: 15), +); diff --git a/example_with_integration_test/integration_test/gherkin/world/custom_world.dart b/example_with_integration_test/integration_test/gherkin/world/custom_world.dart new file mode 100644 index 0000000..21d46df --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/world/custom_world.dart @@ -0,0 +1,13 @@ +import 'package:example_with_integration_test/services/external_application_manager.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class CustomWorld extends FlutterWidgetTesterWorld { + ExternalApplicationManager? _externalApplicationManager; + + ExternalApplicationManager get externalApplicationManager => + _externalApplicationManager!; + + void setExternalApplicationManager(ExternalApplicationManager manager) { + _externalApplicationManager = manager; + } +} diff --git a/example_with_integration_test/integration_test/gherkin_suite_test.dart b/example_with_integration_test/integration_test/gherkin_suite_test.dart new file mode 100644 index 0000000..e3e9fb9 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin_suite_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +import 'gherkin/configuration.dart'; + +part 'gherkin_suite_test.g.dart'; + +@GherkinTestSuite( + useAbsolutePaths: false, +) +void main() { + executeTestSuite( + appMainFunction: appInitializationFn, + configuration: gherkinTestConfiguration, + // if you have lots of test you might need to increase the default timeout + // scenarioExecutionTimeout: Timeout(const Duration(minutes: 30)), + // if your app has lots of endless animations you might need to + // provide your own app lifecycle pump handler that doesn't pump + // at certain lifecycle stages + // appLifecyclePumpHandler: (appPhase, widgetTester) async => {}, + // you can increase the performance of your tests at the cost of + // not drawing some frames but it might lead to unexpected consequences + // framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive, + ); +} diff --git a/example_with_integration_test/integration_test/gherkin_suite_test.g.dart b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart new file mode 100644 index 0000000..2390853 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart @@ -0,0 +1,551 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'gherkin_suite_test.dart'; + +// ************************************************************************** +// GherkinSuiteTestGenerator +// ************************************************************************** + +class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { + _CustomGherkinIntegrationTestRunner({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + required Timeout scenarioExecutionTimeout, + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + LiveTestWidgetsFlutterBindingFramePolicy? framePolicy, + }) : super( + configuration: configuration, + appMainFunction: appMainFunction, + scenarioExecutionTimeout: scenarioExecutionTimeout, + appLifecyclePumpHandler: appLifecyclePumpHandler, + framePolicy: framePolicy, + ); + + @override + void onRun() { + testFeature0(); + testFeature1(); + testFeature2(); + testFeature3(); + testFeature4(); + } + + void testFeature0() { + runFeature( + name: 'Creating todos:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can create single todo item', + description: null, + path: '.\\integration_test\\features\\create.feature', + tags: ['@tag'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I fill the "todo" field with "Buy spinach"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I tap the "add" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the todo list', + multiLineStrings: [], + table: GherkinTable.fromJson('[{"Todo":"Buy spinach"}]'), + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I take a screenshot called \'Johnson\'', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Creating todos', + path: '.\\integration_test\\features\\create.feature', + description: null, + tags: ['@tag'], + ), + ); + + runScenario( + name: 'User can create multiple new todo items', + description: null, + path: '.\\integration_test\\features\\create.feature', + tags: ['@tag', '@debug2'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I fill the "todo" field with "Buy carrots"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I tap the "add" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'And I fill the "todo" field with "Buy hannah\'s apples"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I tap the "add" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'And I fill the "todo" field with "Buy blueberries"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I tap the "add" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the todo list', + multiLineStrings: [], + table: GherkinTable.fromJson( + '[{"Todo":"Buy blueberries"},{"Todo":"Buy hannah\'s apples"},{"Todo":"Buy carrots"}]'), + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I wait 5 seconds for the animation to complete', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I have item with data', + multiLineStrings: [ + """{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } +}""" + ], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onAfter: () async => onAfterRunFeature( + name: 'Creating todos', + path: '.\\integration_test\\features\\create.feature', + description: null, + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature1() { + runFeature( + name: 'Checking data:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can have data', + description: null, + path: '.\\integration_test\\features\\check.feature', + tags: ['@tag', '@tag1'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I have item with data', + multiLineStrings: [ + """{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } +}""" + ], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Checking data', + path: '.\\integration_test\\features\\check.feature', + description: null, + tags: ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Checking data', + path: '.\\integration_test\\features\\check.feature', + description: null, + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature2() { + runFeature( + name: 'Swiping:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can swipe cards left and right', + description: null, + path: '.\\integration_test\\features\\swiping.feature', + tags: ['@tag'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: + 'Given I swipe right by 250 pixels on the "scrollable cards"`', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the text "Page 2" to be present', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: + 'Given I swipe left by 250 pixels on the "scrollable cards"`', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the text "Page 1" to be present', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Swiping', + path: '.\\integration_test\\features\\swiping.feature', + description: null, + tags: ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Swiping', + path: '.\\integration_test\\features\\swiping.feature', + description: null, + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature3() { + runFeature( + name: 'Parsing:', + tags: ['@debug'], + run: () { + runScenario( + name: 'Parsing a', + description: null, + path: '.\\integration_test\\features\\parsing.feature', + tags: ['@debug'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given the text "^[A-Z]{3}\\\\d{5}\\\$"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Parsing', + path: '.\\integration_test\\features\\parsing.feature', + description: """Complex description: +- Line "one". +- Line two, more text +- Line three""", + tags: ['@debug'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Parsing', + path: '.\\integration_test\\features\\parsing.feature', + description: """Complex description: +- Line "one". +- Line two, more text +- Line three""", + tags: ['@debug'], + ), + ); + }, + ); + } + + void testFeature4() { + runFeature( + name: 'Expect failures:', + tags: [], + run: () { + runScenario( + name: 'Exception should be added to json report', + description: null, + path: '.\\integration_test\\features\\failure.feature', + tags: ['@failure-expected'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: + 'When I tap the "button is not here but exception should be logged in report" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Expect failures', + path: '.\\integration_test\\features\\failure.feature', + description: + """Ensure that when a test fails the exception or test failure is reported""", + tags: [], + ), + ); + + runScenario( + name: 'Failed expect() should be added to json report', + description: "Description for this scenario!", + path: '.\\integration_test\\features\\failure.feature', + tags: ['@failure-expected'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I tap the "add" button', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'And I fill the "todo" field with "Buy hannah\'s apples"', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect a failure', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onAfter: () async => onAfterRunFeature( + name: 'Expect failures', + path: '.\\integration_test\\features\\failure.feature', + description: + """Ensure that when a test fails the exception or test failure is reported""", + tags: [], + ), + ); + }, + ); + } +} + +void executeTestSuite({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + Timeout scenarioExecutionTimeout = const Timeout(Duration(minutes: 10)), + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + LiveTestWidgetsFlutterBindingFramePolicy? framePolicy, +}) { + _CustomGherkinIntegrationTestRunner( + configuration: configuration, + appMainFunction: appMainFunction, + appLifecyclePumpHandler: appLifecyclePumpHandler, + scenarioExecutionTimeout: scenarioExecutionTimeout, + framePolicy: framePolicy, + ).run(); +} diff --git a/example_with_integration_test/ios/.gitignore b/example_with_integration_test/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/example_with_integration_test/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example_with_integration_test/ios/Flutter/AppFrameworkInfo.plist b/example_with_integration_test/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/example_with_integration_test/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/example_with_integration_test/ios/Flutter/Debug.xcconfig b/example_with_integration_test/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example_with_integration_test/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example_with_integration_test/ios/Flutter/Release.xcconfig b/example_with_integration_test/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example_with_integration_test/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example_with_integration_test/ios/Runner.xcodeproj/project.pbxproj b/example_with_integration_test/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c95289c --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,495 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithIntegrationTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithIntegrationTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleWithIntegrationTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example_with_integration_test/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example_with_integration_test/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_with_integration_test/ios/Runner.xcworkspace/contents.xcworkspacedata b/example_with_integration_test/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example_with_integration_test/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example_with_integration_test/ios/Runner/AppDelegate.swift b/example_with_integration_test/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example_with_integration_test/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example_with_integration_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example_with_integration_test/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example_with_integration_test/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example_with_integration_test/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_with_integration_test/ios/Runner/Base.lproj/Main.storyboard b/example_with_integration_test/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example_with_integration_test/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_with_integration_test/ios/Runner/Info.plist b/example_with_integration_test/ios/Runner/Info.plist new file mode 100644 index 0000000..a80845a --- /dev/null +++ b/example_with_integration_test/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example_with_integration_test + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example_with_integration_test/ios/Runner/Runner-Bridging-Header.h b/example_with_integration_test/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example_with_integration_test/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example_with_integration_test/lib/blocs/todo_bloc.dart b/example_with_integration_test/lib/blocs/todo_bloc.dart new file mode 100644 index 0000000..91f5937 --- /dev/null +++ b/example_with_integration_test/lib/blocs/todo_bloc.dart @@ -0,0 +1,72 @@ +import 'package:example_with_integration_test/models/todo_status_enum.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:uuid/uuid.dart'; +import 'package:example_with_integration_test/models/todo_model.dart'; +import 'package:example_with_integration_test/repositories/todo_repository.dart'; + +class TodoBloc { + final Subject _dataRefresher = BehaviorSubject.seeded(null); + final Subject _newModel = ReplaySubject(maxSize: 1); + final TodoRepository _repository; + late final Stream> _todos; + + Stream> get todos => _todos; + Stream get newModel => _newModel; + + TodoBloc(this._repository) { + _newModel.add(_createNewModel()); + _todos = _dataRefresher + .switchMap((value) => _repository.all()) + .map( + (items) => items.toList( + growable: false, + )..sort( + (a, b) => b.created.compareTo(a.created), + ), + ) + .shareReplay(maxSize: 1); + } + + Stream add(Todo model) { + return _repository.add(model).doOnData( + (_) { + _newModel.add(_createNewModel()); + _updateTodoItems(); + }, + ); + } + + Stream remove(Todo model) { + return _repository.delete(model).doOnData( + (_) { + _updateTodoItems(); + }, + ); + } + + Stream update(Todo model) { + return _repository.update(model).doOnData( + (_) { + _updateTodoItems(); + }, + ); + } + + void dispose() { + _dataRefresher.close(); + _repository.dispose(); + } + + void _updateTodoItems() { + _dataRefresher.add(null); + } + + Todo _createNewModel() { + return Todo( + created: DateTime.now().toUtc(), + id: Uuid().v4(), + status: TodoStatus.pending, + updated: DateTime.now().toUtc(), + ); + } +} diff --git a/example_with_integration_test/lib/main.dart b/example_with_integration_test/lib/main.dart new file mode 100644 index 0000000..3358d54 --- /dev/null +++ b/example_with_integration_test/lib/main.dart @@ -0,0 +1,54 @@ +import 'package:example_with_integration_test/services/external_application_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; + +import 'module.dart'; +import 'widgets/views/home_view.dart'; + +void main() { + runApp( + TodoApp( + injector: Injector(), + ), + ); +} + +class TodoApp extends StatelessWidget { + final ExternalApplicationManager? externalApplicationManager; + final Injector injector; + + TodoApp({ + required this.injector, + this.externalApplicationManager, + }) : super() { + ModuleContainer().initialise(injector); + } + + @override + Widget build(BuildContext context) { + // if the app is in test mode and an ExternalApplicationManager is passed in + // let the app respond to external changes + return externalApplicationManager == null + ? MaterialApp( + title: 'Todo App', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: injector.get(), + ) + : StreamBuilder( + stream: externalApplicationManager!.applicationReset, + builder: (ctx, _) { + return MaterialApp( + title: 'Todo App', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: injector.get(), + ); + }, + ); + } +} diff --git a/example_with_integration_test/lib/models/todo_model.dart b/example_with_integration_test/lib/models/todo_model.dart new file mode 100644 index 0000000..19a995a --- /dev/null +++ b/example_with_integration_test/lib/models/todo_model.dart @@ -0,0 +1,24 @@ +import 'package:example_with_integration_test/models/todo_status_enum.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'todo_model.g.dart'; + +@JsonSerializable() +class Todo { + final String id; + final DateTime created; + final DateTime updated; + String? action; + TodoStatus status; + + Todo({ + required this.id, + required this.created, + required this.updated, + required this.status, + this.action, + }); + + factory Todo.fromJson(Map json) => _$TodoFromJson(json); + Map toJson() => _$TodoToJson(this); +} diff --git a/example_with_integration_test/lib/models/todo_model.g.dart b/example_with_integration_test/lib/models/todo_model.g.dart new file mode 100644 index 0000000..177ddf3 --- /dev/null +++ b/example_with_integration_test/lib/models/todo_model.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'todo_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Todo _$TodoFromJson(Map json) => Todo( + id: json['id'] as String, + created: DateTime.parse(json['created'] as String), + updated: DateTime.parse(json['updated'] as String), + status: $enumDecode(_$TodoStatusEnumMap, json['status']), + action: json['action'] as String?, + ); + +Map _$TodoToJson(Todo instance) => { + 'id': instance.id, + 'created': instance.created.toIso8601String(), + 'updated': instance.updated.toIso8601String(), + 'action': instance.action, + 'status': _$TodoStatusEnumMap[instance.status], + }; + +const _$TodoStatusEnumMap = { + TodoStatus.pending: 'pending', + TodoStatus.complete: 'complete', +}; diff --git a/example_with_integration_test/lib/models/todo_status_enum.dart b/example_with_integration_test/lib/models/todo_status_enum.dart new file mode 100644 index 0000000..f08faf1 --- /dev/null +++ b/example_with_integration_test/lib/models/todo_status_enum.dart @@ -0,0 +1,4 @@ +enum TodoStatus { + pending, + complete, +} diff --git a/example_with_integration_test/lib/module.dart b/example_with_integration_test/lib/module.dart new file mode 100644 index 0000000..d2ddb1b --- /dev/null +++ b/example_with_integration_test/lib/module.dart @@ -0,0 +1,27 @@ +import 'package:example_with_integration_test/widgets/views/home_view.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; + +import 'blocs/todo_bloc.dart'; +import 'repositories/todo_repository.dart'; + +class ModuleContainer { + Injector initialise(Injector injector) { + injector.map( + (i) => TodoRepository(), + isSingleton: true, + ); + + injector.map( + (i) => TodoBloc(i.get()), + ); + + // Views + injector.map( + (i) => HomeView( + blocFactory: () => i.get(), + ), + ); + + return injector; + } +} diff --git a/example_with_integration_test/lib/repositories/todo_repository.dart b/example_with_integration_test/lib/repositories/todo_repository.dart new file mode 100644 index 0000000..f78e60c --- /dev/null +++ b/example_with_integration_test/lib/repositories/todo_repository.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:example_with_integration_test/models/todo_model.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// +/// This is a very naive and inefficient repository please do not copy this! +/// +class TodoRepository { + static const STORAGE_ACCESSOR_KEY = 'TODO'; + + final Subject _storage = + ReplaySubject(maxSize: 1); + + TodoRepository() { + StreamSubscription? sub; + sub = SharedPreferences.getInstance().asStream().listen( + (sp) { + _storage.add(sp); + sub?.cancel(); + }, + ); + } + + Stream> all() { + return _storage + .map((accessor) => accessor.getStringList(STORAGE_ACCESSOR_KEY)) + .map( + (items) => (items ?? []) + .map( + (data) => Todo.fromJson( + jsonDecode(data), + ), + ) + .toList( + growable: false, + ), + ); + } + + Stream add(Todo item) { + return all().switchMap( + (items) { + final itemsAsJson = (items.toList()..add(item)) + .map((x) => jsonEncode(x.toJson())) + .toList( + growable: false, + ); + + return _storage.switchMap( + (accessor) => accessor + .setStringList(STORAGE_ACCESSOR_KEY, itemsAsJson) + .asStream(), + ); + }, + ); + } + + Stream update(Todo item) { + return all().switchMap( + (items) { + final itemsAsJson = (items.toList() + ..removeWhere((x) => x.id == item.id) + ..add(item)) + .map((x) => jsonEncode(x.toJson())) + .toList( + growable: false, + ); + + return _storage.switchMap( + (accessor) => accessor + .setStringList(STORAGE_ACCESSOR_KEY, itemsAsJson) + .asStream(), + ); + }, + ); + } + + Stream delete(Todo item) { + return all().switchMap( + (items) { + final itemsAsJson = (items.toList() + ..removeWhere((x) => x.id == item.id)) + .map((x) => jsonEncode(x.toJson())) + .toList( + growable: false, + ); + + return _storage.switchMap( + (accessor) => accessor + .setStringList(STORAGE_ACCESSOR_KEY, itemsAsJson) + .asStream(), + ); + }, + ); + } + + Stream purge() { + return _storage.switchMap( + (accessor) => accessor.remove(STORAGE_ACCESSOR_KEY).asStream(), + ); + } + + void dispose() { + _storage.close(); + } +} diff --git a/example_with_integration_test/lib/services/external_application_manager.dart b/example_with_integration_test/lib/services/external_application_manager.dart new file mode 100644 index 0000000..966d0af --- /dev/null +++ b/example_with_integration_test/lib/services/external_application_manager.dart @@ -0,0 +1,23 @@ +import 'package:example_with_integration_test/repositories/todo_repository.dart'; +import 'package:flutter_simple_dependency_injection/injector.dart'; +import 'package:rxdart/rxdart.dart'; + +/// Class that is able to manager the application and perform tasks +/// such as resetting the app data etc +class ExternalApplicationManager { + final Injector _injector; + // final Subject _applicationReset = BehaviorSubject.seeded(null); + final Subject _applicationReset = ReplaySubject(maxSize: 1); + + Stream get applicationReset => _applicationReset; + + ExternalApplicationManager(this._injector); + + /// Resets the application to a newly installed state by removing all data + Future resetApplication() async { + await _injector.get().purge().first; + _applicationReset.add(null); + + return Future.value(); + } +} diff --git a/example_with_integration_test/lib/widgets/components/add_todo_component.dart b/example_with_integration_test/lib/widgets/components/add_todo_component.dart new file mode 100644 index 0000000..62893ce --- /dev/null +++ b/example_with_integration_test/lib/widgets/components/add_todo_component.dart @@ -0,0 +1,96 @@ +import 'package:example_with_integration_test/models/todo_model.dart'; +import 'package:example_with_integration_test/widgets/view_utils_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; + +class AddTodoComponent extends StatefulWidget { + final void Function(Todo) onAdded; + final Stream todo; + + const AddTodoComponent({ + required this.todo, + required this.onAdded, + }) : super(); + + @override + _AddTodoComponentState createState() => _AddTodoComponentState(); +} + +class _AddTodoComponentState extends State + with ViewUtilsMixin { + final TextEditingController _textEditingController = TextEditingController(); + final Subject disposed$ = PublishSubject(); + + @override + void initState() { + super.initState(); + widget.todo.takeUntil(disposed$).listen( + (model) { + setState(() { + _textEditingController.text = model.action ?? ''; + }); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Form( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 16, + ), + child: SizedBox( + width: 200, + child: TextFormField( + controller: _textEditingController, + style: Theme.of(context).textTheme.subtitle1, + key: const Key('todo'), + decoration: InputDecoration( + labelText: 'Add todo item... ', + ), + validator: (text) => text == null || text.isEmpty + ? 'You must add a todo item' + : null, + ), + ), + ), + FloatingActionButton( + key: const Key('add'), + onPressed: onAdd, + backgroundColor: Theme.of(context).primaryColor, + focusElevation: 3, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + child: const Icon(Icons.add), + ), + ], + ), + ), + ); + } + + void onAdd() { + subscribeOnce( + widget.todo, + onData: (todo) { + todo.action = _textEditingController.text; + + widget.onAdded(todo); + }, + ); + } + + @override + void dispose() { + super.dispose(); + disposed$.add(null); + _textEditingController.dispose(); + disposed$.close(); + } +} diff --git a/example_with_integration_test/lib/widgets/view_utils_mixin.dart b/example_with_integration_test/lib/widgets/view_utils_mixin.dart new file mode 100644 index 0000000..e1437e1 --- /dev/null +++ b/example_with_integration_test/lib/widgets/view_utils_mixin.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class ViewUtilsMixin { + @protected + StreamSubscription subscribeOnce( + Stream stream, { + void Function(T data)? onData, + void Function(Object, StackTrace)? onError, + void Function()? onDone, + }) { + StreamSubscription? sub; + var hasErrored = false; + return sub = stream.listen( + (data) { + sub?.cancel(); + if (!hasErrored && onData != null) { + onData(data); + } + }, + onError: (Object er, StackTrace st) { + hasErrored = true; + if (onError != null) { + onError(er, st); + } + }, + onDone: onDone, + cancelOnError: true, + ); + } +} diff --git a/example_with_integration_test/lib/widgets/views/home_view.dart b/example_with_integration_test/lib/widgets/views/home_view.dart new file mode 100644 index 0000000..ce05cd6 --- /dev/null +++ b/example_with_integration_test/lib/widgets/views/home_view.dart @@ -0,0 +1,181 @@ +import 'package:example_with_integration_test/blocs/todo_bloc.dart'; +import 'package:example_with_integration_test/models/todo_model.dart'; +import 'package:example_with_integration_test/models/todo_status_enum.dart'; +import 'package:example_with_integration_test/widgets/components/add_todo_component.dart'; +import 'package:flutter/material.dart'; + +import '../view_utils_mixin.dart'; + +class HomeView extends StatefulWidget { + final TodoBloc Function() blocFactory; + + const HomeView({ + required this.blocFactory, + Key? key, + }) : super(key: key); + + @override + _HomeViewState createState() => _HomeViewState(blocFactory()); +} + +class _HomeViewState extends State with ViewUtilsMixin { + final TodoBloc bloc; + + _HomeViewState(this.bloc); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Todo List', + ), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 100, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: AddTodoComponent( + todo: bloc.newModel, + onAdded: (todo) { + subscribeOnce(bloc.add(todo)); + }, + ), + ), + ), + SizedBox( + height: 16, + ), + StreamBuilder>( + stream: bloc.todos, + builder: (_, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + if (data.isEmpty) { + return const Icon( + Icons.list, + size: 64, + color: Colors.black26, + ); + } else { + return ListView.builder( + shrinkWrap: true, + itemCount: data.length, + itemBuilder: (ctx, index) { + final todo = data.elementAt(index); + return Dismissible( + background: Container( + color: Colors.red, + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Icon( + Icons.delete, + color: Colors.white, + ), + Icon( + Icons.delete, + color: Colors.white, + ), + ], + ), + ), + key: Key(todo.action!), + onDismissed: (direction) { + subscribeOnce( + bloc.remove(todo), + onDone: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Todo deleted'), + ), + ); + }, + ); + }, + child: ListTile( + title: Text( + todo.action!, + style: Theme.of(context) + .textTheme + .bodyText1! + .copyWith( + decoration: + todo.status == TodoStatus.complete + ? TextDecoration.lineThrough + : null, + ), + ), + ), + ); + }, + ); + } + } else { + return Center( + child: Text('Loading...'), + ); + } + }, + ), + Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + key: const Key('scrollable cards'), + width: 300, + height: 250, + child: PageView.builder( + itemCount: 3, + itemBuilder: (ctx, index) { + return Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: index == 0 + ? Colors.amber + : Colors.blueAccent, + borderRadius: BorderRadius.circular(10), + ), + child: SizedBox( + key: Key('Page ${index + 1}'), + width: 200, + height: 200, + child: Center( + child: Text( + 'Page ${index + 1}', + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + super.dispose(); + bloc.dispose(); + } +} diff --git a/example_with_integration_test/package.json b/example_with_integration_test/package.json new file mode 100644 index 0000000..eab66b3 --- /dev/null +++ b/example_with_integration_test/package.json @@ -0,0 +1,11 @@ +{ + "name": "example_with_integration_test", + "version": "1.0.0", + "main": "generate-cucumber-html-report.js", + "scripts": { + "generate-report": "" + }, + "devDependencies": { + "cucumber-html-reporter": "^5.5.0" + } +} diff --git a/example_with_integration_test/pubspec.lock b/example_with_integration_test/pubspec.lock new file mode 100644 index 0000000..08a0802 --- /dev/null +++ b/example_with_integration_test/pubspec.lock @@ -0,0 +1,643 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "30.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.11" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.2" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.2" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_gherkin: + dependency: "direct dev" + description: + path: ".." + relative: true + source: path + version: "3.0.0-rc.17" + flutter_simple_dependency_injection: + dependency: "direct main" + description: + name: flutter_simple_dependency_injection + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + gherkin: + dependency: transitive + description: + name: gherkin + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + rxdart: + dependency: "direct main" + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.2.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.10" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/example_with_integration_test/pubspec.yaml b/example_with_integration_test/pubspec.yaml new file mode 100644 index 0000000..9ee14fe --- /dev/null +++ b/example_with_integration_test/pubspec.yaml @@ -0,0 +1,33 @@ +name: example_with_integration_test +description: A new Flutter project. + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: '>=2.17.0 <3.0.0' + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + json_serializable: ^6.0.1 + json_annotation: ^4.3.0 + rxdart: ^0.27.2 + shared_preferences: ^2.0.8 + uuid: ^3.0.5 + flutter_simple_dependency_injection: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + build_runner: + flutter_gherkin: + path: ../ + +flutter: + + uses-material-design: true \ No newline at end of file diff --git a/example_with_integration_test/test_driver/integration_test_driver.dart b/example_with_integration_test/test_driver/integration_test_driver.dart new file mode 100644 index 0000000..9981375 --- /dev/null +++ b/example_with_integration_test/test_driver/integration_test_driver.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/common.dart'; +import 'package:integration_test/integration_test_driver.dart' + as integration_test_driver; + +DriverLogCallback logDriverMessages = (String source, String message) { + final msg = '$source: $message'; + if (message.toLowerCase().contains('error')) { + stderr.writeln(msg); + } else { + stdout.writeln(msg); + } +}; + +Future main() { + // Flutter Driver logs all messages to stderr by default so if this is run on a build server + // the process will fail due to writing errors. So handle this yourself for now + driverLog = logDriverMessages; + // The Gherkin report data send back to this runner by the app after + // the tests have run will be saved to this directory + integration_test_driver.testOutputsDirectory = + 'integration_test/gherkin/reports'; + + return integrationDriver(); +} + +// Rre-implement this rather than using `integration_test_driver.integrationDriver()` +// so that failed test runs will have reports saved to disk rather than just exiting +Future integrationDriver({ + Duration timeout = const Duration(minutes: 60), +}) async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String jsonResult = await driver.requestData(null, timeout: timeout); + final Response response = Response.fromJson(jsonResult); + + await driver.close(); + + final reports = json.decode(response.data!['gherkin_reports'].toString()) + as List; + + await writeGherkinReports(reports); + + if (response.allTestsPassed) { + exit(0); + } else { + print('Failure Details:\n${response.formattedFailureDetails}'); + exit(1); + } +} + +Future writeGherkinReports(List reports) async { + final filenamePrefix = + DateTime.now().toIso8601String().split('.').first.replaceAll(':', '-'); + + for (var i = 0; i < reports.length; i += 1) { + final reportData = reports.elementAt(i) as List; + + await fs + .directory(integration_test_driver.testOutputsDirectory) + .create(recursive: true); + File file = File( + '${integration_test_driver.testOutputsDirectory}/' + '$filenamePrefix' + '-v${i + 1}.json', + ); + + await file.writeAsString(json.encode(reportData)); + } +} diff --git a/lib/flutter_gherkin.dart b/lib/flutter_gherkin.dart index bb4f22e..ecd6cdd 100644 --- a/lib/flutter_gherkin.dart +++ b/lib/flutter_gherkin.dart @@ -1,10 +1,15 @@ library flutter_gherkin; +/// *************************************** +/// Library export for use with integration_test with include reference to flutter_test which reference dart:ui +/// which are not allowed when running tests with flutter_driver hence this separate library declaration file +/// *************************************** + // Flutter specific implementations -export 'src/flutter/build_mode.dart'; -export 'src/flutter/flutter_world.dart'; -export 'src/flutter/flutter_test_configuration.dart'; -export 'src/flutter/utils/driver_utils.dart'; +export 'src/flutter/configuration/build_mode.dart'; +export 'src/flutter/world/flutter_world.dart'; +export 'src/flutter/configuration/flutter_test_configuration.dart'; +export 'src/flutter/adapters/app_driver_adapter.dart'; // Well known steps export 'src/flutter/steps/given_i_open_the_drawer_step.dart'; @@ -24,9 +29,14 @@ export 'src/flutter/steps/text_exists_within_step.dart'; export 'src/flutter/steps/wait_until_key_exists_step.dart'; export 'src/flutter/steps/when_tap_the_back_button_step.dart'; export 'src/flutter/steps/wait_until_type_exists_step.dart'; +export 'src/flutter/steps/wait_until_key_exists_step.dart'; +export 'src/flutter/steps/take_a_screenshot_step.dart'; // Hooks export 'src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart'; -// Reporters -export 'src/flutter/reporters/flutter_driver_reporter.dart'; +// integration_test specific exports +export 'src/flutter/adapters/widget_tester_app_driver_adapter.dart'; +export 'src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart'; +export 'src/flutter/runners/gherkin_integration_test_runner.dart'; +export 'src/flutter/world/flutter_widget_tester_world.dart'; diff --git a/lib/flutter_gherkin_with_driver.dart b/lib/flutter_gherkin_with_driver.dart new file mode 100644 index 0000000..f7fc51c --- /dev/null +++ b/lib/flutter_gherkin_with_driver.dart @@ -0,0 +1,35 @@ +library flutter_gherkin; + +// Flutter specific implementations +export 'src/flutter/configuration/build_mode.dart'; +export 'src/flutter/world/flutter_world.dart'; +export 'src/flutter/configuration/flutter_test_configuration.dart'; +export 'src/flutter/adapters/app_driver_adapter.dart'; + +// Well known steps +export 'src/flutter/steps/given_i_open_the_drawer_step.dart'; +export 'src/flutter/steps/then_expect_element_to_have_value_step.dart'; +export 'src/flutter/steps/when_fill_field_step.dart'; +export 'src/flutter/steps/when_pause_step.dart'; +export 'src/flutter/steps/when_tap_widget_step.dart'; +export 'src/flutter/steps/restart_app_step.dart'; +export 'src/flutter/steps/sibling_contains_text_step.dart'; +export 'src/flutter/steps/swipe_step.dart'; +export 'src/flutter/steps/tap_text_within_widget_step.dart'; +export 'src/flutter/steps/tap_widget_of_type_step.dart'; +export 'src/flutter/steps/tap_widget_of_type_within_step.dart'; +export 'src/flutter/steps/tap_widget_with_text_step.dart'; +export 'src/flutter/steps/text_exists_step.dart'; +export 'src/flutter/steps/text_exists_within_step.dart'; +export 'src/flutter/steps/wait_until_key_exists_step.dart'; +export 'src/flutter/steps/when_tap_the_back_button_step.dart'; +export 'src/flutter/steps/wait_until_type_exists_step.dart'; + +// Hooks +export 'src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart'; + +// Flutter driver specific implementations +export 'src/flutter/configuration/flutter_driver_test_configuration.dart'; +export 'src/flutter/adapters/flutter_driver_app_driver_adapter.dart'; +export 'src/flutter/reporters/flutter_driver_reporter.dart'; +export 'src/flutter/world/flutter_driver_world.dart'; diff --git a/lib/src/flutter/adapters/app_driver_adapter.dart b/lib/src/flutter/adapters/app_driver_adapter.dart new file mode 100644 index 0000000..082eb66 --- /dev/null +++ b/lib/src/flutter/adapters/app_driver_adapter.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +enum FindType { + key, + text, + tooltip, + type, +} + +enum ExpectedWidgetResultType { + first, + last, + list, +} + +abstract class AppDriverAdapter { + TNativeAdapter _driver; + + AppDriverAdapter(this._driver); + + TNativeAdapter get nativeDriver => _driver; + + void setNativeDriver(TNativeAdapter driver) { + _driver = driver; + } + + /// Returns the correct finder type instance + /// `data` can be `String`, `Key` or a `Type` + /// `findType` denotes the type of finder returned + TFinderType findBy( + dynamic data, + FindType findType, + ); + + TFinderType findByAncestor( + TFinderType of, + TFinderType matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }); + + /// Finds widgets that are descendants of the [of] parameter and that match the [matching] parameter. + TFinderType findByDescendant( + TFinderType of, + TFinderType matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }); + + Future waitForAppToSettle({ + Duration duration = const Duration(milliseconds: 100), + Duration timeout = const Duration(seconds: 30), + }); + + Future widget( + TFinderType finder, [ + ExpectedWidgetResultType expectResultType = ExpectedWidgetResultType.first, + ]); + + Future> screenshot({ + String? screenshotName, + }); + + Future isPresent( + TFinderType finder, { + Duration? timeout = const Duration(seconds: 1), + }); + + Future isAbsent( + TFinderType finder, { + Duration? timeout = const Duration(seconds: 1), + }); + + Future enterText( + TFinderType finder, + String text, { + Duration? timeout = const Duration(seconds: 30), + }); + + Future getText( + TFinderType finder, { + Duration? timeout = const Duration(seconds: 30), + }); + + Future tap( + TFinderType finder, { + Duration? timeout = const Duration(seconds: 30), + }); + + Future longPress( + TFinderType finder, { + Duration pressDuration = const Duration(milliseconds: 500), + Duration? timeout = const Duration(seconds: 30), + }); + + Future pageBack(); + + /// Scrolls a descendant scrollable widget by the give parameters + Future scroll( + TFinderType finder, { + double dx, + double dy, + Duration? duration = const Duration(milliseconds: 200), + Duration? timeout = const Duration(seconds: 30), + }); + + /// Repeatedly scrolls a [Scrollable] by delta until finder is visible. + /// Between each scroll, wait for duration time for settling. + /// If scrollable is null, this will find a [Scrollable]. + Future scrollUntilVisible( + TFinderType item, { + TFinderType scrollable, + double dx, + double dy, + Duration? timeout = const Duration(seconds: 30), + }); + + Future scrollIntoView( + TFinderType finder, { + Duration? timeout = const Duration(seconds: 30), + }); + + /// Will wait until the give condition returns `true` polling every `pollInterval`. If `condition` has not returned true + /// within the given `timeout` this will cause the returned future to complete with a [TimeoutException]. + Future waitUntil( + Future Function() condition, { + Duration? timeout = const Duration(seconds: 10), + Duration? pollInterval = const Duration(milliseconds: 500), + }) async { + return Future.microtask( + () async { + final completer = Completer(); + var maxAttempts = + (timeout!.inMilliseconds / pollInterval!.inMilliseconds).round(); + var attempts = 0; + + while (attempts < maxAttempts) { + final result = await condition(); + if (result) { + completer.complete(); + break; + } else { + await Future.delayed(pollInterval); + } + } + }, + ).timeout( + timeout!, + ); + } + + void dispose() {} +} diff --git a/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart b/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart new file mode 100644 index 0000000..fb423ed --- /dev/null +++ b/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart @@ -0,0 +1,236 @@ +import 'dart:async'; + +import 'package:flutter_driver/flutter_driver.dart'; + +import 'app_driver_adapter.dart'; + +class FlutterDriverAppDriverAdapter + extends AppDriverAdapter { + FlutterDriverAppDriverAdapter(FlutterDriver rawAdapter) : super(rawAdapter); + + @override + Future waitForAppToSettle({ + Duration? duration = const Duration(milliseconds: 100), + Duration? timeout = const Duration(seconds: 30), + }) async { + try { + await nativeDriver.waitUntilNoTransientCallbacks(timeout: timeout); + } catch (_) { + return 1; + } + + return 0; + } + + @override + Future widget( + SerializableFinder finder, [ + ExpectedWidgetResultType expectResultType = ExpectedWidgetResultType.first, + ]) { + throw UnimplementedError( + 'Flutter driver does not support directly interacting with the widget tree', + ); + } + + @override + void dispose() { + nativeDriver.close().catchError( + (e, st) { + // Avoid an unhandled error + return null; + }, + ); + } + + @override + Future> screenshot({ + String? screenshotName, + }) { + return nativeDriver.screenshot(); + } + + @override + Future isPresent( + SerializableFinder finder, { + Duration? timeout = const Duration(seconds: 1), + }) async { + try { + await nativeDriver.waitFor( + finder, + timeout: timeout, + ); + return true; + } catch (_) { + return false; + } + } + + @override + Future isAbsent( + SerializableFinder finder, { + Duration? timeout = const Duration(seconds: 1), + }) async { + try { + await nativeDriver.waitForAbsent( + finder, + timeout: timeout, + ); + return true; + } catch (_) { + return false; + } + } + + @override + Future getText( + SerializableFinder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await waitForAppToSettle(timeout: timeout); + + return await nativeDriver.getText( + finder, + timeout: timeout, + ); + } + + @override + Future enterText( + SerializableFinder finder, + String text, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await tap( + finder, + timeout: timeout, + ); + await nativeDriver.enterText( + text, + timeout: timeout, + ); + } + + @override + Future tap( + SerializableFinder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.tap(finder, timeout: timeout); + await waitForAppToSettle(timeout: timeout); + } + + @override + Future longPress( + SerializableFinder finder, { + Duration? pressDuration = const Duration(milliseconds: 500), + Duration? timeout = const Duration(seconds: 30), + }) async { + await scroll( + finder, + dx: 0, + dy: 0, + duration: pressDuration, + timeout: timeout, + ); + await waitForAppToSettle(timeout: timeout); + } + + @override + Future scroll( + SerializableFinder finder, { + double? dx, + double? dy, + Duration? duration = const Duration(milliseconds: 200), + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.scroll( + finder, + dx ?? 0, + dy ?? 0, + duration ?? const Duration(milliseconds: 200), + timeout: timeout, + ); + await waitForAppToSettle(timeout: timeout); + } + + @override + SerializableFinder findBy( + dynamic data, + FindType findType, + ) { + switch (findType) { + case FindType.key: + return find.byValueKey(data.toString()); + case FindType.text: + return find.text(data); + case FindType.tooltip: + return find.byTooltip(data); + case FindType.type: + return find.byType(data.toString()); + } + } + + @override + SerializableFinder findByAncestor( + SerializableFinder of, + SerializableFinder matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }) { + return find.ancestor( + of: of, + matching: matching, + matchRoot: matchRoot, + firstMatchOnly: firstMatchOnly, + ); + } + + @override + SerializableFinder findByDescendant( + SerializableFinder of, + SerializableFinder matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }) { + return find.descendant( + of: of, + matching: matching, + matchRoot: matchRoot, + firstMatchOnly: firstMatchOnly, + ); + } + + @override + Future scrollUntilVisible( + SerializableFinder item, { + SerializableFinder? scrollable, + double? dx, + double? dy, + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.scrollUntilVisible( + scrollable!, + item, + timeout: timeout, + dxScroll: dx ?? 0, + dyScroll: dy ?? 0, + ); + } + + @override + Future scrollIntoView( + SerializableFinder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.scrollIntoView( + finder, + timeout: timeout, + ); + } + + @override + Future pageBack() async { + await tap(find.pageBack()); + await waitForAppToSettle(); + } +} diff --git a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart new file mode 100644 index 0000000..2066f5c --- /dev/null +++ b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart @@ -0,0 +1,306 @@ +import 'dart:io' show Platform; +import 'dart:ui' as ui show ImageByteFormat; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'app_driver_adapter.dart'; + +class WidgetTesterAppDriverAdapter + extends AppDriverAdapter { + IntegrationTestWidgetsFlutterBinding binding; + bool waitImplicitlyAfterAction; + + WidgetTesterAppDriverAdapter({ + required WidgetTester rawAdapter, + required this.binding, + required this.waitImplicitlyAfterAction, + }) : super(rawAdapter); + + @override + Future waitForAppToSettle({ + Duration? duration = const Duration(milliseconds: 100), + Duration? timeout = const Duration(seconds: 30), + }) async { + final result = await _implicitWait( + duration: duration, + timeout: timeout, + force: true, + ); + + return result; + } + + Future _implicitWait({ + Duration? duration = const Duration(milliseconds: 100), + Duration? timeout = const Duration(seconds: 30), + bool? force, + }) async { + if (waitImplicitlyAfterAction || force == true) { + try { + final result = await nativeDriver.pumpAndSettle( + duration ?? const Duration(milliseconds: 100), + EnginePhase.sendSemanticsUpdate, + timeout ?? const Duration(seconds: 30), + ); + + return result; + } catch (_) { + return 0; + } + } + + return 0; + } + + @override + Future widget( + Finder finder, [ + ExpectedWidgetResultType expectResultType = ExpectedWidgetResultType.first, + ]) { + try { + final element = nativeDriver.widget(finder); + + return Future.value(element); + } on StateError { + throw TestFailure( + 'Unable to find element with finder ${finder.toString()}', + ); + } + } + + Future> takeScreenshotUsingRenderElement() async { + RenderObject? renderObject = binding.renderViewElement?.renderObject; + if (renderObject != null) { + while (!renderObject!.isRepaintBoundary) { + renderObject = renderObject.parent as RenderObject?; + assert(renderObject != null); + } + + if (renderObject.debugNeedsPaint) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + final layer = renderObject.debugLayer as OffsetLayer; + + return await layer + .toImage(renderObject.semanticBounds) + .then((value) => value.toByteData(format: ui.ImageByteFormat.png)) + .then((value) => value!.buffer.asUint8List()); + } + + throw Exception('Unable to take screenshot on Android device'); + } + + @override + Future> screenshot({String? screenshotName}) async { + final name = + screenshotName ?? 'screenshot_${DateTime.now().millisecondsSinceEpoch}'; + if (kIsWeb || Platform.isAndroid) { + // try { + // // TODO: See https://github.com/flutter/flutter/issues/92381 + // // we need to call `revertFlutterImage` once it has been implemented + // await binding.convertFlutterSurfaceToImage(); + // await binding.pump(); + // // ignore: no_leading_underscores_for_local_identifiers + // } catch (_, __) {} + + return await takeScreenshotUsingRenderElement(); + } else { + return await binding.takeScreenshot(name); + } + } + + @override + Future isPresent( + Finder finder, { + Duration? timeout = const Duration(seconds: 1), + }) async { + return finder.evaluate().isNotEmpty; + } + + @override + Future isAbsent( + Finder finder, { + Duration? timeout = const Duration(seconds: 1), + }) async { + return await isPresent(finder).then((value) => !value); + } + + @override + Future getText( + Finder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await _implicitWait(timeout: timeout); + + final instance = await widget(finder); + if (instance is Text) { + return instance.data; + } else if (instance is TextSpan) { + return (instance as TextSpan).text; + } else if (instance is TextFormField) { + return instance.controller?.text; + } + + throw Exception( + 'Unable to get text from unknown type `${instance.runtimeType}`'); + } + + @override + Future enterText( + Finder finder, + String text, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.enterText( + finder, + text, + ); + await _implicitWait( + timeout: timeout, + ); + } + + @override + Future tap( + Finder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.tap(finder); + await _implicitWait( + timeout: timeout, + ); + } + + @override + Future longPress( + Finder finder, { + Duration? pressDuration = const Duration(milliseconds: 500), + Duration? timeout = const Duration(seconds: 30), + }) async { + await scroll( + finder, + dx: 0, + dy: 0, + duration: pressDuration, + timeout: timeout, + ); + + await _implicitWait(timeout: timeout); + } + + @override + Finder findBy( + dynamic data, + FindType findType, + ) { + switch (findType) { + case FindType.key: + return find.byKey(data is Key ? data : Key(data)); + case FindType.text: + return find.text(data); + case FindType.tooltip: + return find.byTooltip(data); + case FindType.type: + return find.byType(data); + } + } + + @override + Finder findByAncestor( + Finder of, + Finder matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }) { + return find.ancestor( + of: of, + matching: matching, + matchRoot: matchRoot, + ); + } + + @override + Finder findByDescendant( + Finder of, + Finder matching, { + bool matchRoot = false, + bool firstMatchOnly = false, + }) { + return find.descendant( + of: of, + matching: matching, + matchRoot: matchRoot, + ); + } + + @override + Future scroll( + Finder finder, { + double? dx, + double? dy, + Duration? duration = const Duration(milliseconds: 200), + Duration? timeout = const Duration(seconds: 30), + }) async { + final scrollableFinder = findByDescendant( + finder, + find.byType(Scrollable), + matchRoot: true, + ); + final state = nativeDriver.firstState(scrollableFinder) as ScrollableState; + final position = state.position; + position.jumpTo(dy ?? dx ?? 0); + + // must force a pump and settle to ensure the scroll is performed + await _implicitWait( + duration: duration, + timeout: timeout, + force: true, + ); + } + + @override + Future scrollUntilVisible( + Finder item, { + Finder? scrollable, + double? dx, + double? dy, + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.scrollUntilVisible( + item, + dy ?? dx ?? 0, + scrollable: scrollable, + ); + + // must force a pump and settle to ensure the scroll is performed + await _implicitWait( + timeout: timeout, + force: true, + ); + } + + @override + Future scrollIntoView( + Finder finder, { + Duration? timeout = const Duration(seconds: 30), + }) async { + await nativeDriver.ensureVisible(finder); + + // must force a pump and settle to ensure the scroll is performed + await _implicitWait( + timeout: timeout, + force: true, + ); + } + + @override + Future pageBack() async { + await nativeDriver.pageBack(); + await _implicitWait(); + } +} diff --git a/lib/src/flutter/build_mode.dart b/lib/src/flutter/build_mode.dart deleted file mode 100644 index 2563335..0000000 --- a/lib/src/flutter/build_mode.dart +++ /dev/null @@ -1 +0,0 @@ -enum BuildMode { Debug, Profile } diff --git a/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart b/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart new file mode 100644 index 0000000..fb5b487 --- /dev/null +++ b/lib/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart @@ -0,0 +1,24 @@ +import 'package:gherkin/gherkin.dart'; + +/// An annotation used to specify a class to generate Gherkin tests that adhere +/// to the style required by the integration_test package +class GherkinTestSuite { + /// Path to the feature files to generate tests for + final Iterable featurePaths; + + /// The execution order of features - this default to random to avoid any inter-test dependencies + final ExecutionOrder executionOrder; + + /// The default feature language + final String featureDefaultLanguage; + + /// True (the default) to use absolute file paths for reporters + final bool useAbsolutePaths; + + const GherkinTestSuite({ + this.executionOrder = ExecutionOrder.random, + this.featureDefaultLanguage = 'en', + this.featurePaths = const ['integration_test/features/**.feature'], + this.useAbsolutePaths = true, + }); +} diff --git a/lib/src/flutter/code_generation/builders/gherkin_test_suite_builder.dart b/lib/src/flutter/code_generation/builders/gherkin_test_suite_builder.dart new file mode 100644 index 0000000..81df0c1 --- /dev/null +++ b/lib/src/flutter/code_generation/builders/gherkin_test_suite_builder.dart @@ -0,0 +1,8 @@ +library flutter_gherkin.builder; + +import 'package:build/build.dart'; +import 'package:flutter_gherkin/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart'; +import 'package:source_gen/source_gen.dart'; + +Builder gherkinTestSuiteBuilder(BuilderOptions options) => + SharedPartBuilder([GherkinSuiteTestGenerator()], 'gherkin_tests'); diff --git a/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart b/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart new file mode 100644 index 0000000..3e38b79 --- /dev/null +++ b/lib/src/flutter/code_generation/generators/gherkin_suite_test_generator.dart @@ -0,0 +1,386 @@ +import 'dart:io'; + +// ignore: implementation_imports +import 'package:build/src/builder/build_step.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:flutter_gherkin/src/flutter/code_generation/annotations/gherkin_full_test_suite_annotation.dart'; +import 'package:gherkin/gherkin.dart'; +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:source_gen/source_gen.dart'; + +class NoOpReporter extends MessageReporter { + @override + Future message(String message, MessageLevel level) async { + if (level == MessageLevel.info || level == MessageLevel.debug) { + // ignore: avoid_print + print(message); + } else if (level == MessageLevel.warning) { + // ignore: avoid_print + print('\x1B[33m$message\x1B[0m'); + } else if (level == MessageLevel.error) { + // ignore: avoid_print + print('\x1B[31m$message\x1B[0m'); + } + } +} + +class GherkinSuiteTestGenerator + extends GeneratorForAnnotation { + static const String template = ''' +class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { + _CustomGherkinIntegrationTestRunner({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + required Timeout scenarioExecutionTimeout, + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + LiveTestWidgetsFlutterBindingFramePolicy? framePolicy, + }) : super( + configuration: configuration, + appMainFunction: appMainFunction, + scenarioExecutionTimeout: scenarioExecutionTimeout, + appLifecyclePumpHandler: appLifecyclePumpHandler, + framePolicy: framePolicy, + ); + + @override + void onRun() { + {{features_to_execute}} + } + + {{feature_functions}} +} + +Future executeTestSuite({ + required FlutterTestConfiguration configuration, + required StartAppFn appMainFunction, + Timeout scenarioExecutionTimeout = const Timeout(const Duration(minutes: 10)), + AppLifecyclePumpHandlerFn? appLifecyclePumpHandler, + LiveTestWidgetsFlutterBindingFramePolicy? framePolicy, +}) => + _CustomGherkinIntegrationTestRunner( + configuration: configuration, + appMainFunction: appMainFunction, + appLifecyclePumpHandler: appLifecyclePumpHandler, + scenarioExecutionTimeout: scenarioExecutionTimeout, + framePolicy: framePolicy, + ).run(); +'''; + final _reporter = NoOpReporter(); + final _languageService = LanguageService(); + + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + _languageService.initialise( + annotation.read('featureDefaultLanguage').literalValue.toString(), + ); + final idx = annotation + .read('executionOrder') + .objectValue + .getField('index')! + .toIntValue()!; + final executionOrder = ExecutionOrder.values[idx]; + final featureFiles = annotation + .read('featurePaths') + .listValue + .map((path) => Glob(path.toStringValue()!)) + .map((glob) => + glob.listSync().map((entity) => File(entity.path)).toList()) + .reduce((value, element) => value..addAll(element)); + final useAbsolutePaths = + annotation.read('useAbsolutePaths').objectValue.toBoolValue(); + + if (executionOrder == ExecutionOrder.random) { + featureFiles.shuffle(); + } + + final featureExecutionFunctionsBuilder = StringBuffer(); + final generator = FeatureFileTestGenerator(); + final featuresToExecute = StringBuffer(); + var id = 0; + + for (var featureFile in featureFiles) { + final code = await generator.generate( + id++, + await featureFile.readAsString(), + useAbsolutePaths ?? true ? featureFile.absolute.path : featureFile.path, + _languageService, + _reporter, + ); + + if (code.isNotEmpty) { + featuresToExecute.writeln('testFeature${id - 1}();'); + featureExecutionFunctionsBuilder.writeln(code); + } + } + + return template + .replaceAll('{{feature_functions}}', + featureExecutionFunctionsBuilder.toString()) + .replaceAll( + '{{features_to_execute}}', + featuresToExecute.toString(), + ); + } +} + +class FeatureFileTestGenerator { + Future generate( + int id, + String featureFileContents, + String path, + LanguageService languageService, + MessageReporter reporter, + ) async { + final visitor = FeatureFileTestGeneratorVisitor(); + + return await visitor.generateTests( + id, + featureFileContents, + path, + languageService, + reporter, + ); + } +} + +class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor { + static const String functionTemplate = ''' + void testFeature{{feature_number}}() { + runFeature( + name: '{{feature_name}}:', + tags: {{tags}}, + run: () { + {{scenarios}} + }, + ); + } + '''; + static const String scenarioTemplate = ''' + runScenario( + name: '{{scenario_name}}', + description: {{scenario_description}}, + path: '{{path}}', + tags:{{tags}}, + steps: [{{steps}},], + {{onBefore}} + {{onAfter}} + ); + '''; + static const String stepTemplate = ''' +(TestDependencies dependencies, bool skip,) async { + return runStep( + name: '{{step_name}}', + multiLineStrings: {{step_multi_line_strings}}, + table: {{step_table}}, + dependencies: dependencies, + skip: skip, + );} + '''; + static const String onBeforeScenarioRun = ''' + onBefore: () async => onBeforeRunFeature( + name:'{{feature_name}}', + path:'{{path}}', + description: {{feature_description}}, + tags:{{feature_tags}},), + '''; + static const String onAfterScenarioRun = ''' + onAfter: () async => onAfterRunFeature( + name:'{{feature_name}}', + path:'{{path}}', + description: {{feature_description}}, + tags:{{feature_tags}},), + '''; + + final StringBuffer _buffer = StringBuffer(); + int? _id; + String? _currentFeatureCode; + String? _currentScenarioCode; + final StringBuffer _scenarioBuffer = StringBuffer(); + final StringBuffer _stepBuffer = StringBuffer(); + final _steps = []; + + Future generateTests( + int id, + String featureFileContents, + String path, + LanguageService languageService, + MessageReporter reporter, + ) async { + _id = id; + await visit( + featureFileContents, + path, + languageService, + reporter, + ); + + _flushScenario(); + _flushFeature(); + + return _buffer.toString(); + } + + @override + Future visitFeature( + String name, + String? description, + Iterable tags, + int childScenarioCount, + ) async { + if (childScenarioCount > 0) { + _currentFeatureCode = _replaceVariable( + functionTemplate, + 'feature_number', + _id.toString(), + ); + _currentFeatureCode = _replaceVariable( + _currentFeatureCode!, + 'feature_name', + _escapeText(name), + ); + _currentFeatureCode = _replaceVariable( + _currentFeatureCode!, + 'tags', + '[${tags.map((e) => "'$e'").join(', ')}]', + ); + } + } + + @override + Future visitScenario( + String featureName, + String? featureDescription, + Iterable featureTags, + String name, + String? description, + Iterable tags, + String path, { + required bool isFirst, + required bool isLast, + }) async { + _flushScenario(); + _currentScenarioCode = _replaceVariable( + scenarioTemplate, + 'onBefore', + isFirst ? onBeforeScenarioRun : '', + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'onAfter', + isLast ? onAfterScenarioRun : '', + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'feature_name', + _escapeText(featureName), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'feature_description', + _escapeText( + featureDescription == null ? null : '"""$featureDescription"""', + ), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'path', + _escapeText(path), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'feature_tags', + '[${featureTags.map((e) => "'$e'").join(', ')}]', + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'scenario_name', + _escapeText(name), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'scenario_description', + _escapeText(description == null ? null : '"$description"'), + ); + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'tags', + '[${tags.map((e) => "'$e'").join(', ')}]', + ); + } + + @override + Future visitScenarioStep( + String name, + Iterable multiLineStrings, + GherkinTable? table, + ) async { + var code = _replaceVariable( + stepTemplate, + 'step_name', + _escapeText(name), + ); + code = _replaceVariable( + code, + 'step_multi_line_strings', + '[${multiLineStrings.map((s) => '"""$s"""').join(',')}]', + ); + code = _replaceVariable( + code, + 'step_table', + table == null + ? 'null' + : 'GherkinTable.fromJson(\'${_escapeText(table.toJson())}\')', + ); + + _stepBuffer.writeln(code); + _steps.add(code); + } + + void _flushFeature() { + if (_currentFeatureCode != null) { + _currentFeatureCode = _replaceVariable( + _currentFeatureCode!, + 'scenarios', + _scenarioBuffer.toString(), + ); + + _buffer.writeln(_currentFeatureCode); + } + + _currentFeatureCode = null; + _scenarioBuffer.clear(); + } + + void _flushScenario() { + if (_currentScenarioCode != null) { + if (_steps.isNotEmpty) { + _currentScenarioCode = _replaceVariable( + _currentScenarioCode!, + 'steps', + _steps.join(','), + ); + + _steps.clear(); + } + + _scenarioBuffer.writeln(_currentScenarioCode); + } + + _currentScenarioCode = null; + _stepBuffer.clear(); + } + + String _replaceVariable(String content, String property, String? value) { + return content.replaceAll('{{$property}}', value ?? 'null'); + } + + String? _escapeText(String? text) => text + ?.replaceAll("\\", "\\\\") + .replaceAll("'", "\\'") + .replaceAll(r"$", r"\$"); +} diff --git a/lib/src/flutter/configuration/build_mode.dart b/lib/src/flutter/configuration/build_mode.dart new file mode 100644 index 0000000..bd97a14 --- /dev/null +++ b/lib/src/flutter/configuration/build_mode.dart @@ -0,0 +1 @@ +enum BuildMode { debug, profile } diff --git a/lib/src/flutter/configuration/flutter_driver_test_configuration.dart b/lib/src/flutter/configuration/flutter_driver_test_configuration.dart new file mode 100644 index 0000000..cb440ef --- /dev/null +++ b/lib/src/flutter/configuration/flutter_driver_test_configuration.dart @@ -0,0 +1,281 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart'; +import 'package:flutter_gherkin/src/flutter/hooks/app_runner_hook.dart'; +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:gherkin/gherkin.dart'; + +class FlutterDriverTestConfiguration extends FlutterTestConfiguration { + String? _observatoryDebuggerUri; + + FlutterDriverTestConfiguration({ + String? featurePath = 'features/*.*.feature', + Iterable? features, + super.featureDefaultLanguage = 'en', + super.order = ExecutionOrder.random, + super.defaultTimeout = const Duration(seconds: 10), + super.featureFileMatcher = const IoFeatureFileAccessor(), + super.featureFileReader = const IoFeatureFileAccessor(), + super.stopAfterTestFailed = false, + super.tagExpression, + super.hooks, + super.reporters = const [], + super.createWorld, + super.waitImplicitlyAfterAction = true, + super.customStepParameterDefinitions, + super.stepDefinitions, + this.targetAppPath = 'test_driver/app.dart', + this.targetAppWorkingDirectory, + this.buildFlavour, + this.targetDeviceId, + this.runningAppProtocolEndpointUri, + this.onBeforeFlutterDriverConnect, + this.onAfterFlutterDriverConnect, + this.restartAppBetweenScenarios = true, + this.logFlutterProcessOutput = false, + this.keepAppRunningAfterTests = false, + this.verboseFlutterProcessLogs = false, + this.build = true, + this.buildMode = BuildMode.debug, + this.flutterBuildTimeout = const Duration(seconds: 90), + this.flutterDriverReconnectionDelay = const Duration(seconds: 2), + this.flutterDriverMaxConnectionAttempts = 3, + }) : + // assert(featurePath != null && features != null), + super( + features: features ?? [RegExp(featurePath!)], + ); + + /// Provide a configuration object with default settings such as the reports and feature file location + static FlutterDriverTestConfiguration standard( + Iterable> steps, { + String featurePath = 'features/*.*.feature', + String targetAppPath = 'test_driver/app.dart', + String? targetAppWorkingDirectory, + bool restartAppBetweenScenarios = true, + }) { + return FlutterDriverTestConfiguration( + features: [RegExp(featurePath)], + reporters: [ + StdoutReporter(MessageLevel.error), + ProgressReporter(), + TestRunSummaryReporter(), + JsonReporter(path: './report.json'), + FlutterDriverReporter( + logErrorMessages: true, + logInfoMessages: false, + logWarningMessages: false, + ), + ], + targetAppPath: targetAppPath, + targetAppWorkingDirectory: targetAppWorkingDirectory, + stepDefinitions: steps, + restartAppBetweenScenarios: restartAppBetweenScenarios, + ); + } + + /// restarts the application under test between each scenario. + /// Defaults to true to avoid the application being in an invalid state + /// before each test + final bool restartAppBetweenScenarios; + + /// The target app to run the tests against + /// Defaults to "test_driver/app.dart" + final String targetAppPath; + + /// Option to define the working directory for the process that runs the app under test (optional) + /// Handy if your app is separated from your tests as flutter needs to be able to find a pubspec file + final String? targetAppWorkingDirectory; + + /// The build flavour to run the tests against (optional) + /// Defaults to null + final String? buildFlavour; + + /// The default build mode used for running tests is --debug. + /// We are exposing the option to run the tests also in --profile mode + final BuildMode buildMode; + + /// If the application should be built prior to running the tests + /// Defaults to true + final bool build; + + /// The target device id to run the tests against when multiple devices detected + /// Defaults to null + final String? targetDeviceId; + + /// Will keep the Flutter application running when done testing + /// Defaults to false + final bool keepAppRunningAfterTests; + + /// Logs Flutter process output to stdout + /// The Flutter process is use to start and driver the app under test. + /// The output may contain build and run information + /// Defaults to false + final bool logFlutterProcessOutput; + + /// Sets the --verbose flag on the flutter process + /// Defaults to false + final bool verboseFlutterProcessLogs; + + /// Duration to wait for Flutter to build and start the app on the target device + /// Slower machine may take longer to build and run a large app + /// Defaults to 90 seconds + final Duration flutterBuildTimeout; + + /// Duration to wait before reconnecting the Flutter driver to the app. + /// On slower machines the app might not be in a state where the driver can successfully connect immediately + /// Defaults to 2 seconds + final Duration flutterDriverReconnectionDelay; + + /// The maximum times the flutter driver can try and connect to the running app + /// Defaults to 3 + final int flutterDriverMaxConnectionAttempts; + + /// An observatory url that the test runner can connect to instead of creating a new running instance of the target application + /// 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 + final String? runningAppProtocolEndpointUri; + + /// Called before any attempt to connect Flutter driver to the running application, Depending on your configuration this + /// method will be called before each scenario is run. + final Future Function()? onBeforeFlutterDriverConnect; + + /// Called after the successful connection of Flutter driver to the running application. Depending on your configuration this + /// method will be called on each new connection usually before each scenario is run. + final Future Function(FlutterDriver driver)? + onAfterFlutterDriverConnect; + + void setObservatoryDebuggerUri(String uri) => _observatoryDebuggerUri = uri; + + Future createFlutterDriver([String? dartVmServiceUrl]) async { + final completer = Completer(); + dartVmServiceUrl = (dartVmServiceUrl ?? _observatoryDebuggerUri) ?? + Platform.environment['VM_SERVICE_URL']; + + await runZonedGuarded( + () async { + if (onBeforeFlutterDriverConnect != null) { + await onBeforeFlutterDriverConnect!(); + } + + final driver = await _attemptDriverConnection(dartVmServiceUrl, 1, 3); + if (onAfterFlutterDriverConnect != null) { + await onAfterFlutterDriverConnect!(driver); + } + + completer.complete(driver); + }, + (Object e, StackTrace st) { + if (e is DriverError) { + completer.completeError(e, st); + } + }, + ); + + return completer.future; + } + + Future createFlutterWorld( + TestConfiguration config, + FlutterWorld? world, + ) async { + var flutterConfig = config as FlutterDriverTestConfiguration; + world = world ?? FlutterDriverWorld(); + + final driver = await createFlutterDriver( + flutterConfig.runningAppProtocolEndpointUri?.isNotEmpty ?? false + ? flutterConfig.runningAppProtocolEndpointUri + : null, + ); + + (world as FlutterDriverWorld).setFlutterDriver(driver); + + return world; + } + + @override + TestConfiguration prepare() { + super.prepare(); + _ensureCorrectConfiguration(); + final providedCreateWorld = createWorld; + + return FlutterDriverTestConfiguration( + buildFlavour: buildFlavour, + customStepParameterDefinitions: customStepParameterDefinitions, + defaultTimeout: defaultTimeout, + featureDefaultLanguage: featureDefaultLanguage, + featureFileMatcher: featureFileMatcher, + featureFileReader: featureFileReader, + features: features, + onAfterFlutterDriverConnect: onAfterFlutterDriverConnect, + onBeforeFlutterDriverConnect: onBeforeFlutterDriverConnect, + order: order, + reporters: reporters, + restartAppBetweenScenarios: restartAppBetweenScenarios, + runningAppProtocolEndpointUri: runningAppProtocolEndpointUri, + stepDefinitions: stepDefinitions, + stopAfterTestFailed: stopAfterTestFailed, + tagExpression: tagExpression, + targetAppPath: targetAppPath, + targetAppWorkingDirectory: targetAppWorkingDirectory, + targetDeviceId: targetDeviceId, + createWorld: (config) async { + FlutterWorld? world; + if (providedCreateWorld != null) { + world = await providedCreateWorld(config) as FlutterWorld; + } + + return await createFlutterWorld(config, world); + }, + hooks: List.from(hooks ?? const Iterable.empty()) + ..add( + FlutterAppRunnerHook(), + ), + ); + } + + Future _attemptDriverConnection( + String? dartVmServiceUrl, + int attempt, + int maxAttempts, + ) async { + return await FlutterDriver.connect( + dartVmServiceUrl: dartVmServiceUrl, + ).catchError( + (e, st) async { + if (attempt > maxAttempts) { + throw e; + } else { + // ignore: avoid_print + print( + 'Flutter driver error connecting to application at `$dartVmServiceUrl`,' + 'retrying after delay of $flutterDriverReconnectionDelay', + ); + await Future.delayed(flutterDriverReconnectionDelay); + + return _attemptDriverConnection( + dartVmServiceUrl, + attempt + 1, + maxAttempts, + ); + } + }, + ); + } + + void _ensureCorrectConfiguration() { + if (runningAppProtocolEndpointUri?.isNotEmpty ?? false) { + if (restartAppBetweenScenarios) { + throw AssertionError( + 'Cannot restart app between scenarios if using runningAppProtocolEndpointUri', + ); + } + + if (targetDeviceId?.isNotEmpty ?? false) { + throw AssertionError( + 'Cannot target specific device id if using runningAppProtocolEndpointUri', + ); + } + } + } +} diff --git a/lib/src/flutter/configuration/flutter_test_configuration.dart b/lib/src/flutter/configuration/flutter_test_configuration.dart new file mode 100644 index 0000000..211a5e8 --- /dev/null +++ b/lib/src/flutter/configuration/flutter_test_configuration.dart @@ -0,0 +1,111 @@ +// ignore_for_file: avoid_print + +import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart'; +import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart'; +import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart'; +import 'package:flutter_gherkin/src/flutter/steps/then_expect_widget_to_be_present_step.dart'; +import 'package:flutter_gherkin/src/flutter/steps/when_long_press_widget_step.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gherkin/gherkin.dart'; + +import '../steps/take_a_screenshot_step.dart'; + +class FlutterTestConfiguration extends TestConfiguration { + static final Iterable> _wellKnownParameters = [ + ExistenceParameter(), + SwipeDirectionParameter(), + ]; + static final _wellKnownStepDefinitions = [ + SwipeOnKeyStep(), + SwipeOnTextStep(), + thenExpectElementToHaveValue(), + whenTapBackButtonWidget(), + whenTapWidget(), + whenTapWidgetWithoutScroll(), + whenLongPressWidget(), + whenLongPressWidgetWithoutScroll(), + whenLongPressWidgetForDuration(), + givenOpenDrawer(), + whenPauseStep(), + whenFillFieldStep(), + thenExpectWidgetToBePresent(), + restartAppStep(), + siblingContainsTextStep(), + tapTextWithinWidgetStep(), + tapWidgetOfTypeStep(), + tapWidgetOfTypeWithinStep(), + tapWidgetWithTextStep(), + textExistsStep(), + textExistsWithinStep(), + waitUntilKeyExistsStep(), + waitUntilTypeExistsStep(), + takeScreenshot(), + ]; + + /// Enable semantics in a test by creating a [SemanticsHandle]. + /// See: [testWidgets] and [WidgetController.ensureSemantics]. + final bool semanticsEnabled; + + /// Set to `True` to wait implicit for pumpAndSettle() / waitForAppToSettle() functions after performing actions + /// Defaults to false + final bool waitImplicitlyAfterAction; + + /// Provide a configuration object with default settings + static FlutterTestConfiguration standard( + Iterable> steps, + ) { + return FlutterTestConfiguration( + reporters: [ + StdoutReporter(MessageLevel.error), + ProgressReporter(), + TestRunSummaryReporter(), + ], + stepDefinitions: steps, + ); + } + + /// Provide a configuration object with default settings for web + static FlutterTestConfiguration standardWeb( + Iterable> steps, + ) { + return FlutterTestConfiguration( + reporters: [ + StdoutReporter(MessageLevel.error) + ..setWriteLineFn(print) + ..setWriteFn(print), + ProgressReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + TestRunSummaryReporter() + ..setWriteLineFn(print) + ..setWriteFn(print), + ], + stepDefinitions: steps, + ); + } + + FlutterTestConfiguration({ + super.features = const [], + super.featureDefaultLanguage = 'en', + super.order = ExecutionOrder.random, + super.defaultTimeout = const Duration(seconds: 10), + super.featureFileMatcher = const IoFeatureFileAccessor(), + super.featureFileReader = const IoFeatureFileAccessor(), + super.stopAfterTestFailed = false, + super.tagExpression, + super.hooks, + super.reporters = const [], + super.createWorld, + this.semanticsEnabled = true, + this.waitImplicitlyAfterAction = false, + Iterable>? customStepParameterDefinitions, + Iterable>? stepDefinitions, + }) : super( + customStepParameterDefinitions: List.from( + customStepParameterDefinitions ?? const Iterable.empty(), + )..addAll(_wellKnownParameters), + stepDefinitions: List.from( + stepDefinitions ?? const Iterable.empty(), + )..addAll(_wellKnownStepDefinitions), + ); +} diff --git a/lib/src/flutter/flutter_test_configuration.dart b/lib/src/flutter/flutter_test_configuration.dart deleted file mode 100644 index d5d5c73..0000000 --- a/lib/src/flutter/flutter_test_configuration.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:flutter_gherkin/flutter_gherkin.dart'; -import 'package:flutter_gherkin/src/flutter/build_mode.dart'; -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/hooks/app_runner_hook.dart'; -import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart'; -import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.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/sibling_contains_text_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/swipe_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/tap_text_within_widget_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/tap_widget_of_type_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/tap_widget_of_type_within_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/tap_widget_with_text_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/text_exists_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/text_exists_within_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/then_expect_element_to_have_value_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/wait_until_key_exists_step.dart'; -import 'package:flutter_gherkin/src/flutter/steps/wait_until_type_exists_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_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'; -import 'steps/when_long_press_widget_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> 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 - bool restartAppBetweenScenarios = true; - - /// The target app to run the tests against - /// Defaults to "lib/test_driver/app.dart" - String targetAppPath = 'lib/test_driver/app.dart'; - - /// Option to define the working directory for the process that runs the app under test (optional) - /// Handy if your app is separated from your tests as flutter needs to be able to find a pubspec file - String targetAppWorkingDirectory; - - /// The build flavor to run the tests against (optional) - /// Defaults to empty - String buildFlavor = ''; - - /// The default build mode used for running tests is --debug. - /// We are exposing the option to run the tests also in --profile mode - BuildMode buildMode = BuildMode.Debug; - - /// If the application should be built prior to running the tests - /// Defaults to true - bool build = true; - - /// The target device id to run the tests against when multiple devices detected - /// Defaults to empty - String targetDeviceId = ''; - - /// Will keep the Flutter application running when done testing - /// Defaults to false - bool keepAppRunningAfterTests = false; - - /// Logs Flutter process output to stdout - /// The Flutter process is use to start and driver the app under test. - /// The output may contain build and run information - /// Defaults to false - bool logFlutterProcessOutput = false; - - /// Sets the --verbose flag on the flutter process - /// Defaults to false - bool verboseFlutterProcessLogs = false; - - /// Duration to wait for Flutter to build and start the app on the target device - /// Slower machine may take longer to build and run a large app - /// Defaults to 90 seconds - Duration flutterBuildTimeout = const Duration(seconds: 90); - - /// Duration to wait before reconnecting the Flutter driver to the app. - /// On slower machines the app might not be in a state where the driver can successfully connect immediately - /// Defaults to 2 seconds - Duration flutterDriverReconnectionDelay = const Duration(seconds: 2); - - /// The maximum times the flutter driver can try and connect to the running app - /// Defaults to 3 - int flutterDriverMaxConnectionAttempts = 3; - - /// An observatory url that the test runner can connect to instead of creating a new running instance of the target application - /// 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 - String runningAppProtocolEndpointUri; - - /// Called before any attempt to connect Flutter driver to the running application, Depending on your configuration this - /// method will be called before each scenario is run. - Future Function() onBeforeFlutterDriverConnect; - - /// Called after the successful connection of Flutter driver to the running application. Depending on your configuration this - /// method will be called on each new connection usually before each scenario is run. - Future Function(FlutterDriver driver) onAfterFlutterDriverConnect; - - void setObservatoryDebuggerUri(String uri) => _observatoryDebuggerUri = uri; - - Future createFlutterDriver([String dartVmServiceUrl]) async { - final completer = Completer(); - dartVmServiceUrl = (dartVmServiceUrl ?? _observatoryDebuggerUri) ?? - Platform.environment['VM_SERVICE_URL']; - - await runZonedGuarded( - () async { - if (onBeforeFlutterDriverConnect != null) { - await onBeforeFlutterDriverConnect(); - } - - final driver = await _attemptDriverConnection(dartVmServiceUrl, 1, 3); - if (onAfterFlutterDriverConnect != null) { - await onAfterFlutterDriverConnect(driver); - } - - completer.complete(driver); - }, - (Object e, StackTrace st) { - if (e is DriverError) { - completer.completeError(e, st); - } - }, - ); - - return completer.future; - } - - Future createFlutterWorld( - TestConfiguration config, - FlutterWorld world, - ) async { - var flutterConfig = config as FlutterTestConfiguration; - world = world ?? FlutterWorld(); - - final driver = await createFlutterDriver( - flutterConfig.runningAppProtocolEndpointUri != null && - flutterConfig.runningAppProtocolEndpointUri.isNotEmpty - ? flutterConfig.runningAppProtocolEndpointUri - : null, - ); - - world.setFlutterDriver(driver); - - return world; - } - - @override - void prepare() { - _ensureCorrectConfiguration(); - final providedCreateWorld = createWorld; - createWorld = (config) async { - FlutterWorld world; - if (providedCreateWorld != null) { - world = await providedCreateWorld(config); - } - - return await createFlutterWorld(config, world); - }; - - hooks = List.from(hooks ?? [])..add(FlutterAppRunnerHook()); - customStepParameterDefinitions = - List.from(customStepParameterDefinitions ?? []) - ..addAll([ - ExistenceParameter(), - SwipeDirectionParameter(), - ]); - stepDefinitions = List.from(stepDefinitions ?? []) - ..addAll([ - ThenExpectElementToHaveValue(), - WhenTapBackButtonWidget(), - WhenTapWidget(), - WhenTapWidgetWithoutScroll(), - WhenLongPressWidget(), - WhenLongPressWidgetWithoutScroll(), - WhenLongPressWidgetForDuration(), - GivenOpenDrawer(), - WhenPauseStep(), - WhenFillFieldStep(), - ThenExpectWidgetToBePresent(), - RestartAppStep(), - SiblingContainsTextStep(), - SwipeOnKeyStep(), - SwipeOnTextStep(), - TapTextWithinWidgetStep(), - TapWidgetOfTypeStep(), - TapWidgetOfTypeWithinStep(), - TapWidgetWithTextStep(), - TextExistsStep(), - TextExistsWithinStep(), - WaitUntilKeyExistsStep(), - WaitUntilTypeExistsStep(), - ]); - } - - Future _attemptDriverConnection( - String dartVmServiceUrl, - int attempt, - int maxAttempts, - ) async { - return await FlutterDriver.connect( - dartVmServiceUrl: dartVmServiceUrl, - ).catchError( - (e, st) async { - if (attempt > maxAttempts) { - throw e; - } else { - print( - 'Fluter driver error connecting to application at `$dartVmServiceUrl`, retrying after delay of $flutterDriverReconnectionDelay', - ); - await Future.delayed(flutterDriverReconnectionDelay); - - return _attemptDriverConnection( - dartVmServiceUrl, - attempt + 1, - maxAttempts, - ); - } - }, - ); - } - - void _ensureCorrectConfiguration() { - if (runningAppProtocolEndpointUri != null && - runningAppProtocolEndpointUri.isNotEmpty) { - if (restartAppBetweenScenarios) { - throw AssertionError( - 'Cannot restart app between scenarios if using runningAppProtocolEndpointUri'); - } - - if (targetDeviceId != null && targetDeviceId.isNotEmpty) { - throw AssertionError( - 'Cannot target specific device id if using runningAppProtocolEndpointUri'); - } - } - } -} diff --git a/lib/src/flutter/flutter_world.dart b/lib/src/flutter/flutter_world.dart deleted file mode 100644 index d93e5c9..0000000 --- a/lib/src/flutter/flutter_world.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:gherkin/gherkin.dart'; - -import 'flutter_run_process_handler.dart'; - -class FlutterWorld extends World { - FlutterDriver _driver; - FlutterRunProcessHandler _flutterRunProcessHandler; - - FlutterDriver get driver => _driver; - - void setFlutterDriver(FlutterDriver flutterDriver) { - _driver = flutterDriver; - } - - void setFlutterProcessHandler( - FlutterRunProcessHandler flutterRunProcessHandler) { - _flutterRunProcessHandler = flutterRunProcessHandler; - } - - Future restartApp({ - Duration timeout = const Duration(seconds: 60), - }) async { - await _closeDriver(timeout: timeout); - final result = await _flutterRunProcessHandler?.restart( - timeout: timeout, - ); - - _driver = await FlutterDriver.connect( - dartVmServiceUrl: _flutterRunProcessHandler.currentObservatoryUri, - ); - - return result; - } - - @override - void dispose() async { - super.dispose(); - _flutterRunProcessHandler = null; - await _closeDriver(timeout: const Duration(seconds: 5)); - } - - Future _closeDriver({ - Duration timeout = const Duration(seconds: 60), - }) async { - try { - if (_driver != null) { - await _driver.close().catchError( - (e, st) { - // Avoid an unhandled error - return null; - }, - ); - } - } finally { - _driver = null; - } - } -} diff --git a/lib/src/flutter/hooks/app_runner_hook.dart b/lib/src/flutter/hooks/app_runner_hook.dart index 5bc0bfd..ece11b9 100644 --- a/lib/src/flutter/hooks/app_runner_hook.dart +++ b/lib/src/flutter/hooks/app_runner_hook.dart @@ -1,14 +1,15 @@ import 'dart:io'; -import 'package:flutter_gherkin/src/flutter/flutter_run_process_handler.dart'; -import 'package:flutter_gherkin/src/flutter/flutter_test_configuration.dart'; -import 'package:gherkin/gherkin.dart'; -import '../flutter_world.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_gherkin/src/flutter/configuration/flutter_driver_test_configuration.dart'; +import 'package:flutter_gherkin/src/flutter/runners/flutter_run_process_handler.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_driver_world.dart'; +import 'package:gherkin/gherkin.dart'; /// A hook that manages running the target flutter application /// that is under test class FlutterAppRunnerHook extends Hook { - FlutterRunProcessHandler _flutterRunProcessHandler; + FlutterRunProcessHandler? _flutterRunProcessHandler; bool haveRunFirstScenario = false; @override @@ -39,8 +40,9 @@ class FlutterAppRunnerHook extends Hook { Future onAfterScenario( TestConfiguration config, String scenario, - Iterable tags, - ) async { + Iterable tags, { + bool? passed, + }) async { final flutterConfig = _castConfig(config); haveRunFirstScenario = true; if (_flutterRunProcessHandler != null && @@ -55,17 +57,18 @@ class FlutterAppRunnerHook extends Hook { String scenario, Iterable tags, ) async { - if (world is FlutterWorld) { - world.setFlutterProcessHandler(_flutterRunProcessHandler); + if (world is FlutterDriverWorld) { + world.setFlutterProcessHandler(_flutterRunProcessHandler!); } } - Future _runApp(FlutterTestConfiguration config) async { - if (config.runningAppProtocolEndpointUri != null && - config.runningAppProtocolEndpointUri.isNotEmpty) { - stdout.writeln( - "Connecting to running Flutter app under test at '${config.runningAppProtocolEndpointUri}', this might take a few moments"); - config.setObservatoryDebuggerUri(config.runningAppProtocolEndpointUri); + Future _runApp(FlutterDriverTestConfiguration config) async { + if (config.runningAppProtocolEndpointUri?.isNotEmpty ?? false) { + _log( + "Connecting to running Flutter app under test at '${config.runningAppProtocolEndpointUri}', " + 'this might take a few moments', + ); + config.setObservatoryDebuggerUri(config.runningAppProtocolEndpointUri!); } else { _flutterRunProcessHandler = FlutterRunProcessHandler() ..setLogFlutterProcessOutput(config.logFlutterProcessOutput) @@ -75,14 +78,15 @@ class FlutterAppRunnerHook extends Hook { ..setWorkingDirectory(config.targetAppWorkingDirectory) ..setBuildRequired(haveRunFirstScenario ? false : config.build) ..setKeepAppRunning(config.keepAppRunningAfterTests) - ..setBuildFlavor(config.buildFlavor) + ..setBuildFlavour(config.buildFlavour) ..setBuildMode(config.buildMode) ..setDeviceTargetId(config.targetDeviceId); - stdout.writeln( - "Starting Flutter app under test '${config.targetAppPath}', this might take a few moments"); - await _flutterRunProcessHandler.run(); - final observatoryUri = await _flutterRunProcessHandler + _log( + "Starting Flutter app under test '${config.targetAppPath}', this might take a few moments", + ); + await _flutterRunProcessHandler!.run(); + final observatoryUri = await _flutterRunProcessHandler! .waitForObservatoryDebuggerUri(config.flutterBuildTimeout); config.setObservatoryDebuggerUri(observatoryUri); } @@ -90,19 +94,25 @@ class FlutterAppRunnerHook extends Hook { Future _terminateApp() async { if (_flutterRunProcessHandler != null) { - stdout.writeln('Terminating Flutter app under test'); - await _flutterRunProcessHandler.terminate(); + _log('Terminating Flutter app under test'); + await _flutterRunProcessHandler!.terminate(); _flutterRunProcessHandler = null; } } Future _restartApp() async { if (_flutterRunProcessHandler != null) { - stdout.writeln('Restarting Flutter app under test'); - await _flutterRunProcessHandler.restart(); + _log('Restarting Flutter app under test'); + await _flutterRunProcessHandler!.restart(); } } - FlutterTestConfiguration _castConfig(TestConfiguration config) => - config as FlutterTestConfiguration; + FlutterDriverTestConfiguration _castConfig(TestConfiguration config) => + config as FlutterDriverTestConfiguration; + + void _log(String text) { + if (!kIsWeb) { + stdout.writeln(text); + } + } } diff --git a/lib/src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart b/lib/src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart index a00b2c3..26df932 100644 --- a/lib/src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart +++ b/lib/src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart @@ -1,8 +1,7 @@ import 'dart:convert'; import 'package:gherkin/gherkin.dart'; -import 'package:meta/meta.dart'; -import '../flutter_world.dart'; +import '../world/flutter_world.dart'; class AttachScreenshotOnFailedStepHook extends Hook { @override @@ -15,7 +14,7 @@ class AttachScreenshotOnFailedStepHook extends Hook { stepResult.result == StepExecutionResult.error || stepResult.result == StepExecutionResult.timeout) { try { - final screenshotData = await takeScreenshot(world); + final screenshotData = await _takeScreenshot(world); world.attach(screenshotData, 'image/png', step); } catch (e, st) { world.attach('Failed to take screenshot\n$e\n$st', 'text/plain', step); @@ -23,9 +22,8 @@ class AttachScreenshotOnFailedStepHook extends Hook { } } - @protected - Future takeScreenshot(World world) async { - final bytes = await (world as FlutterWorld).driver.screenshot(); + Future _takeScreenshot(World world) async { + final bytes = await (world as FlutterWorld).appDriver.screenshot(); return base64Encode(bytes); } diff --git a/lib/src/flutter/reporters/flutter_driver_reporter.dart b/lib/src/flutter/reporters/flutter_driver_reporter.dart index 2c97005..775495f 100644 --- a/lib/src/flutter/reporters/flutter_driver_reporter.dart +++ b/lib/src/flutter/reporters/flutter_driver_reporter.dart @@ -13,11 +13,14 @@ enum _FlutterDriverMessageLogLevel { info, warning, error } /// This can cause problems with CI servers for example as they will mark a process as failed if it logs to the /// stderr stream. So Flutter driver will log a normal info message to the stderr and thus make /// the process fail from the perspective of a CI server. -class FlutterDriverReporter extends Reporter { +class FlutterDriverReporter extends Reporter + implements DisposableReporter, TestReporter { final bool logErrorMessages; final bool logWarningMessages; final bool logInfoMessages; + DriverLogCallback? defaultCallback; + FlutterDriverReporter({ this.logErrorMessages = true, this.logWarningMessages = true, @@ -25,13 +28,18 @@ class FlutterDriverReporter extends Reporter { }); @override - Future onTestRunStarted() async { - driverLog = _driverLogMessageHandler; - } + ReportActionHandler get test => ReportActionHandler( + onStarted: ([_]) async { + defaultCallback = driverLog; + driverLog = _driverLogMessageHandler; + }, + ); @override Future dispose() async { - driverLog = null; + if (defaultCallback != null) { + driverLog = defaultCallback!; + } } void _driverLogMessageHandler(String source, String message) { diff --git a/lib/src/flutter/flutter_run_process_handler.dart b/lib/src/flutter/runners/flutter_run_process_handler.dart similarity index 69% rename from lib/src/flutter/flutter_run_process_handler.dart rename to lib/src/flutter/runners/flutter_run_process_handler.dart index 1fc2faa..0d70e22 100644 --- a/lib/src/flutter/flutter_run_process_handler.dart +++ b/lib/src/flutter/runners/flutter_run_process_handler.dart @@ -1,18 +1,15 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter_gherkin/src/flutter/build_mode.dart'; +import 'package:flutter_gherkin/src/flutter/configuration/build_mode.dart'; import 'package:gherkin/gherkin.dart'; class FlutterRunProcessHandler extends ProcessHandler { - static const String FAIL_COLOR = '\u001b[33;31m'; // red - static const String WARN_COLOR = '\u001b[33;10m'; // yellow - static const String RESET_COLOR = '\u001b[33;0m'; - // the flutter process usually outputs something like the below to indicate the app is ready to be connected to // `An Observatory debugger and profiler on AOSP on IA Emulator is available at: http://127.0.0.1:51322/BI_fyYaeoCE=/` + // `Observatory URL on device: http://127.0.0.1:37849/t2xp9hvaxNs=/` static final RegExp _observatoryDebuggerUriRegex = RegExp( - r'observatory (?:debugger|url) .* available .*[:]? (http[s]?:.*\/).*', + r'observatory .*[:] (http[s]?:.*\/).*', caseSensitive: false, multiLine: false, ); @@ -41,20 +38,20 @@ class FlutterRunProcessHandler extends ProcessHandler { multiLine: false, ); - Process _runningProcess; - Stream _processStdoutStream; + Process? _runningProcess; + Stream? _processStdoutStream; final List _openSubscriptions = []; bool _buildApp = true; bool _logFlutterProcessOutput = false; bool _verboseFlutterLogs = false; bool _keepAppRunning = false; - BuildMode _buildMode = BuildMode.Debug; - String _workingDirectory; - String _appTarget; - String _buildFlavor; - String _deviceTargetId; + BuildMode _buildMode = BuildMode.debug; + String? _workingDirectory; + String? _appTarget; + String? _buildFlavour; + String? _deviceTargetId; Duration _driverConnectionDelay = const Duration(seconds: 2); - String currentObservatoryUri; + String? currentObservatoryUri; void setLogFlutterProcessOutput(bool logFlutterProcessOutput) { _logFlutterProcessOutput = logFlutterProcessOutput; @@ -64,23 +61,23 @@ class FlutterRunProcessHandler extends ProcessHandler { _appTarget = targetPath; } - void setDriverConnectionDelay(Duration duration) { + void setDriverConnectionDelay(Duration? duration) { _driverConnectionDelay = duration ?? _driverConnectionDelay; } - void setWorkingDirectory(String workingDirectory) { + void setWorkingDirectory(String? workingDirectory) { _workingDirectory = workingDirectory; } - void setBuildFlavor(String buildFlavor) { - _buildFlavor = buildFlavor; + void setBuildFlavour(String? buildFlavour) { + _buildFlavour = buildFlavour; } void setBuildMode(BuildMode buildMode) { _buildMode = buildMode; } - void setDeviceTargetId(String deviceTargetId) { + void setDeviceTargetId(String? deviceTargetId) { _deviceTargetId = deviceTargetId; } @@ -100,9 +97,9 @@ class FlutterRunProcessHandler extends ProcessHandler { Future run() async { final arguments = ['run', '--target=$_appTarget']; - if (_buildMode == BuildMode.Debug) { + if (_buildMode == BuildMode.debug) { arguments.add('--debug'); - } else if (_buildMode == BuildMode.Profile) { + } else if (_buildMode == BuildMode.profile) { arguments.add('--profile'); } @@ -110,11 +107,11 @@ class FlutterRunProcessHandler extends ProcessHandler { arguments.add('--no-build'); } - if (_buildFlavor != null && _buildFlavor.isNotEmpty) { - arguments.add('--flavor=$_buildFlavor'); + if (_buildFlavour != null && _buildFlavour!.isNotEmpty) { + arguments.add('--flavor=$_buildFlavour'); } - if (_deviceTargetId != null && _deviceTargetId.isNotEmpty) { + if (_deviceTargetId != null && _deviceTargetId!.isNotEmpty) { arguments.add('--device-id=$_deviceTargetId'); } @@ -122,10 +119,6 @@ class FlutterRunProcessHandler extends ProcessHandler { arguments.add('--verbose'); } - if (_keepAppRunning) { - arguments.add('--keep-app-running'); - } - if (_logFlutterProcessOutput) { stdout.writeln( 'Invoking from working directory `${_workingDirectory ?? './'}` command: `flutter ${arguments.join(' ')}`', @@ -140,18 +133,22 @@ class FlutterRunProcessHandler extends ProcessHandler { ); _processStdoutStream = - _runningProcess.stdout.transform(utf8.decoder).asBroadcastStream(); + _runningProcess!.stdout.transform(utf8.decoder).asBroadcastStream(); - _openSubscriptions.add(_runningProcess.stderr + _openSubscriptions.add(_runningProcess!.stderr .map((events) => String.fromCharCodes(events).trim()) .where((event) => event.isNotEmpty) .listen((event) { if (event.contains(_errorMessageRegex)) { - stderr.writeln('${FAIL_COLOR}Flutter build error: $event$RESET_COLOR'); + stderr.writeln( + '${StdoutReporter.kFailColor}Flutter build error: $event${StdoutReporter.kResetColor}', + ); } else { - // This is most likely a depricated api usage warnings (from Gradle) and should not + // This is most likely a deprecated api usage warnings (from Gradle) and should not // cause the test run to fail. - stdout.writeln('$WARN_COLOR$event$RESET_COLOR'); + stdout.writeln( + '${StdoutReporter.kWarnColor}$event${StdoutReporter.kResetColor}', + ); } })); } @@ -161,19 +158,28 @@ class FlutterRunProcessHandler extends ProcessHandler { var exitCode = -1; _ensureRunningProcess(); if (_runningProcess != null) { - _runningProcess.stdin.write('q'); - _openSubscriptions.forEach((s) => s.cancel()); + if (_keepAppRunning) { + _runningProcess!.stdin.write('d'); + } else { + _runningProcess!.stdin.write('q'); + } + + for (var s in _openSubscriptions) { + s.cancel(); + } _openSubscriptions.clear(); - exitCode = await _runningProcess.exitCode; + exitCode = await _runningProcess!.exitCode; _runningProcess = null; } return exitCode; } - Future restart({Duration timeout = const Duration(seconds: 90)}) async { + Future restart({ + Duration? timeout = const Duration(seconds: 90), + }) async { _ensureRunningProcess(); - _runningProcess.stdin.write('R'); + _runningProcess!.stdin.write('R'); await _waitForStdOutMessage( _restartedApplicationSuccessRegex, 'Timeout waiting for app restart', @@ -196,18 +202,18 @@ class FlutterRunProcessHandler extends ProcessHandler { timeout, ); - return currentObservatoryUri; + return currentObservatoryUri!; } Future _waitForStdOutMessage( RegExp matcher, String timeoutMessage, [ - Duration timeout = const Duration(seconds: 90), + Duration? timeout = const Duration(seconds: 90), ]) { _ensureRunningProcess(); final completer = Completer(); - StreamSubscription sub; - sub = _processStdoutStream.timeout( + StreamSubscription? sub; + sub = _processStdoutStream!.timeout( timeout ?? const Duration(seconds: 90), onTimeout: (_) { sub?.cancel(); @@ -223,18 +229,23 @@ class FlutterRunProcessHandler extends ProcessHandler { if (matcher.hasMatch(logLine)) { sub?.cancel(); if (!completer.isCompleted) { - completer.complete(matcher.firstMatch(logLine).group(1)); + completer.complete(matcher.firstMatch(logLine)!.group(1)); } } else if (_noConnectedDeviceRegex.hasMatch(logLine)) { sub?.cancel(); if (!completer.isCompleted) { stderr.writeln( - '${FAIL_COLOR}No connected devices found to run app on and tests against$RESET_COLOR'); + '${StdoutReporter.kFailColor}' + 'No connected devices found to run app on and tests against' + '${StdoutReporter.kResetColor}', + ); } } else if (_moreThanOneDeviceConnectedDeviceRegex.hasMatch(logLine)) { sub?.cancel(); if (!completer.isCompleted) { - stderr.writeln('$FAIL_COLOR$logLine$RESET_COLOR'); + stderr.writeln( + '${StdoutReporter.kFailColor}$logLine${StdoutReporter.kResetColor}', + ); } } }, diff --git a/lib/src/flutter/runners/gherkin_integration_test_runner.dart b/lib/src/flutter/runners/gherkin_integration_test_runner.dart new file mode 100644 index 0000000..78f59ef --- /dev/null +++ b/lib/src/flutter/runners/gherkin_integration_test_runner.dart @@ -0,0 +1,542 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:collection/collection.dart'; + +enum AppLifecyclePhase { + initialisation, + finalisation, +} + +typedef StepFn = Future Function( + TestDependencies dependencies, + bool skip, +); + +typedef StartAppFn = Future Function(World world); +typedef AppLifecyclePumpHandlerFn = Future Function( + AppLifecyclePhase phase, + WidgetTester tester, +); + +class TestDependencies { + final World world; + final AttachmentManager attachmentManager; + + TestDependencies( + this.world, + this.attachmentManager, + ); +} + +abstract class GherkinIntegrationTestRunner { + final TagExpressionEvaluator _tagExpressionEvaluator = + TagExpressionEvaluator(); + final FlutterTestConfiguration configuration; + final StartAppFn appMainFunction; + final AppLifecyclePumpHandlerFn? appLifecyclePumpHandler; + final Timeout scenarioExecutionTimeout; + final LiveTestWidgetsFlutterBindingFramePolicy? framePolicy; + final AggregatedReporter _reporter = AggregatedReporter(); + + late final Iterable? _executableSteps; + late final Iterable? _customParameters; + late final Hook? _hook; + late final IntegrationTestWidgetsFlutterBinding _binding; + + AggregatedReporter get reporter => _reporter; + Hook get hook => _hook!; + + /// A Gherkin test runner that uses [WidgetTester] to instrument the app under test. + /// + /// [configuration] the configuration for the test run. + /// + /// [appMainFunction] a function to start the app under test. + /// + /// [appLifecyclePumpHandler] a function to determine how to pump the app during various lifecycle phases, + /// if null a default handler is used see [_appLifecyclePhasePumper]. + /// + /// [scenarioExecutionTimeout] the default execution timeout for the whole test run. + GherkinIntegrationTestRunner({ + required this.configuration, + required this.appMainFunction, + required this.scenarioExecutionTimeout, + this.appLifecyclePumpHandler, + this.framePolicy, + }) { + configuration.prepare(); + _registerReporters(configuration.reporters); + _hook = _registerHooks(configuration.hooks); + _customParameters = + _registerCustomParameters(configuration.customStepParameterDefinitions); + _executableSteps = _registerStepDefinitions( + configuration.stepDefinitions!, + _customParameters!, + ); + } + + Future run() async { + _binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + _binding.framePolicy = framePolicy ?? _binding.framePolicy; + + tearDownAll( + () async { + await onRunComplete(); + }, + ); + + await hook.onBeforeRun(configuration); + await reporter.test.onStarted.invoke(); + + onRun(); + } + + void onRun(); + + Future onRunComplete() async { + await reporter.test.onFinished.invoke(); + await hook.onAfterRun(configuration); + setTestResultData(_binding); + () async => await reporter.dispose(); + } + + void setTestResultData(IntegrationTestWidgetsFlutterBinding binding) { + final json = (reporter).serialize(); + binding.reportData = {'gherkin_reports': json}; + } + + @protected + void runFeature({ + required String name, + required void Function() run, + Iterable? tags, + }) { + group( + name, + () { + run(); + }, + ); + } + + @protected + Future onBeforeRunFeature({ + required String name, + required String path, + String? description, + Iterable? tags, + }) async { + final debugInformation = RunnableDebugInformation(path, 0, name); + final featureTags = + (tags ?? const Iterable.empty()).map((t) => Tag(t.toString(), 0)); + await reporter.feature.onStarted.invoke( + FeatureMessage( + name: name, + description: description, + context: debugInformation, + tags: featureTags.toList(growable: false), + ), + ); + } + + @protected + Future onAfterRunFeature({ + required String name, + required String path, + String? description, + required List? tags, + }) async { + final debugInformation = RunnableDebugInformation(path, 0, name); + await reporter.feature.onFinished.invoke( + FeatureMessage( + name: name, + description: description, + context: debugInformation, + tags: (tags ?? const Iterable.empty()) + .map( + (t) => Tag(t.toString(), 0), + ) + .toList(growable: false), + ), + ); + } + + @protected + void runScenario({ + required String name, + required Iterable? tags, + required List steps, + required String path, + String? description, + Future Function()? onBefore, + Future Function()? onAfter, + }) { + if (_evaluateTagFilterExpression(configuration.tagExpression, tags)) { + testWidgets( + name, + (WidgetTester tester) async { + if (onBefore != null) { + await onBefore(); + } + bool failed = false; + + final debugInformation = RunnableDebugInformation(path, 0, name); + final scenarioTags = (tags ?? const Iterable.empty()).map( + (t) => Tag(t.toString(), 0), + ); + final dependencies = await createTestDependencies( + configuration, + tester, + ); + + try { + await hook.onBeforeScenario( + configuration, + name, + scenarioTags, + ); + + await startApp( + tester, + dependencies.world, + ); + + await hook.onAfterScenarioWorldCreated( + dependencies.world, + name, + scenarioTags, + ); + + await reporter.scenario.onStarted.invoke( + ScenarioMessage( + name: name, + description: description, + context: debugInformation, + tags: scenarioTags.toList(), + ), + ); + var hasToSkip = false; + for (int i = 0; i < steps.length; i++) { + try { + final result = await steps[i](dependencies, hasToSkip); + if (_isNegativeResult(result.result)) { + failed = true; + hasToSkip = true; + } + } catch (err, st) { + failed = true; + hasToSkip = true; + + await reporter.onException(err, st); + } + } + } finally { + await reporter.scenario.onFinished.invoke( + ScenarioMessage( + name: name, + description: description, + context: debugInformation, + hasPassed: !failed, + ), + ); + + await hook.onAfterScenario( + configuration, + name, + scenarioTags, + passed: !failed, + ); + + if (onAfter != null) { + await onAfter(); + } + + // need to pump so app can finalise + await _appLifecyclePhasePumper( + AppLifecyclePhase.finalisation, + tester, + ); + + await cleanUpScenarioRun(dependencies); + } + }, + timeout: scenarioExecutionTimeout, + semanticsEnabled: configuration.semanticsEnabled, + ); + } else { + reporter.message( + 'Ignoring scenario `$name` as tag expression `${configuration.tagExpression}` not satisfied', + MessageLevel.info, + ); + } + } + + @protected + Future startApp( + WidgetTester tester, + World world, + ) async { + await appMainFunction(world); + + // need to pump so app is initialised + await _appLifecyclePhasePumper(AppLifecyclePhase.initialisation, tester); + } + + @protected + Future createTestDependencies( + TestConfiguration configuration, + WidgetTester tester, + ) async { + World? world; + final attachmentManager = + await configuration.getAttachmentManager(configuration); + + if (configuration.createWorld != null) { + world = await configuration.createWorld!(configuration); + } + + world = world ?? FlutterWidgetTesterWorld(); + world.setAttachmentManager(attachmentManager); + + (world as FlutterWorld).setAppAdapter( + WidgetTesterAppDriverAdapter( + rawAdapter: tester, + binding: _binding, + waitImplicitlyAfterAction: configuration is FlutterTestConfiguration + ? (configuration).waitImplicitlyAfterAction + : true, + ), + ); + + return TestDependencies( + world, + attachmentManager, + ); + } + + @protected + Future runStep({ + required String name, + required Iterable multiLineStrings, + required dynamic table, + required TestDependencies dependencies, + required bool skip, + }) async { + StepResult? result; + + try { + final executable = _executableSteps!.firstWhereOrNull( + (s) => s.expression.isMatch(name), + ); + + if (executable == null) { + final message = 'Step definition not found for text: `$name`'; + throw GherkinStepNotDefinedException(message); + } + + var parameters = _getStepParameters( + step: name, + multiLineStrings: multiLineStrings, + table: table, + code: executable, + ); + + await _onBeforeStepRun( + world: dependencies.world, + step: name, + table: table, + multiLineStrings: multiLineStrings, + ); + + if (skip) { + result = StepResult( + 0, + StepExecutionResult.skipped, + resultReason: 'Previous step(s) failed', + ); + } else { + for (int i = 0; i < configuration.stepMaxRetries + 1; i++) { + result = await executable.step.run( + dependencies.world, + reporter, + configuration.defaultTimeout, + parameters, + ); + + if (!_isNegativeResult(result.result) || + configuration.stepMaxRetries == 0) { + break; + } else { + await Future.delayed(configuration.retryDelay); + } + } + } + } catch (err, st) { + result = ErroredStepResult( + 0, + StepExecutionResult.error, + err, + st, + ); + } + + await _onAfterStepRun( + name, + result!, + dependencies, + ); + + return result; + } + + @protected + Future cleanUpScenarioRun(TestDependencies dependencies) async { + dependencies.attachmentManager.dispose(); + dependencies.world.dispose(); + } + + void _registerReporters(Iterable? reporters) { + if (reporters != null) { + for (var r in reporters) { + _reporter.addReporter(r); + } + } + } + + Hook _registerHooks(Iterable? hooks) { + final hook = AggregatedHook(); + if (hooks != null) { + hook.addHooks(hooks); + } + + return hook; + } + + Iterable _registerCustomParameters( + Iterable? customParameters, + ) { + final parameters = []; + + parameters.add(FloatParameterLower()); + parameters.add(FloatParameterCamel()); + parameters.add(NumParameterLower()); + parameters.add(NumParameterCamel()); + parameters.add(IntParameterLower()); + parameters.add(IntParameterCamel()); + parameters.add(StringParameterLower()); + parameters.add(StringParameterCamel()); + parameters.add(WordParameterLower()); + parameters.add(WordParameterCamel()); + parameters.add(PluralParameter()); + if (customParameters != null && customParameters.isNotEmpty) { + parameters.addAll(customParameters); + } + + return parameters; + } + + Iterable _registerStepDefinitions( + Iterable stepDefinitions, + Iterable customParameters, + ) { + return stepDefinitions + .map( + (s) => ExecutableStep( + GherkinExpression(s.pattern as RegExp, customParameters), + s, + ), + ) + .toList(growable: false); + } + + Iterable _getStepParameters({ + required String step, + required Iterable multiLineStrings, + required ExecutableStep code, + GherkinTable? table, + }) { + var parameters = code.expression.getParameters(step); + if (multiLineStrings.isNotEmpty) { + parameters = parameters.toList()..addAll(multiLineStrings); + } + + if (table != null) { + parameters = parameters.toList()..add(table); + } + + return parameters; + } + + Future _onAfterStepRun( + String step, + StepResult result, + TestDependencies dependencies, + ) async { + await hook.onAfterStep( + dependencies.world, + step, + result, + ); + + await reporter.step.onFinished.invoke( + StepMessage( + name: step, + context: RunnableDebugInformation('', 0, step), + result: result, + attachments: dependencies.attachmentManager + .getAttachmentsForContext(step) + .toList(), + ), + ); + } + + Future _onBeforeStepRun({ + required World world, + required String step, + required Iterable multiLineStrings, + GherkinTable? table, + }) async { + await hook.onBeforeStep(world, step); + await reporter.step.onStarted.invoke( + StepMessage( + name: step, + context: RunnableDebugInformation('', 0, step), + table: table, + multilineString: + multiLineStrings.isNotEmpty ? multiLineStrings.first : null, + ), + ); + } + + bool _evaluateTagFilterExpression( + String? tagExpression, + Iterable? tags, + ) { + return tagExpression == null || tagExpression.isEmpty + ? true + : _tagExpressionEvaluator.evaluate( + tagExpression, + tags!.toList(growable: false), + ); + } + + bool _isNegativeResult(StepExecutionResult result) { + return result == StepExecutionResult.error || + result == StepExecutionResult.fail || + result == StepExecutionResult.timeout; + } + + Future _appLifecyclePhasePumper( + AppLifecyclePhase phase, + WidgetTester tester, + ) async { + if (appLifecyclePumpHandler != null) { + await appLifecyclePumpHandler!(phase, tester); + } else { + await tester.pumpAndSettle(); + } + } +} diff --git a/lib/src/flutter/steps/given_i_open_the_drawer_step.dart b/lib/src/flutter/steps/given_i_open_the_drawer_step.dart index bc6b512..f1c5019 100644 --- a/lib/src/flutter/steps/given_i_open_the_drawer_step.dart +++ b/lib/src/flutter/steps/given_i_open_the_drawer_step.dart @@ -1,6 +1,5 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Opens the applications main drawer @@ -8,23 +7,34 @@ import 'package:gherkin/gherkin.dart'; /// Examples: /// /// `Given I open the drawer` -StepDefinitionGeneric GivenOpenDrawer() { +StepDefinitionGeneric givenOpenDrawer() { return given1( RegExp(r'I (open|close) the drawer'), (action, context) async { - final drawerFinder = find.byType('Drawer'); - final isOpen = await FlutterDriverUtils.isPresent( - context.world.driver, drawerFinder); + final drawerFinder = context.world.appDriver.findBy( + 'Drawer', + FindType.type, + ); + final isOpen = await context.world.appDriver.isPresent( + 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)); + await context.world.appDriver.scroll( + drawerFinder, + dx: -300.0, + dy: 0.0, + duration: const Duration(milliseconds: 300), + ); } else if (!isOpen && action == 'open') { - await FlutterDriverUtils.tap( - context.world.driver, - find.byTooltip('Open navigation menu'), - timeout: context.configuration?.timeout, + await context.world.appDriver.tap( + context.world.appDriver.findBy( + 'Open navigation menu', + FindType.tooltip, + ), + timeout: context.configuration.timeout, ); } }, diff --git a/lib/src/flutter/steps/restart_app_step.dart b/lib/src/flutter/steps/restart_app_step.dart index ecb924b..dfb51e2 100644 --- a/lib/src/flutter/steps/restart_app_step.dart +++ b/lib/src/flutter/steps/restart_app_step.dart @@ -1,12 +1,12 @@ import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -StepDefinitionGeneric RestartAppStep() { +StepDefinitionGeneric restartAppStep() { return given( 'I restart the app', (context) async { await context.world.restartApp( - timeout: context.configuration?.timeout, + timeout: context.configuration.timeout, ); }, ); diff --git a/lib/src/flutter/steps/sibling_contains_text_step.dart b/lib/src/flutter/steps/sibling_contains_text_step.dart index 11159a8..d71a3c8 100644 --- a/lib/src/flutter/steps/sibling_contains_text_step.dart +++ b/lib/src/flutter/steps/sibling_contains_text_step.dart @@ -1,6 +1,4 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Discovers a widget by its text within the same parent. @@ -13,26 +11,25 @@ import 'package:gherkin/gherkin.dart'; /// Examples: /// /// `Then I expect a "Row" that contains the text "X" to also contain the text "Y"` -StepDefinitionGeneric SiblingContainsTextStep() { +StepDefinitionGeneric siblingContainsTextStep() { return given3( 'I expect a {string} that contains the text {string} to also contain the text {string}', (ancestorType, leadingText, valueText, context) async { - final ancestor = await find.ancestor( - of: find.text(leadingText), - matching: find.byType(ancestorType), + final ancestor = await context.world.appDriver.findByAncestor( + context.world.appDriver.findBy(leadingText, FindType.text), + context.world.appDriver.findBy(ancestorType, FindType.type), firstMatchOnly: true, ); - final valueWidget = await find.descendant( - of: ancestor, - matching: find.text(valueText), + final valueWidget = await context.world.appDriver.findByDescendant( + ancestor, + context.world.appDriver.findBy(valueText, FindType.text), firstMatchOnly: true, ); - final isPresent = await FlutterDriverUtils.isPresent( - context.world.driver, + final isPresent = await context.world.appDriver.isPresent( valueWidget, - timeout: context.configuration?.timeout ?? const Duration(seconds: 20), + timeout: context.configuration.timeout ?? const Duration(seconds: 20), ); context.expect(isPresent, true); diff --git a/lib/src/flutter/steps/swipe_step.dart b/lib/src/flutter/steps/swipe_step.dart index 98f9ffe..43d312a 100644 --- a/lib/src/flutter/steps/swipe_step.dart +++ b/lib/src/flutter/steps/swipe_step.dart @@ -1,15 +1,14 @@ -import 'package:meta/meta.dart'; -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; import '../parameters/swipe_direction_parameter.dart'; mixin _SwipeHelper on When3WithWorld { - @protected Future swipeOnFinder( - SerializableFinder finder, + dynamic finder, SwipeDirection direction, int swipeAmount, ) async { @@ -17,22 +16,20 @@ mixin _SwipeHelper final offset = direction == SwipeDirection.right ? swipeAmount : (swipeAmount * -1); - await world.driver.scroll( + await world.appDriver.scroll( finder, - offset.toDouble(), - 0, - Duration(milliseconds: 500), + dx: offset.toDouble(), + duration: const Duration(milliseconds: 500), timeout: timeout, ); } else { final offset = direction == SwipeDirection.up ? swipeAmount : (swipeAmount * -1); - await world.driver.scroll( + await world.appDriver.scroll( finder, - 0, - offset.toDouble(), - Duration(milliseconds: 500), + dy: offset.toDouble(), + duration: const Duration(milliseconds: 500), timeout: timeout, ); } @@ -54,13 +51,13 @@ class SwipeOnKeyStep int swipeAmount, String key, ) async { - final finder = find.byValueKey(key); + final finder = world.appDriver.findBy(key, FindType.key); await swipeOnFinder(finder, direction, swipeAmount); } @override RegExp get pattern => - RegExp(r'I swipe {swipe_direction} by {int} pixels on the {string}$'); + RegExp(r'I swipe {swipe_direction} by {int} pixels on the {string}'); } /// Swipes in a cardinal direction on a widget discovered by its test. @@ -77,7 +74,7 @@ class SwipeOnTextStep int swipeAmount, String text, ) async { - final finder = find.text(text); + final finder = world.appDriver.findBy(text, FindType.text); await swipeOnFinder(finder, direction, swipeAmount); } diff --git a/lib/src/flutter/steps/take_a_screenshot_step.dart b/lib/src/flutter/steps/take_a_screenshot_step.dart new file mode 100644 index 0000000..a5467fd --- /dev/null +++ b/lib/src/flutter/steps/take_a_screenshot_step.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +StepDefinitionGeneric takeScreenshot() { + return given1( + 'I take a screenshot called {String}', + (name, context) async { + final bytes = await context.world.appDriver.screenshot( + screenshotName: name, + ); + + context.world.attach(base64Encode(bytes), 'image/png'); + }, + ); +} diff --git a/lib/src/flutter/steps/tap_text_within_widget_step.dart b/lib/src/flutter/steps/tap_text_within_widget_step.dart index 6c8506f..7d51084 100644 --- a/lib/src/flutter/steps/tap_text_within_widget_step.dart +++ b/lib/src/flutter/steps/tap_text_within_widget_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -8,36 +7,36 @@ import 'package:gherkin/gherkin.dart'; /// Examples: /// /// `Then I tap the label that contains the text "Logout" within the "user_settings_list"` -StepDefinitionGeneric TapTextWithinWidgetStep() { +StepDefinitionGeneric tapTextWithinWidgetStep() { return given2( RegExp( r'I tap the (?:button|element|label|field|text|widget) that contains the text {string} within the {string}'), (text, ancestorKey, context) async { final timeout = - context.configuration?.timeout ?? const Duration(seconds: 20); - final finder = find.descendant( - of: find.byValueKey(ancestorKey), - matching: find.text(text), + context.configuration.timeout ?? const Duration(seconds: 20); + final finder = context.world.appDriver.findByDescendant( + context.world.appDriver.findBy(ancestorKey, FindType.key), + context.world.appDriver.findBy(text, FindType.text), firstMatchOnly: true, ); - final isPresent = await FlutterDriverUtils.isPresent( - context.world.driver, + final isPresent = await context.world.appDriver.isPresent( finder, timeout: timeout * .2, ); if (!isPresent) { - await context.world.driver.scrollUntilVisible( - find.byValueKey(ancestorKey), - find.text(text), - dyScroll: -100.0, + await context.world.appDriver.scrollUntilVisible( + context.world.appDriver.findByDescendant( + context.world.appDriver.findBy(ancestorKey, FindType.key), + context.world.appDriver.findBy(text, FindType.text), + ), + dy: -100.0, timeout: timeout * .9, ); } - await FlutterDriverUtils.tap( - context.world.driver, + await context.world.appDriver.tap( finder, timeout: timeout, ); diff --git a/lib/src/flutter/steps/tap_widget_of_type_step.dart b/lib/src/flutter/steps/tap_widget_of_type_step.dart index d5d2849..1832a37 100644 --- a/lib/src/flutter/steps/tap_widget_of_type_step.dart +++ b/lib/src/flutter/steps/tap_widget_of_type_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -9,14 +8,16 @@ import 'package:gherkin/gherkin.dart'; /// `Then I tap the element of type "MaterialButton"` /// `Then I tap the label of type "ListTile"` /// `Then I tap the field of type "TextField"` -StepDefinitionGeneric TapWidgetOfTypeStep() { +StepDefinitionGeneric tapWidgetOfTypeStep() { return given1( RegExp( r'I tap the (?:button|element|label|icon|field|text|widget) of type {string}$'), (input1, context) async { - await FlutterDriverUtils.tap( - context.world.driver, - find.byType(input1), + await context.world.appDriver.tap( + context.world.appDriver.findBy( + input1, + FindType.type, + ), ); }, ); diff --git a/lib/src/flutter/steps/tap_widget_of_type_within_step.dart b/lib/src/flutter/steps/tap_widget_of_type_within_step.dart index e3b5abe..d77953a 100644 --- a/lib/src/flutter/steps/tap_widget_of_type_within_step.dart +++ b/lib/src/flutter/steps/tap_widget_of_type_within_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -7,20 +6,17 @@ import 'package:gherkin/gherkin.dart'; /// Examples: /// /// `Then I tap the element of type "MaterialButton" within the "user_settings_list"` -StepDefinitionGeneric TapWidgetOfTypeWithinStep() { +StepDefinitionGeneric tapWidgetOfTypeWithinStep() { return when2( RegExp( r'I tap the (?:button|element|label|icon|field|text|widget) of type {string} within the {string}$'), (widgetType, ancestorKey, context) async { - final finder = find.descendant( - of: find.byValueKey(ancestorKey), - matching: find.byType(widgetType), + final finder = context.world.appDriver.findByDescendant( + context.world.appDriver.findBy(ancestorKey, FindType.key), + context.world.appDriver.findBy(widgetType, FindType.type), firstMatchOnly: true, ); - await FlutterDriverUtils.tap( - context.world.driver, - finder, - ); + await context.world.appDriver.tap(finder); }, ); } diff --git a/lib/src/flutter/steps/tap_widget_with_text_step.dart b/lib/src/flutter/steps/tap_widget_with_text_step.dart index 394a664..3a44fe9 100644 --- a/lib/src/flutter/steps/tap_widget_with_text_step.dart +++ b/lib/src/flutter/steps/tap_widget_with_text_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -9,17 +8,14 @@ import 'package:gherkin/gherkin.dart'; /// `Then I tap the label that contains the text "Logout"` /// `Then I tap the button that contains the text "Sign up"` /// `Then I tap the widget that contains the text "My User Profile"` -StepDefinitionGeneric TapWidgetWithTextStep() { +StepDefinitionGeneric tapWidgetWithTextStep() { return then1( RegExp( r'I tap the (?:button|element|label|field|text|widget) that contains the text {string}$'), (input1, context) async { - final finder = find.text(input1); - await context.world.driver.scrollIntoView(finder); - await FlutterDriverUtils.tap( - context.world.driver, - finder, - ); + final finder = context.world.appDriver.findBy(input1, FindType.text); + await context.world.appDriver.scrollIntoView(finder); + await context.world.appDriver.tap(finder); }, ); } diff --git a/lib/src/flutter/steps/text_exists_step.dart b/lib/src/flutter/steps/text_exists_step.dart index 6ccc080..c1d9d59 100644 --- a/lib/src/flutter/steps/text_exists_step.dart +++ b/lib/src/flutter/steps/text_exists_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -9,22 +8,20 @@ import '../parameters/existence_parameter.dart'; /// Examples: /// /// `Then I expect the text "Logout" to be present` -/// `But I expect the text "Signup" to be absent` -StepDefinitionGeneric TextExistsStep() { +/// `But I expect the text "Sign up" to be absent` +StepDefinitionGeneric textExistsStep() { return then2( RegExp(r'I expect the text {string} to be {existence}$'), (text, exists, context) async { if (exists == Existence.present) { - final isPresent = await FlutterDriverUtils.isPresent( - context.world.driver, - find.text(text), + final isPresent = await context.world.appDriver.isPresent( + context.world.appDriver.findBy(text, FindType.text), ); context.expect(isPresent, true); } else { - final isAbsent = await FlutterDriverUtils.isAbsent( - context.world.driver, - find.text(text), + final isAbsent = await context.world.appDriver.isAbsent( + context.world.appDriver.findBy(text, FindType.text), ); context.expect(isAbsent, true); } diff --git a/lib/src/flutter/steps/text_exists_within_step.dart b/lib/src/flutter/steps/text_exists_within_step.dart index 9a34698..bd9cda2 100644 --- a/lib/src/flutter/steps/text_exists_within_step.dart +++ b/lib/src/flutter/steps/text_exists_within_step.dart @@ -1,4 +1,3 @@ -import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; @@ -9,20 +8,19 @@ import '../parameters/existence_parameter.dart'; /// Examples: /// /// `Then I expect the text "Logout" to be present within the "user_settings_list"` -/// `But I expect the text "Signup" to be absent within the "login_screen"` -StepDefinitionGeneric TextExistsWithinStep() { +/// `But I expect the text "Sign up" to be absent within the "login_screen"` +StepDefinitionGeneric textExistsWithinStep() { return then3( RegExp( r'I expect the text {string} to be {existence} within the {string}$'), (text, exists, ancestorKey, context) async { - final finder = find.descendant( - of: find.byValueKey(ancestorKey), - matching: find.text(text), + final finder = context.world.appDriver.findByDescendant( + context.world.appDriver.findBy(ancestorKey, FindType.key), + context.world.appDriver.findBy(text, FindType.text), firstMatchOnly: true, ); - final isPresent = await FlutterDriverUtils.isPresent( - context.world.driver, + final isPresent = await context.world.appDriver.isPresent( finder, ); diff --git a/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart b/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart index b21ea91..2547f2f 100644 --- a/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart +++ b/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart @@ -1,6 +1,5 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Expects the element found with the given control key to have the given string value. @@ -13,20 +12,19 @@ import 'package:gherkin/gherkin.dart'; /// /// `Then I expect the "controlKey" to be "Hello World"` /// `And I expect the "controlKey" to be "Hello World"` -StepDefinitionGeneric ThenExpectElementToHaveValue() { +StepDefinitionGeneric thenExpectElementToHaveValue() { return given2( 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), - ); + final finder = context.world.appDriver.findBy(key, FindType.key); + final text = await context.world.appDriver.getText(finder); + context.expect(text, value); } catch (e) { - await context.reporter.message('Step error: $e', MessageLevel.error); + // await context.reporter('Step error: $e', MessageLevel.error); rethrow; } }, ); -} \ No newline at end of file +} diff --git a/lib/src/flutter/steps/then_expect_widget_to_be_present_step.dart b/lib/src/flutter/steps/then_expect_widget_to_be_present_step.dart index f0c9557..1c86cdc 100644 --- a/lib/src/flutter/steps/then_expect_widget_to_be_present_step.dart +++ b/lib/src/flutter/steps/then_expect_widget_to_be_present_step.dart @@ -1,6 +1,5 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Expects a widget with the given key to be present within n seconds @@ -12,18 +11,23 @@ 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` -StepDefinitionGeneric ThenExpectWidgetToBePresent() { +StepDefinitionGeneric thenExpectWidgetToBePresent() { return given2( - 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); - }, - configuration: StepDefinitionConfiguration() - ..timeout = const Duration(days: 1)); + 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 { + await context.world.appDriver.waitUntil( + () async { + await context.world.appDriver.waitForAppToSettle(); + + return context.world.appDriver.isPresent( + context.world.appDriver.findBy(key, FindType.key), + ); + }, + timeout: Duration(seconds: seconds), + ); + }, + configuration: StepDefinitionConfiguration() + ..timeout = const Duration(days: 1), + ); } diff --git a/lib/src/flutter/steps/wait_until_key_exists_step.dart b/lib/src/flutter/steps/wait_until_key_exists_step.dart index 3a65497..71c9dcf 100644 --- a/lib/src/flutter/steps/wait_until_key_exists_step.dart +++ b/lib/src/flutter/steps/wait_until_key_exists_step.dart @@ -1,6 +1,5 @@ import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:flutter_driver/flutter_driver.dart'; import '../parameters/existence_parameter.dart'; @@ -10,21 +9,20 @@ import '../parameters/existence_parameter.dart'; /// /// `Then I wait until the "login_loading_indicator" is absent` /// `And I wait until the "login_screen" is present` -StepDefinitionGeneric WaitUntilKeyExistsStep() { +StepDefinitionGeneric waitUntilKeyExistsStep() { return then2( 'I wait until the {string} is {existence}', (keyString, existence, context) async { - await FlutterDriverUtils.waitUntil( - context.world.driver, - () { + await context.world.appDriver.waitUntil( + () async { + await context.world.appDriver.waitForAppToSettle(); + return existence == Existence.absent - ? FlutterDriverUtils.isAbsent( - context.world.driver, - find.byValueKey(keyString), + ? context.world.appDriver.isAbsent( + context.world.appDriver.findBy(keyString, FindType.key), ) - : FlutterDriverUtils.isPresent( - context.world.driver, - find.byValueKey(keyString), + : context.world.appDriver.isPresent( + context.world.appDriver.findBy(keyString, FindType.key), ); }, ); diff --git a/lib/src/flutter/steps/wait_until_type_exists_step.dart b/lib/src/flutter/steps/wait_until_type_exists_step.dart index 9df53d3..540b215 100644 --- a/lib/src/flutter/steps/wait_until_type_exists_step.dart +++ b/lib/src/flutter/steps/wait_until_type_exists_step.dart @@ -1,6 +1,5 @@ import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; -import 'package:flutter_driver/flutter_driver.dart'; import '../parameters/existence_parameter.dart'; @@ -10,21 +9,20 @@ import '../parameters/existence_parameter.dart'; /// /// `Then I wait until the element of type "ProgressIndicator" is absent` /// `And I wait until the button of type the "MaterialButton" is present` -StepDefinitionGeneric WaitUntilTypeExistsStep() { +StepDefinitionGeneric waitUntilTypeExistsStep() { return then2( 'I wait until the (?:button|element|label|icon|field|text|widget) of type {string} is {existence}', (ofType, existence, context) async { - await FlutterDriverUtils.waitUntil( - context.world.driver, - () { + await context.world.appDriver.waitUntil( + () async { + await context.world.appDriver.waitForAppToSettle(); + return existence == Existence.absent - ? FlutterDriverUtils.isAbsent( - context.world.driver, - find.byType(ofType), + ? context.world.appDriver.isAbsent( + context.world.appDriver.findBy(ofType, FindType.type), ) - : FlutterDriverUtils.isPresent( - context.world.driver, - find.byType(ofType), + : context.world.appDriver.isPresent( + context.world.appDriver.findBy(ofType, FindType.type), ); }, ); diff --git a/lib/src/flutter/steps/when_fill_field_step.dart b/lib/src/flutter/steps/when_fill_field_step.dart index 30f744f..9cbb62c 100644 --- a/lib/src/flutter/steps/when_fill_field_step.dart +++ b/lib/src/flutter/steps/when_fill_field_step.dart @@ -1,6 +1,5 @@ -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/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Enters the given text into the widget with the key provided @@ -8,14 +7,13 @@ 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" -StepDefinitionGeneric WhenFillFieldStep() { +StepDefinitionGeneric whenFillFieldStep() { return given2( '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, + final finder = context.world.appDriver.findBy(key, FindType.key); + await context.world.appDriver.scrollIntoView(finder); + await context.world.appDriver.enterText( finder, value, ); diff --git a/lib/src/flutter/steps/when_long_press_widget_step.dart b/lib/src/flutter/steps/when_long_press_widget_step.dart index 74adfea..f55958b 100644 --- a/lib/src/flutter/steps/when_long_press_widget_step.dart +++ b/lib/src/flutter/steps/when_long_press_widget_step.dart @@ -1,6 +1,5 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Long presses the widget found with the given control key. @@ -17,34 +16,29 @@ import 'package:gherkin/gherkin.dart'; /// `When I long press "controlKey" field` /// `When I long press "controlKey" text` /// `When I long press "controlKey" widget` -StepDefinitionGeneric WhenLongPressWidget() { +StepDefinitionGeneric whenLongPressWidget() { return when1( RegExp( r'I long press the {string} (?:button|element|label|icon|field|text|widget)$'), (key, context) async { - final finder = find.byValueKey(key); + final finder = context.world.appDriver.findBy(key, FindType.key); - await context.world.driver.scrollIntoView( - finder, - ); - await FlutterDriverUtils.longPress( - context.world.driver, - finder, - ); + await context.world.appDriver.scrollIntoView(finder); + await context.world.appDriver.longPress(finder); + await context.world.appDriver.waitForAppToSettle(); }, ); } /// Long presses the widget found with the given control key, without scrolling into view -StepDefinitionGeneric WhenLongPressWidgetWithoutScroll() { +StepDefinitionGeneric whenLongPressWidgetWithoutScroll() { return when1( RegExp( r'I long press the {string} (?:button|element|label|icon|field|text|widget) without scrolling it into view$'), (key, context) async { - final finder = find.byValueKey(key); + final finder = context.world.appDriver.findBy(key, FindType.key); - await FlutterDriverUtils.longPress( - context.world.driver, + await context.world.appDriver.longPress( finder, ); }, @@ -52,18 +46,17 @@ StepDefinitionGeneric WhenLongPressWidgetWithoutScroll() { } /// Long presses the widget found with the given control key, for the given duration -StepDefinitionGeneric WhenLongPressWidgetForDuration() { +StepDefinitionGeneric whenLongPressWidgetForDuration() { return when2( RegExp( r'I long press the {string} (?:button|element|label|icon|field|text|widget) for {int} milliseconds$'), (key, milliseconds, context) async { - final finder = find.byValueKey(key); + final finder = context.world.appDriver.findBy(key, FindType.key); - await context.world.driver.scrollIntoView( + await context.world.appDriver.scrollIntoView( finder, ); - await FlutterDriverUtils.longPress( - context.world.driver, + await context.world.appDriver.longPress( finder, pressDuration: Duration(milliseconds: milliseconds), ); diff --git a/lib/src/flutter/steps/when_pause_step.dart b/lib/src/flutter/steps/when_pause_step.dart index c19d987..0e9d392 100644 --- a/lib/src/flutter/steps/when_pause_step.dart +++ b/lib/src/flutter/steps/when_pause_step.dart @@ -1,3 +1,4 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; import 'package:gherkin/gherkin.dart'; /// Pauses the execution for the provided number of seconds. @@ -7,11 +8,15 @@ import 'package:gherkin/gherkin.dart'; /// Examples: /// When I pause for 10 seconds /// When I pause for 120 seconds -StepDefinitionGeneric WhenPauseStep() { - return when1( - 'I pause for {int} second(s)', - (wait, _) async { +StepDefinitionGeneric whenPauseStep() { + return when1( + 'I (?:pause|wait) for {int} second(?:s)?', + (wait, context) async { await Future.delayed(Duration(seconds: wait)); + await context.world.appDriver.waitForAppToSettle(); }, + configuration: StepDefinitionConfiguration() + // add a large timeout here, I think 15 is more than enough + ..timeout = const Duration(minutes: 15), ); } diff --git a/lib/src/flutter/steps/when_tap_the_back_button_step.dart b/lib/src/flutter/steps/when_tap_the_back_button_step.dart index 144fb34..4f4846c 100644 --- a/lib/src/flutter/steps/when_tap_the_back_button_step.dart +++ b/lib/src/flutter/steps/when_tap_the_back_button_step.dart @@ -1,6 +1,4 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Taps the back button widget @@ -10,14 +8,11 @@ import 'package:gherkin/gherkin.dart'; /// `When I tap the back button"` /// `When I tap the back element"` /// `When I tap the back widget"` -StepDefinitionGeneric WhenTapBackButtonWidget() { +StepDefinitionGeneric whenTapBackButtonWidget() { return when( RegExp(r'I tap the back (?:button|element|widget|icon|text)$'), (context) async { - await FlutterDriverUtils.tap( - context.world.driver, - find.pageBack(), - ); + await context.world.appDriver.pageBack(); }, ); } diff --git a/lib/src/flutter/steps/when_tap_widget_step.dart b/lib/src/flutter/steps/when_tap_widget_step.dart index 4f5d5c8..213780e 100644 --- a/lib/src/flutter/steps/when_tap_widget_step.dart +++ b/lib/src/flutter/steps/when_tap_widget_step.dart @@ -1,6 +1,5 @@ -import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; -import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/src/flutter/adapters/app_driver_adapter.dart'; +import 'package:flutter_gherkin/src/flutter/world/flutter_world.dart'; import 'package:gherkin/gherkin.dart'; /// Taps the widget found with the given control key. @@ -17,33 +16,33 @@ import 'package:gherkin/gherkin.dart'; /// `When I tap "controlKey" field"` /// `When I tap "controlKey" text"` /// `When I tap "controlKey" widget"` -StepDefinitionGeneric WhenTapWidget() { +StepDefinitionGeneric whenTapWidget() { return when1( RegExp( r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'), (key, context) async { - final finder = find.byValueKey(key); + final finder = context.world.appDriver.findBy(key, FindType.key); - await context.world.driver.scrollIntoView( + await context.world.appDriver.scrollIntoView( finder, ); - await FlutterDriverUtils.tap( - context.world.driver, + await context.world.appDriver.tap( finder, + timeout: context.configuration.timeout, ); }, ); } -StepDefinitionGeneric WhenTapWidgetWithoutScroll() { +StepDefinitionGeneric whenTapWidgetWithoutScroll() { return when1( 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); + final finder = + context.world.appDriver.findByDescendant(key, FindType.key); - await FlutterDriverUtils.tap( - context.world.driver, + await context.world.appDriver.tap( finder, ); }, diff --git a/lib/src/flutter/utils/driver_utils.dart b/lib/src/flutter/utils/driver_utils.dart deleted file mode 100644 index 4f23dd0..0000000 --- a/lib/src/flutter/utils/driver_utils.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_driver/flutter_driver.dart'; - -class FlutterDriverUtils { - static Future isPresent( - FlutterDriver driver, - SerializableFinder finder, { - Duration timeout = const Duration(seconds: 1), - }) async { - try { - await driver.waitFor(finder, timeout: timeout); - return true; - } catch (_) { - return false; - } - } - - static Future isAbsent( - FlutterDriver driver, - SerializableFinder finder, { - Duration timeout = const Duration(seconds: 30), - }) async { - try { - await driver.waitForAbsent(finder, timeout: timeout); - return true; - } catch (_) { - return false; - } - } - - static Future waitForFlutter( - FlutterDriver driver, { - Duration timeout = const Duration(seconds: 30), - }) async { - try { - await driver.waitUntilNoTransientCallbacks(timeout: timeout); - return true; - } catch (_) { - return false; - } - } - - static Future enterText( - FlutterDriver driver, - SerializableFinder finder, - String text, { - Duration timeout = const Duration(seconds: 30), - }) async { - await FlutterDriverUtils.tap(driver, finder, timeout: timeout); - await driver.enterText(text, timeout: timeout); - } - - static Future getText( - FlutterDriver driver, - SerializableFinder finder, { - Duration timeout = const Duration(seconds: 30), - }) async { - await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout); - final text = await driver.getText(finder, timeout: timeout); - return text; - } - - static Future tap( - FlutterDriver driver, - SerializableFinder finder, { - Duration timeout = const Duration(seconds: 30), - }) async { - await driver.tap(finder, timeout: timeout); - await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout); - } - - static Future longPress( - FlutterDriver driver, - SerializableFinder finder, { - Duration pressDuration = const Duration(milliseconds: 500), - Duration timeout = const Duration(seconds: 30), - }) async { - await driver.scroll(finder, 0, 0, pressDuration, timeout: timeout); - await FlutterDriverUtils.waitForFlutter(driver, timeout: timeout); - } - - /// Waits until the [condition] returns true - /// Will raise a complete with a [TimeoutException] if the - /// condition does not return true with the timeout period. - static Future waitUntil( - FlutterDriver driver, - Future Function() condition, { - Duration timeout = const Duration(seconds: 10), - Duration pollInterval = const Duration(milliseconds: 500), - }) async { - return Future.microtask(() async { - final completer = Completer(); - var maxAttempts = - (timeout.inMilliseconds / pollInterval.inMilliseconds).round(); - var attempts = 0; - - while (attempts < maxAttempts) { - final result = await condition(); - if (result) { - completer.complete(); - break; - } else { - await Future.delayed(pollInterval); - } - } - }).timeout(timeout); - } -} diff --git a/lib/src/flutter/world/flutter_driver_world.dart b/lib/src/flutter/world/flutter_driver_world.dart new file mode 100644 index 0000000..d904f40 --- /dev/null +++ b/lib/src/flutter/world/flutter_driver_world.dart @@ -0,0 +1,57 @@ +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart'; + +import '../runners/flutter_run_process_handler.dart'; + +/// Driver version of the FlutterWorld with a typed driver +class FlutterDriverWorld extends FlutterTypedAdapterWorld { + FlutterRunProcessHandler? _flutterRunProcessHandler; + + void setFlutterDriver(FlutterDriver flutterDriver) { + setAppAdapter(FlutterDriverAppDriverAdapter(flutterDriver)); + } + + void setFlutterProcessHandler( + FlutterRunProcessHandler flutterRunProcessHandler, + ) { + _flutterRunProcessHandler = flutterRunProcessHandler; + } + + @override + Future restartApp({ + Duration? timeout = const Duration(seconds: 60), + }) async { + await _closeDriver(timeout: timeout); + final result = await _flutterRunProcessHandler?.restart( + timeout: timeout, + ); + + final driver = await FlutterDriver.connect( + dartVmServiceUrl: _flutterRunProcessHandler!.currentObservatoryUri, + ); + + setFlutterDriver(driver); + + return result!; + } + + @override + void dispose() async { + super.dispose(); + appDriver.dispose(); + _flutterRunProcessHandler = null; + await _closeDriver(timeout: const Duration(seconds: 5)); + } + + Future _closeDriver({ + Duration? timeout = const Duration(seconds: 60), + }) async { + await rawAppDriver.close().catchError( + (e, st) { + // Avoid an unhandled error + return null; + }, + ); + } +} diff --git a/lib/src/flutter/world/flutter_widget_tester_world.dart b/lib/src/flutter/world/flutter_widget_tester_world.dart new file mode 100644 index 0000000..a3125dc --- /dev/null +++ b/lib/src/flutter/world/flutter_widget_tester_world.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +/// The world object that can be used to store state during a single test. +/// It also allows interaction with the app under test through the `appDriver` +/// which exposes an instance of `AppDriverAdapter` and an +/// instance of `WidgetTester` via the property `rawAppDriver` +class FlutterWidgetTesterWorld + extends FlutterTypedAdapterWorld {} diff --git a/lib/src/flutter/world/flutter_world.dart b/lib/src/flutter/world/flutter_world.dart new file mode 100644 index 0000000..c10ea65 --- /dev/null +++ b/lib/src/flutter/world/flutter_world.dart @@ -0,0 +1,43 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +import '../adapters/app_driver_adapter.dart'; + +/// The world object that can be used to store state during a single test. +/// It also allows interaction with the app under test through the `appDriver` +/// which exposes an instance of `AppDriverAdapter` +class FlutterWorld extends World { + AppDriverAdapter? _adapter; + + /// The adapter that is used to agnostically drive the app under test + AppDriverAdapter get appDriver => _adapter!; + + /// Sets the app driver that is used to control the app under test + void setAppAdapter(AppDriverAdapter appAdapter) { + _adapter = appAdapter; + } + + /// Restart the app under test + Future restartApp({ + Duration? timeout = const Duration(seconds: 60), + }) { + throw UnimplementedError('Unable to restart the app during the test'); + } +} + +/// The world object that can be used to store state during a single test. +/// It also allows interaction with the app under test through the `appDriver` +/// which exposes an instance of `AppDriverAdapter` and a typed instance of `TDriver` +/// of the actual class that is able to interact with the app under test +class FlutterTypedAdapterWorld extends FlutterWorld { + /// The underlying driver that is able to instrument the app under test + /// It is suggested you use `appDriver` for all interactions with the app under tests + /// however if you need a specific api not available on `appDriver` this property + /// exposes the actual class that can interact with the app under test + TDriver get rawAppDriver => _adapter!.nativeDriver as TDriver; + + /// The adapter that is used to agnostically drive the app under test + @override + AppDriverAdapter get appDriver => + _adapter as AppDriverAdapter; +} diff --git a/pre-publish-checks.cmd b/pre-publish-checks.cmd index 7c5a0b2..f29a6d9 100644 --- a/pre-publish-checks.cmd +++ b/pre-publish-checks.cmd @@ -1,4 +1,3 @@ -CALL "C:\Google\flutter\bin\cache\dart-sdk\bin\dartanalyzer" --options analysis_options.yaml . -CALL "C:\Google\flutter\bin\cache\dart-sdk\bin\dartfmt" . -w -CALL flutter packages upgrade +CALL flutter analyze +CALL dart format . --fix CALL flutter test \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 5685121..66577f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,112 +7,154 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "12.0.0" + version: "39.0.0" analyzer: - dependency: transitive + dependency: "direct main" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.40.6" + version: "4.0.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "3.1.11" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.3.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" + build: + dependency: "direct main" + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: "direct dev" + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" - cli_util: + version: "1.3.1" + checked_yaml: dependency: transitive description: - name: cli_util + name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" - coverage: + version: "3.0.2" + crypto: dependency: transitive description: - name: coverage + name: crypto url: "https://pub.dartlang.org" source: hosted - version: "0.14.2" - crypto: + version: "3.0.1" + csslib: dependency: transitive description: - name: crypto + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "4.16.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.3.0" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.0.0-nullsafety.2" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -123,6 +165,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" flutter_test: dependency: "direct main" description: flutter @@ -139,327 +188,241 @@ packages: name: gherkin url: "https://pub.dartlang.org" source: hosted - version: "1.1.9" + version: "3.1.0" glob: dependency: "direct main" description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.2" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.0" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.4" - io: + version: "2.1.0" + html: dependency: transitive description: - name: io + name: html url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" - js: + version: "0.15.0" + integration_test: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + json_annotation: dependency: transitive description: - name: js + name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "0.6.3-nullsafety.2" - json_rpc_2: + version: "4.5.0" + lints: dependency: transitive description: - name: json_rpc_2 + name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.0.0" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" - meta: - dependency: "direct main" - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0-nullsafety.3" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7" - node_interop: + version: "0.12.11" + material_color_utilities: dependency: transitive description: - name: node_interop + name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" - node_io: - dependency: transitive - description: - name: node_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - node_preamble: - dependency: transitive + version: "0.1.4" + meta: + dependency: "direct dev" description: - name: node_preamble + name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.4.12" + version: "1.7.0" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.8.1" pedantic: dependency: "direct dev" description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.2" - platform: + version: "1.11.1" + petitparser: dependency: transitive description: - name: platform + name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.2" - pool: + version: "5.0.0" + platform: dependency: transitive description: - name: pool + name: platform url: "https://pub.dartlang.org" source: hosted - version: "1.5.0-nullsafety.2" + version: "3.1.0" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.0.0-nullsafety.2" + version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.9" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - shelf_static: - dependency: transitive - description: - name: shelf_static - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.8" - shelf_web_socket: + version: "2.1.1" + pubspec_parse: dependency: transitive description: - name: shelf_web_socket + name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "1.2.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0-nullsafety.3" - source_maps: - dependency: transitive + source_gen: + dependency: "direct main" description: - name: source_maps + name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.10.10-nullsafety.2" + version: "1.2.2" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.2" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" sync_http: dependency: transitive description: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" - test: - dependency: "direct dev" - description: - name: test - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0-nullsafety.5" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" - test_core: + version: "0.4.9" + typed_data: dependency: transitive description: - name: test_core + name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "0.3.12-nullsafety.5" - typed_data: + version: "1.3.0" + uuid: dependency: transitive description: - name: typed_data + name: uuid url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "3.0.6" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" - vm_service_client: - dependency: transitive - description: - name: vm_service_client - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6+2" + version: "8.2.2" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" + version: "1.0.1" webdriver: dependency: transitive description: name: webdriver url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" - webkit_inspection_protocol: + version: "3.0.0" + xml: dependency: transitive description: - name: webkit_inspection_protocol + name: xml url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "6.1.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.1" sdks: - dart: ">=2.10.0-110 <2.11.0" - flutter: ">=1.13.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5ec1f33..ecf9e26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,27 +1,33 @@ name: flutter_gherkin description: A Gherkin / Cucumber parser and test runner for Dart and Flutter -version: 1.1.9 +version: 3.0.0-rc.17 homepage: https://github.com/jonsamwell/flutter_gherkin environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.13.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=2.2.0" dependencies: flutter: sdk: flutter flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_driver: sdk: flutter - glob: ^1.1.7 - meta: ">=1.1.6 <2.0.0" - gherkin: ^1.1.9 - # gherkin: - # path: ../dart_gherkin + analyzer: '>=2.1.0 < 6.0.0' + collection: ^1.15.0 + gherkin: ^3.1.0 + source_gen: ^1.1.1 + build: ^2.1.1 + glob: ^2.0.2 dev_dependencies: - test: - pedantic: ^1.9.2 + meta: '>=1.7.0 < 2.0.0' + pedantic: ^1.11.1 + build_config: ^1.0.0 + flutter_lints: ^2.0.1 + dart_code_metrics: ^4.16.0 flutter: diff --git a/test/flutter_configuration_test.dart b/test/flutter_configuration_test.dart index d979ab2..c51978b 100644 --- a/test/flutter_configuration_test.dart +++ b/test/flutter_configuration_test.dart @@ -1,49 +1,42 @@ -import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart'; import 'package:flutter_gherkin/src/flutter/hooks/app_runner_hook.dart'; -import 'package:test/test.dart'; - +import 'package:flutter_test/flutter_test.dart'; import 'mocks/parameter_mock.dart'; import 'mocks/step_definition_mock.dart'; void main() { group('config', () { - group('prepare', () { - test('flutter app runner hook added', () { - final config = FlutterTestConfiguration(); - expect(config.hooks, isNull); - config.prepare(); - expect(config.hooks, isNotNull); - expect(config.hooks.length, 1); - expect(config.hooks.elementAt(0), (x) => x is FlutterAppRunnerHook); - }); + test('flutter app runner hook added', () { + final config = FlutterDriverTestConfiguration(); + final newConfig = config.prepare(); - test('common steps definition added', () { - final config = FlutterTestConfiguration(); - expect(config.stepDefinitions, isNull); + expect(newConfig.hooks, isNotNull); + expect(newConfig.hooks!.length, 1); + expect(newConfig.hooks!.elementAt(0), (x) => x is FlutterAppRunnerHook); + }); - config.prepare(); - expect(config.stepDefinitions, isNotNull); - expect(config.stepDefinitions.length, 23); - expect(config.customStepParameterDefinitions, isNotNull); - expect(config.customStepParameterDefinitions.length, 2); - }); + test('common steps definition added', () { + final config = FlutterDriverTestConfiguration(); + expect(config.stepDefinitions, isNotNull); + expect(config.stepDefinitions!.length, 24); + expect(config.customStepParameterDefinitions, isNotNull); + expect(config.customStepParameterDefinitions!.length, 2); + }); - test('common step definition added to existing steps', () { - final config = FlutterTestConfiguration() - ..stepDefinitions = [MockStepDefinition()] - ..customStepParameterDefinitions = [MockParameter()]; - expect(config.stepDefinitions.length, 1); + test('common step definition added to existing steps', () { + final config = FlutterTestConfiguration( + stepDefinitions: [MockStepDefinition()], + customStepParameterDefinitions: [MockParameter()], + ); - config.prepare(); - expect(config.stepDefinitions, isNotNull); - expect(config.stepDefinitions.length, 24); - expect(config.stepDefinitions.elementAt(0), - (x) => x is MockStepDefinition); - expect(config.customStepParameterDefinitions, isNotNull); - expect(config.customStepParameterDefinitions.length, 3); - expect(config.customStepParameterDefinitions.elementAt(0), - (x) => x is MockParameter); - }); + expect(config.stepDefinitions, isNotNull); + expect(config.stepDefinitions!.length, 25); + expect( + config.stepDefinitions!.elementAt(0), (x) => x is MockStepDefinition); + expect(config.customStepParameterDefinitions, isNotNull); + expect(config.customStepParameterDefinitions!.length, 3); + expect(config.customStepParameterDefinitions!.elementAt(0), + (x) => x is MockParameter); }); }); } diff --git a/test/mocks/step_definition_mock.dart b/test/mocks/step_definition_mock.dart index 820c76f..c1197fa 100644 --- a/test/mocks/step_definition_mock.dart +++ b/test/mocks/step_definition_mock.dart @@ -5,7 +5,7 @@ typedef OnRunCode = Future Function(Iterable parameters); class MockStepDefinition extends StepDefinitionBase { bool hasRun = false; int runCount = 0; - final OnRunCode code; + final OnRunCode? code; MockStepDefinition([this.code, int expectedParameterCount = 0]) : super(null, expectedParameterCount); @@ -15,10 +15,10 @@ class MockStepDefinition extends StepDefinitionBase { hasRun = true; runCount += 1; if (code != null) { - await code(parameters); + await code!(parameters); } } @override - RegExp get pattern => null; + RegExp get pattern => RegExp(''); }