commit 23055b409224a457be84329713b7523d27a65ddb Author: Jon Samwell Date: Fri Oct 26 21:09:22 2018 +1100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..446ed0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +ios/.generated/ +ios/Flutter/Generated.xcconfig +ios/Runner/GeneratedPluginRegistrant.* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a0b391c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "request": "launch", + "type": "dart" + }, + { + "name": "Debug example", + "request": "launch", + "type": "dart", + "program": "example/test_driver/app_test.dart", + "flutterMode": "debug" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4e742db --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "launch", + "type": "shell", + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "options": { + "cwd": "${workspaceFolder}/example" + }, + "command": "flutter run --target=test_driver/app.dart", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc74a6e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..154699a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jonathan Samwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6b06c8 --- /dev/null +++ b/README.md @@ -0,0 +1,425 @@ +# flutter_gherkin + +A fully features Gherkin parser and test runner. Works with Flutter and Dart 2. + +This implementation of the Gherkin tries to follow as closely as possible other implementations of Gherkin and specifically [Cucumber](https://docs.cucumber.io/cucumber/) in it's various forms. + +```dart + # Comment + @tag + Feature: Eating too many cucumbers may not be good for you + + Eating too much of anything may not be good for you. + + Scenario: Eating a few is no problem + Given Alice is hungry + When she eats 3 cucumbers + Then she will be full +``` + +## Table of Contents + + + +- [flutter_gherkin](#flutter_gherkin) + - [Table of Contents](#table-of-contents) + - [Getting Started](#getting-started) + - [Configuration](#configuration) + - [Features Files](#features-files) + - [Steps Definitions](#steps-definitions) + - [Given](#given) + - [Then](#then) + - [Timeouts](#timeouts) + - [Multiline Strings](#multiline-strings) + - [Data tables](#data-tables) + - [Well known step parameters](#well-known-step-parameters) + - [Pluralisation](#pluralisation) + - [Custom Parameters](#custom-parameters) + - [World Context (per test scenario shared state)](#world-context-per-test-scenario-shared-state) + - [Assertions](#assertions) + - [Tags](#tags) + - [Hooks](#hooks) + - [Reporting](#reporting) + - [Flutter](#flutter) + - [Flutter Specific Configuration](#flutter-specific-configuration) + - [Restarting the app before each test](#restarting-the-app-before-each-test) + - [Flutter World](#flutter-world) + - [Pre-defined Steps](#pre-defined-steps) + - [Debugging](#debugging) + + + +## Getting Started + +See for information on the Gherkin syntax and Behaviour Driven Development (BDD). + +To get started with BDD in flutter the first step is to write a feature file and a test scenario within that. + +First create a folder called `test_driver` (this is inline with the current integration test as we will need to use the Flutter driver to automate the app). Within the folder create a folder called `features`, then create a file called `counter.feature`. + +```dart +Feature: Counter + The counter should be incremented when the button is pressed. + + Scenario: Counter increases when the button is pressed + Given I expect the "counter" to be "0" + When I tap the "increment" button 10 times + Then I expect the "counter" to be "10" +``` + +Now we have created a scenario we need to implement the steps within. Steps are just classes that extends from the base step definition class or any of its variations `Given`, `Then`, `When`, `And`, `But`. + +Granted the example is a little contrived but is serves to illustrate the process. + +This library has a couple of built in step definitions for convenience. The first step uses the built in step, however the second step `When I tap the "increment" button 10 times` is a custom step and has to be implemented. To implement a step we have to create a simple class. + +```dart +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class TapButtonNTimesStep extends When2WithWorld { + @override + Future executeStep(String input1, int input2) async { + final locator = find.byValueKey(input1); + for (var i = 0; i < 10; i += 1) { + await world.driver.tap(locator); + } + } + + @override + RegExp get pattern => RegExp(r"I tap the {string} button {int} times"); +} +``` + +As you can see the class inherits from `When2WithWorld` and specifies the types of the two input parameters. The third type `FlutterWorld` is a special Flutter context object that allow access to the Flutter driver instance within the step. If you did not need this you could inherit from `When2` which does not type the world context object but still provides two input parameters. + +The input parameters are retrieved via the pattern regex from well know parameter types `{string}` and `{int}` [explained below](#well-known-step-parameters). They are just special syntax to indicate you are expecting a string and an integer at those points in the step text. Therefore, when the step to execute is `When I tap the "increment" button 10 times` the parameters "increment" and 10 will be passed into the step as the correct types. Note that in the pattern you can use any regex capture group to indicate any input parameter. For example the regex ```RegExp(r"When I tap the {string} (button|icon) {int} times")``` indicates 3 parameters and would match to either of the below step text. + +```dart +When I tap the "increment" button 10 times // passes 3 parameters "increment", "button" & 10 +When I tap the "increment" icon 2 times // passes 3 parameters "increment", "icon" & 2 +``` + +### Configuration + +## Features Files + +### Steps Definitions + +Step definitions are the coded representation of a textual step in a feature file. Each step starts with either `Given`, `Then`, `When`, `And` or `But`. It is worth noting that all steps are actually the same but semantically different. The keyword is not taken into account when matching a step. Therefore the two below steps are actually treated the same and will result in the same step definition being invoked. + +Note: Step definitions (in this implementation) are allowed up to 5 input parameters. If you find yourself needing more than this you might want to consider making your step more isolated or using a `Table` parameter. + +``` +Given there are 6 kangaroos +Then there are 6 kangaroos +``` + +However, the domain language you choose will influence what keyword works best in each context. For more information . + +#### Given + +`Given` steps are used to describe the initial state of a system. The execution of a `Given` step will usually put the system into well defined state. + +To implement a `Given` step you can inherit from the ```Given``` class. + +``` +Given Bob has logged in +``` + +Would be implemented like so: + +```dart +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class GivenWellKnownUserIsLoggedIn extends Given1 { + @override + Future executeStep(String wellKnownUsername) async { + // implement your code + } + + @override + RegExp get pattern => RegExp(r"(Bob|Mary|Emma|Jon) has logged in"); +} +``` + +If you need to have more than one Given in a block it is often best to use the additional keywords `And` or `But`. + +``` +Given Bob has logged in +And opened the dashboard +``` + +#### Then + +`Then` steps are used to describe an expected outcome, or result. They would typically have an assertion in which can pass or fail. + +``` +Then I expect 10 apples +``` + +Would be implemented like so: + +```dart +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class ThenExpectAppleCount extends Then1 { + @override + Future executeStep(int count) async { + // example code + final actualCount = await _getActualCount(); + expectMatch(actualCount, count); + } + + @override + RegExp get pattern => RegExp(r"I expect {int} apple(s)"); +} +``` + +**Caveat**: The `expect` library currently only works within the library's own `test` function blocks; so using it with a `Then` step will cause an error. Therefore, the `expectMatch` or `expectA` or `this.expect` methods have been added which mimic the underlying functionality of `except` in that they assert that the give is true. The `Matcher` within Dart's test library still work and can be used as expected. + +#### Timeouts + +By default a step will timeout if it exceed the `defaultTimeout` parameter in the configuration file. In some cases you want have a step that is longer or shorter running and in the case you can optionally proved a custom timeout to that step. To do this pass in a `Duration` object in the step's call to `super`. + +For example, the below sets the step's timeout to 10 seconds. + +```dart +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class TapButtonNTimesStep extends When2WithWorld { + TapButtonNTimesStep() + : super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10)); + + @override + Future executeStep(String input1, int input2) async { + final locator = find.byValueKey(input1); + for (var i = 0; i < 10; i += 1) { + await world.driver.tap(locator, timeout: timeout); + } + } + + @override + RegExp get pattern => RegExp(r"I tap the {string} button {int} times"); +} +``` + +#### Multiline Strings + +Mulitline strings can follow a step and will be give to the step it proceeds as the final argument. To denote a multiline string the pre and postfix can either be third double or single quotes `""" ... """` or `''' ... '''`. + +For example: + +``` +Given I provide the following "review" comment +""" +Some long review comment. +That can span multiple lines + +Skip lines + +Maybe even include some numbers +1 +2 +3 +""" +``` + +The matching step definition would then be: + +```dart +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class GivenIProvideAComment extends Given2 { + @override + Future executeStep(String commentType, String comment) async { + // TODO: implement executeStep + } + + @override + RegExp get pattern => RegExp(r"I provide the following {string} comment"); +} + +``` + +#### Data tables + +```dart +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +/// This step expects a multiline string proceeding it +/// +/// For example: +/// +/// `Given I add the users` +/// | Firstname | Surname | Age | Gender | +/// | Woody | Johnson | 28 | Male | +/// | Edith | Summers | 23 | Female | +/// | Megan | Hill | 83 | Female | +class GivenIAddTheUsers extends Given1 { + @override + Future executeStep(Table dataTable) async { + // TODO: implement executeStep + for (var row in dataTable.rows) { + // do something with row + row.columns.forEach((columnValue) => print(columnValue)); + } + } + + @override + RegExp get pattern => RegExp(r"I add the users"); +} +``` + +#### Well known step parameters + +In addition to being able to define a step's own parameters (by using regex capturing groups) there are some well known parameter types you can include that will automatically match and convert the parameter into the correct type before passing it to you step definition. (see ). + +In most scenarios theses parameters will be enough for you to write quite advanced step definitions. + +| Parameter Name | Description | Aliases | Type | Example | +| -------------- | --------------------------------------------- | ------------------------------ | ------ | ------------------------------------------------------------------- | +| {word} | Matches a single word surrounded by a quotes | {word}, {Word} | String | `Given I eat a {word}` would match `Given I eat a "worm"` | +| {string} | Matches one more words surrounded by a quotes | {string}, {String} | String | `Given I eat a {string}` would match `Given I eat a "can of worms"` | +| {int} | Matches an integer | {int}, {Int} | int | `Given I see {int} worm(s)` would match `Given I see 6 worms` | +| {num} | Matches an number | {num}, {Num}, {float}, {Float} | num | `Given I see {num} worm(s)` would match `Given I see 0.75 worms` | + +Note that you can combine there well known parameters in any step. For example `Given I {word} {int} worm(s)` would match `Given I "see" 6 worms` and also match `Given I "eat" 1 worm` + +#### Pluralisation + +As the aim of a feature is to convey human readable tests it is often desirable to optionally have some word pluaralised so you can use the special pluralisation syntax to do simple pluralisation of some words in your step definition. For example: + +The step string `Given I see {int} worm(s)` has the pluralisation syntax on the word "worm" and thus would be matched to both `Given I see 1 worm` and `Given I see 4 worms`. + +#### Custom Parameters + +While the well know step parameter will be sufficient in most cases there are time when you would want to defined a custom parameter that might be used across more than or step definition or convert into a custom type. + +The below custom parameter defines a regex that matches the words "red", "green" or "blue". The matches word is passed into the function which is then able to convert the string into a Color object. The name of the custom parameter is used to identity the parameter within the step text. In the below example the word "colour" is used. This is combined with the pre / post prefixes (which default to "{" and "}") to match to the custom parameter. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class ColourParameter extends CustomParameter { + ColourParameter() + : super("colour", RegExp(r"(red|green|blue)"), (c) { + switch (c.toLowerCase()) { + case "red": + return Colors.red; + case "green": + return Colors.green; + case "blue": + return Colors.blue; + } + }); +} +``` + +The step definition would then use this custom parameter like so: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class GivenIPickAColour extends Given1 { + @override + Future executeStep(Color input1) async { + // TODO: implement executeStep + } + + @override + RegExp get pattern => RegExp(r"I pick the colour {colour}"); +} +``` + +This customer parameter would be used like this: `Given I pick the colour red`. When the step is invoked the word "red" would matched and passed to the custom parameter to convert it into a Color object which is then finally passed to the step definition code as a Color object. + +#### World Context (per test scenario shared state) + +#### Assertions + +### Tags + +Tags are a great way of organising your features and marking them with filterable information. Tags can be uses to filter the scenarios that are run. For instance you might have a set of smoke tests to run on every check-in as the full test suite is only ran once a day. You could also use an `@ignore` or `@todo` tag to ignore certain scenarios that might not be ready to run yet. + +You can filter the scenarios by providing a tag expression to your configuration file. Tag expression are simple infix expressions such as: + +`@smoke` + +`@smoke and @perf` + +`@billing or @onboarding` + +`@smoke and not @ignore` + +You can even us brackets to ensure the order of precedence + +`@smoke and not (@ignore or @todo)` + +You can use the usual boolean statement "and", "or", "not" + +Also see + +## Hooks + +## Reporting + +## Flutter + +### Flutter Specific Configuration + +#### Restarting the app before each test + +By default to ensure your app is in a consistent state at the start of each test the app is shut-down and restarted. This behaviour can be turned off by setting the `restartAppBetweenScenarios` flag in your configuration object. Although in more complex scenarios you might want to handle the app reset behaviour yourself; possibly via hooks. + +You might additionally want to do some clean-up of your app after each test by implementing an `onAfterScenario` hook. + +#### Flutter World + +### Pre-defined Steps + +### Debugging + +In VSCode simply add add this block to your launch.json file (if you testable app is called `app_test.dart` and within the `test_driver` folder, if not replace that with the correct file path). Don't forget to put a break point somewhere! + +```json +{ + "name": "Debug Features Tests", + "request": "launch", + "type": "dart", + "program": "test_driver/app_test.dart", + "flutterMode": "debug" +} +``` + +After which the file will most likely look like this + +```json +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "request": "launch", + "type": "dart" + }, + { + "name": "Debug Features Tests", + "request": "launch", + "type": "dart", + "program": "test_driver/app_test.dart", + "flutterMode": "debug" + } + ] +} +``` diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..47e0b4d --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,71 @@ +# 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/.metadata b/example/.metadata new file mode 100644 index 0000000..39581c9 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,8 @@ +# 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: f37c235c32fc15babe6dc7b7bc2ee4387e5ecf92 + channel: beta diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..64a12f6 --- /dev/null +++ b/example/README.md @@ -0,0 +1,8 @@ +# example + +A new Flutter project. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.io/). diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..3d260e6 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,61 @@ +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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 27 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion 16 + targetSdkVersion 27 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + 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 { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1b515f8 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000..84f8920 --- /dev/null +++ b/example/android/app/src/main/java/com/example/example/MainActivity.java @@ -0,0 +1,13 @@ +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/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..d4225c7 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.1.2' + } +} + +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/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..8bd86f6 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9372d0f --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#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-4.4-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +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/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9367d48 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + 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/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bdbe25e --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,436 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* 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 */; }; + 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 = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 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 */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 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 */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + 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 = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + 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 */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 2D5378261FAA1A9400D5DBA9 /* flutter_assets 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\" 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 = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m 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 */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + 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_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_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 = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + 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_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_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 = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + 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.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + 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.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..1263ac8 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..36e21bb --- /dev/null +++ b/example/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..59a72e9 --- /dev/null +++ b/example/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#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/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..3d43d11 Binary files /dev/null and b/example/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/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/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/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/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/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..0513117 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + 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/ios/Runner/main.m b/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#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 new file mode 100644 index 0000000..8381ea6 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,99 @@ +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; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + drawer: Drawer( + key: Key("drawer"), + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + child: Text('Drawer Header'), + decoration: BoxDecoration( + color: Colors.blue, + ), + ), + ListTile( + title: Text('Item 1'), + onTap: () { + // Update the state of the app + // ... + // Then close the drawer + Navigator.pop(context); + }, + ), + ListTile( + title: 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: [ + 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: Key('counter'), + style: Theme.of(context).textTheme.display1, + ), + ], + ), + ), + 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: Key('increment'), + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..43d0197 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,74 @@ +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.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + path: ^1.6.2 + glob: ^1.1.7 + flutter_gherkin: + path: ../ + + # 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 + + +# 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/test_driver/app.dart b/example/test_driver/app.dart new file mode 100644 index 0000000..a5366b9 --- /dev/null +++ b/example/test_driver/app.dart @@ -0,0 +1,12 @@ +import '../lib/main.dart'; +import 'package:flutter/widgets.dart'; +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. + runApp(new MyApp()); +} diff --git a/example/test_driver/app_test.dart b/example/test_driver/app_test.dart new file mode 100644 index 0000000..6fada8e --- /dev/null +++ b/example/test_driver/app_test.dart @@ -0,0 +1,13 @@ +import 'dart:async'; +import 'package:glob/glob.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +Future main() { + final config = FlutterTestConfiguration() + ..features = [Glob(r"test_driver/features/*.feature")] + ..reporters = [StdoutReporter()] + ..restartAppBetweenScenarios = true + ..targetAppPath = "test_driver/app.dart" + ..exitAfterTestRun = true; + return GherkinRunner().execute(config); +} diff --git a/example/test_driver/app_test_old.dart b/example/test_driver/app_test_old.dart new file mode 100644 index 0000000..76d55f2 --- /dev/null +++ b/example/test_driver/app_test_old.dart @@ -0,0 +1,40 @@ +// // Imports the Flutter Driver API +// import 'package:flutter_driver/flutter_driver.dart'; +// import 'package:test/test.dart'; + +// void main() { +// group('Counter App', () { +// // First, define the Finders. We can use these to locate Widgets from the +// // test suite. Note: the Strings provided to the `byValueKey` method must +// // be the same as the Strings we used for the Keys in step 1. +// final counterTextFinder = find.byValueKey('counter'); +// final buttonFinder = find.byValueKey('increment'); + +// FlutterDriver driver; + +// // Connect to the Flutter driver before running any tests +// setUpAll(() async { +// driver = await FlutterDriver.connect(); +// }); + +// // Close the connection to the driver after the tests have completed +// tearDownAll(() async { +// if (driver != null) { +// driver.close(); +// } +// }); + +// test('starts at 0', () async { +// // Use the `driver.getText` method to verify the counter starts at 0. +// expect(await driver.getText(counterTextFinder), "0"); +// }); + +// test('increments the counter', () async { +// // First, tap on the button +// await driver.tap(buttonFinder); + +// // Then, verify the counter text has been incremented by 1 +// expect(await driver.getText(counterTextFinder), "1"); +// }); +// }); +// } diff --git a/example/test_driver/features/counter.feature b/example/test_driver/features/counter.feature new file mode 100644 index 0000000..0dd1a82 --- /dev/null +++ b/example/test_driver/features/counter.feature @@ -0,0 +1,7 @@ +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" diff --git a/example/test_driver/features/counter_increases.feature b/example/test_driver/features/counter_increases.feature new file mode 100644 index 0000000..d644a4f --- /dev/null +++ b/example/test_driver/features/counter_increases.feature @@ -0,0 +1,7 @@ +Feature: Counter + The counter should be increment when the button is pressed. + + Scenario: Counter increases when the button is pressed + 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/drawer.feature b/example/test_driver/features/drawer.feature new file mode 100644 index 0000000..4d89353 --- /dev/null +++ b/example/test_driver/features/drawer.feature @@ -0,0 +1,7 @@ +Feature: Drawer + + Scenario: should open the drawer + Given I open the drawer + # Given I close the drawer + # Then I see the menu item "Item 1" + # And I see the menu item "Item 2" diff --git a/example/test_driver/steps/colour_parameter.dart b/example/test_driver/steps/colour_parameter.dart new file mode 100644 index 0000000..4b3149c --- /dev/null +++ b/example/test_driver/steps/colour_parameter.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class ColourParameter extends CustomParameter { + ColourParameter() + : super("colour", RegExp(r"red|green|blue", caseSensitive: true), (c) { + switch (c.toLowerCase()) { + case "red": + return Colors.red; + case "green": + return Colors.green; + case "blue": + return Colors.blue; + } + }); +} diff --git a/example/test_driver/steps/data_table_example_step.dart b/example/test_driver/steps/data_table_example_step.dart new file mode 100644 index 0000000..52d87ea --- /dev/null +++ b/example/test_driver/steps/data_table_example_step.dart @@ -0,0 +1,24 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +/// This step expects a multiline string proceeding it +/// +/// For example: +/// +/// `Given I add the users` +/// | Firstname | Surname | Age | Gender | +/// | Woody | Johnson | 28 | Male | +/// | Edith | Summers | 23 | Female | +/// | Megan | Hill | 83 | Female | +class GivenIAddTheUsers extends Given1
{ + @override + Future executeStep(Table dataTable) async { + // TODO: implement executeStep + for (var row in dataTable.rows) { + // do something with row + row.columns.forEach((columnValue) => print(columnValue)); + } + } + + @override + RegExp get pattern => RegExp(r"I add the users"); +} 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 new file mode 100644 index 0000000..b5263f7 --- /dev/null +++ b/example/test_driver/steps/given_I_pick_a_colour_step.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class GivenIPickAColour extends Given1 { + @override + Future executeStep(Color input1) async { + // TODO: implement executeStep + } + + @override + RegExp get pattern => RegExp(r"I pick a {colour}"); +} diff --git a/example/test_driver/steps/multiline_string_example_step.dart b/example/test_driver/steps/multiline_string_example_step.dart new file mode 100644 index 0000000..ddf8cc4 --- /dev/null +++ b/example/test_driver/steps/multiline_string_example_step.dart @@ -0,0 +1,19 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +/// This step expects a multiline string proceeding it +/// +/// For example: +/// +/// `Given I provide the following "review" comment` +/// """ +/// Some comment +/// """ +class GivenIProvideAComment extends Given2 { + @override + Future executeStep(String commentType, String comment) async { + // TODO: implement executeStep + } + + @override + RegExp get pattern => RegExp(r"I provide the following {string} comment"); +} diff --git a/example/test_driver/steps/tap_button_n_times_step.dart b/example/test_driver/steps/tap_button_n_times_step.dart new file mode 100644 index 0000000..d1fc0f0 --- /dev/null +++ b/example/test_driver/steps/tap_button_n_times_step.dart @@ -0,0 +1,18 @@ +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class TapButtonNTimesStep extends When2WithWorld { + TapButtonNTimesStep() + : super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10)); + + @override + Future executeStep(String input1, int input2) async { + final locator = find.byValueKey(input1); + for (var i = 0; i < 10; i += 1) { + await world.driver.tap(locator, timeout: timeout); + } + } + + @override + RegExp get pattern => RegExp(r"I tap the {string} button {int} times"); +} diff --git a/flutter_cucumber.iml b/flutter_cucumber.iml new file mode 100644 index 0000000..8d48a06 --- /dev/null +++ b/flutter_cucumber.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/flutter_gherkin.dart b/lib/flutter_gherkin.dart new file mode 100644 index 0000000..3b83107 --- /dev/null +++ b/lib/flutter_gherkin.dart @@ -0,0 +1,31 @@ +library flutter_gherkin; + +export "src/test_runner.dart"; +export "src/configuration.dart"; +export "src/gherkin/steps/world.dart"; +export "src/gherkin/steps/step_definition.dart"; +export "src/gherkin/steps/step_configuration.dart"; +export "src/gherkin/steps/given.dart"; +export "src/gherkin/steps/then.dart"; +export "src/gherkin/steps/when.dart"; +export "src/gherkin/steps/and.dart"; +export "src/gherkin/steps/but.dart"; +export "src/gherkin/parameters/custom_parameter.dart"; + +//models +export "src/gherkin/models/table.dart"; +export "src/gherkin/models/table_row.dart"; + +// Reporters +export "src/reporters/reporter.dart"; +export "src/reporters/message_level.dart"; +export "src/reporters/messages.dart"; +export "src/reporters/stdout_reporter.dart"; + +// Hooks +export "src/hooks/hook.dart"; + +// Flutter specific implementations +export "src/flutter/flutter_world.dart"; +export "src/flutter/flutter_test_configuration.dart"; +export "src/flutter/utils/driver_utils.dart"; diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart new file mode 100644 index 0000000..3435494 --- /dev/null +++ b/lib/src/configuration.dart @@ -0,0 +1,50 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; +import 'package:flutter_gherkin/src/hooks/hook.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; +import 'package:glob/glob.dart'; + +typedef Future CreateWorld(TestConfiguration config); + +enum ExecutionOrder { sequential, random } + +class TestConfiguration { + /// The glob path(s) to all the features + Iterable features; + + /// The default feature language + String featureDefaultLanguage = "en"; + + /// a filter to limit the features that are run based on tags + /// see https://docs.cucumber.io/cucumber/tag-expressions/ for expression syntax + String tagExpression; + + /// The default step timeout - this can be override when definition a step definition + Duration defaultTimeout = Duration(seconds: 10); + + /// The execution order of features - this default to random to avoid any inter-test depedencies + ExecutionOrder order = ExecutionOrder.random; + + /// The user defined step definitions that are matched with written steps in the features + Iterable stepDefinitions; + + /// Any user defined step parameters + Iterable> customStepParameterDefinitions; + + /// Hooks that are run at certain points in the execution cycle + Iterable hooks; + + /// a list of reporters to use. + /// Built-in reporters: + /// - StdoutReporter + /// + /// Custom reporters can be created by extending (or implementing) Reporter.dart + Iterable reporters; + + /// An optional function to create a world object for each scenario. + CreateWorld createWorld; + + /// the program will exit after all the tests have run + bool exitAfterTestRun = true; +} diff --git a/lib/src/expect/expect_mimic.dart b/lib/src/expect/expect_mimic.dart new file mode 100644 index 0000000..a7f6a65 --- /dev/null +++ b/lib/src/expect/expect_mimic.dart @@ -0,0 +1,61 @@ +import 'package:flutter_gherkin/src/expect/expect_mimic_utils.dart'; +import 'package:test/test.dart'; + +/// This is an atrocity but I can't see a way around it at the moment +/// To use the expect() it must be called within a test() or this happens: +/// +/// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart#L95 +/// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect_async.dart#L237 +/// +/// Unfortunately, I cannot get the test framework to play nicely with dynamically +/// creating and adding tests as the tests framework seems to build the tests before +/// I need it to and this happens: +/// +/// "Can't call test() once tests have begun running." +/// +/// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/backend/declarer.dart#L274 +/// +/// We still want to be able to use the Matchers are we can't expect people not to use them +/// So we are stuck here using smoke and mirrors and mimicing the expect / expectAsync methods in our step class +/// +/// https://github.com/dart-lang/test/blob/7555efe8cab11fea89a22685c6c2198c81a58c2b/lib/src/frontend/expect.dart +class ExpectMimic { + /// Assert that [actual] matches [matcher]. + /// + /// This is the main assertion function. [reason] is optional and is typically + /// not supplied, as a reason is generated from [matcher]; if [reason] + /// is included it is appended to the reason generated by the matcher. + /// + /// [matcher] can be a value in which case it will be wrapped in an + /// [equals] matcher. + /// + /// If the assertion fails a [TestFailure] is thrown. + /// + /// If [skip] is a String or `true`, the assertion is skipped. The arguments are + /// still evaluated, but [actual] is not verified to match [matcher]. If + /// [actual] is a [Future], the test won't complete until the future emits a + /// value. + /// + /// Certain matchers, like [completion] and [throwsA], either match or fail + /// asynchronously. When you use [expect] with these matchers, it ensures that + /// the test doesn't complete until the matcher has either matched or failed. If + /// you want to wait for the matcher to complete before continuing the test, you + /// can call [expectLater] instead and `await` the result. + void expect(actualValue, matcher, {String reason}) { + var matchState = {}; + matcher = wrapMatcher(matcher); + final result = matcher.matches(actualValue, matchState); + final formatter = (actual, matcher, reason, matchState, verbose) { + var mismatchDescription = new StringDescription(); + matcher.describeMismatch( + actual, mismatchDescription, matchState, verbose); + + return formatFailure(matcher, actual, mismatchDescription.toString(), + reason: reason); + }; + if (!result) { + fail(formatter( + actualValue, matcher as Matcher, reason, matchState, false)); + } + } +} diff --git a/lib/src/expect/expect_mimic_utils.dart b/lib/src/expect/expect_mimic_utils.dart new file mode 100644 index 0000000..3736493 --- /dev/null +++ b/lib/src/expect/expect_mimic_utils.dart @@ -0,0 +1,51 @@ +import 'package:matcher/matcher.dart'; + +/// Returns a pretty-printed representation of [value]. +/// +/// The matcher package doesn't expose its pretty-print function directly, but +/// we can use it through StringDescription. +String prettyPrint(value) => + new StringDescription().addDescriptionOf(value).toString(); + +String formatFailure(Matcher expected, actual, String which, {String reason}) { + var buffer = new StringBuffer(); + buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); + buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); + if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: ')); + if (reason != null) buffer.writeln(reason); + return buffer.toString(); +} + +/// Indent each line in [string] by [size] spaces. +/// +/// If [first] is passed, it's used in place of the first line's indentation and +/// [size] defaults to `first.length`. Otherwise, [size] defaults to 2. +String indent(String string, {int size, String first}) { + size ??= first == null ? 2 : first.length; + return prefixLines(string, " " * size, first: first); +} + +/// Prepends each line in [text] with [prefix]. +/// +/// If [first] or [last] is passed, the first and last lines, respectively, are +/// prefixed with those instead. If [single] is passed, it's used if there's +/// only a single line; otherwise, [first], [last], or [prefix] is used, in that +/// order of precedence. +String prefixLines(String text, String prefix, + {String first, String last, String single}) { + first ??= prefix; + last ??= prefix; + single ??= first ?? last ?? prefix; + + var lines = text.split('\n'); + if (lines.length == 1) return "$single$text"; + + var buffer = new StringBuffer("$first${lines.first}\n"); + + // Write out all but the first and last lines with [prefix]. + for (var line in lines.skip(1).take(lines.length - 2)) { + buffer.writeln("$prefix$line"); + } + buffer.write("$last${lines.last}"); + return buffer.toString(); +} diff --git a/lib/src/feature_file_runner.dart b/lib/src/feature_file_runner.dart new file mode 100644 index 0000000..ffad0a3 --- /dev/null +++ b/lib/src/feature_file_runner.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/exceptions/step_not_defined_error.dart'; +import 'package:flutter_gherkin/src/gherkin/expressions/tag_expression.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/background.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/exectuable_step.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_run_result.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; +import 'package:flutter_gherkin/src/reporters/messages.dart'; + +class FeatureFileRunner { + final TestConfiguration _config; + final TagExpressionEvaluator _tagExpressionEvaluator; + final Iterable _steps; + final Reporter _reporter; + final Hook _hook; + + FeatureFileRunner(this._config, this._tagExpressionEvaluator, this._steps, + this._reporter, this._hook); + + Future run(FeatureFile featureFile) async { + bool haveAllFeaturesPassed = true; + for (var feature in featureFile.features) { + haveAllFeaturesPassed &= await _runFeature(feature); + } + + return haveAllFeaturesPassed; + } + + Future _runFeature(FeatureRunnable feature) async { + bool haveAllScenariosPassed = true; + if (_canRunFeature(_config.tagExpression, feature)) { + try { + await _reporter.onFeatureStarted( + StartedMessage(Target.feature, feature.name, feature.debug)); + await _log("Attempting to running feature '${feature.name}'", + feature.debug, MessageLevel.info); + for (final scenario in feature.scenarios) { + haveAllScenariosPassed &= + await _runScenario(scenario, feature.background); + } + } catch (e, stacktrace) { + await _log("Error while running feature '${feature.name}'\n$e", + feature.debug, MessageLevel.error); + await _reporter.onException(e, stacktrace); + rethrow; + } finally { + await _reporter.onFeatureFinished( + FinishedMessage(Target.feature, feature.name, feature.debug)); + await _log("Finished running feature '${feature.name}'", feature.debug, + MessageLevel.info); + } + } else { + await _log( + "Ignoring feature '${feature.name}' as tag expression not satified for feature", + feature.debug, + MessageLevel.info); + } + + return haveAllScenariosPassed; + } + + bool _canRunFeature(String tagExpression, FeatureRunnable feature) { + return tagExpression == null + ? true + : _tagExpressionEvaluator.evaluate(tagExpression, feature.tags); + } + + Future _runScenario( + ScenarioRunnable scenario, BackgroundRunnable background) async { + World world; + bool scenarioPassed = true; + await _hook.onBeforeScenario(_config, scenario.name); + if (_config.createWorld != null) { + await _log("Creating new world for scenerio '${scenario.name}'", + scenario.debug, MessageLevel.debug); + world = await _config.createWorld(_config); + } + + _reporter.onScenarioStarted( + StartedMessage(Target.scenario, scenario.name, scenario.debug)); + if (background != null) { + await _log("Running background steps for scenerio '${scenario.name}'", + scenario.debug, MessageLevel.info); + for (var step in background.steps) { + final result = await _runStep(step, world, !scenarioPassed); + scenarioPassed = result.result == StepExecutionResult.pass; + if (!_canContinueScenario(result)) { + scenarioPassed = false; + _log( + "Background step '${step.name}' did not pass, all remaining steps will be skiped", + step.debug, + MessageLevel.warning); + } + } + } + + for (var step in scenario.steps) { + final result = await _runStep(step, world, !scenarioPassed); + scenarioPassed = result.result == StepExecutionResult.pass; + if (!_canContinueScenario(result)) { + scenarioPassed = false; + _log( + "Step '${step.name}' did not pass, all remaining steps will be skiped", + step.debug, + MessageLevel.warning); + } + } + + world?.dispose(); + + await _hook.onAfterScenario(_config, scenario.name); + _reporter.onScenarioFinished( + FinishedMessage(Target.scenario, scenario.name, scenario.debug)); + return scenarioPassed; + } + + bool _canContinueScenario(StepResult stepResult) { + return stepResult.result == StepExecutionResult.pass; + } + + Future _runStep( + StepRunnable step, World world, bool skipExecution) async { + StepResult result; + ExectuableStep code = _matchStepToExectuableStep(step); + Iterable parameters = _getStepParameters(step, code); + + await _log( + "Attempting to run step '${step.name}'", step.debug, MessageLevel.info); + await _reporter + .onStepStarted(StartedMessage(Target.step, step.name, step.debug)); + if (skipExecution) { + result = StepResult(0, StepExecutionResult.skipped); + } else { + result = await _runWithinTest( + step.name, + () async => code.step + .run(world, _reporter, _config.defaultTimeout, parameters)); + } + await _reporter + .onStepFinished(StepFinishedMessage(step.name, step.debug, result)); + return result; + } + + /// the idea here is that we could use this as an abstraction to run + /// within another test framework + Future _runWithinTest(String name, Future fn()) async { + // the timeout is handled indepedently from this + final completer = Completer(); + try { + // test(name, () async { + try { + final result = await fn(); + completer.complete(result); + } catch (e) { + completer.completeError(e); + } + // }, timeout: Timeout.none); + } catch (e) { + completer.completeError(e); + } + return completer.future; + } + + ExectuableStep _matchStepToExectuableStep(StepRunnable step) { + final executable = _steps.firstWhere( + (s) => s.expression.isMatch(step.debug.lineText), + orElse: () => null); + if (executable == null) { + final message = """ + Step definition not found for text: + + '${step.debug.lineText}' + + File path: ${step.debug.filePath}#${step.debug.lineNumber} + Line: ${step.debug.lineText} + + --------------------------------------------- + + You must implement the step: + + /// The 'Given' class can be replaced with 'Then', 'When' 'And' or 'But' + /// All classes can take up to 5 input parameters anymore and you should probably us a table + /// For example: `When4` + /// You can also specify the type of world context you want + /// `When4WithWorld` + class Given_${step.debug.lineText.trim().replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_')} extends Given1 { + @override + RegExp get pattern => RegExp(r"${step.debug.lineText}"); + + @override + Future executeStep(String input1) async { + // If the step is "Given I do a 'windy pop'" + // in this example input1 would equal 'windy pop' + + // your code... + } + } + """; + throw new GherkinStepNotDefinedException(message); + } + + return executable; + } + + Iterable _getStepParameters(StepRunnable step, ExectuableStep code) { + Iterable parameters = + code.expression.getParameters(step.debug.lineText); + if (step.multilineStrings.length > 0) { + parameters = parameters.toList()..addAll(step.multilineStrings); + } + + return parameters; + } + + Future _log(String message, RunnableDebugInformation context, + MessageLevel level) async { + await _reporter.message( + "$message # ${context.filePath}:${context.lineNumber}", level); + } +} diff --git a/lib/src/flutter/flutter_run_process_handler.dart b/lib/src/flutter/flutter_run_process_handler.dart new file mode 100644 index 0000000..8660661 --- /dev/null +++ b/lib/src/flutter/flutter_run_process_handler.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_gherkin/src/processes/process_handler.dart'; + +class FlutterRunProcessHandler extends ProcessHandler { + static const String FAIL_COLOR = "\u001b[33;31m"; // red + static const String RESET_COLOR = "\u001b[33;0m"; + + static RegExp _observatoryDebuggerUriRegex = RegExp( + r"observatory debugger .*[:]? (http[s]?:.*\/).*", + caseSensitive: false, + multiLine: false); + Process _runningProcess; + Stream _processStdoutStream; + List _openSubscriptions = List(); + String _appTarget; + String _workingDirectory; + + void setApplicationTargetFile(String targetPath) { + _appTarget = targetPath; + } + + void setWorkingDirectory(String workingDirectory) { + _workingDirectory = workingDirectory; + } + + @override + Future run() async { + _runningProcess = await Process.start( + "flutter", ["run", "--target=$_appTarget"], + workingDirectory: _workingDirectory, runInShell: true); + _processStdoutStream = + _runningProcess.stdout.transform(utf8.decoder).asBroadcastStream(); + + _openSubscriptions.add(_runningProcess.stderr.listen((events) { + stderr.writeln( + "${FAIL_COLOR}Flutter run error: ${String.fromCharCodes(events)}$RESET_COLOR"); + })); + } + + @override + Future terminate() async { + int exitCode = -1; + _ensureRunningProcess(); + if (_runningProcess != null) { + _runningProcess.stdin.write("q"); + _openSubscriptions.forEach((s) => s.cancel()); + _openSubscriptions.clear(); + exitCode = await _runningProcess.exitCode; + _runningProcess = null; + } + + return exitCode; + } + + Future waitForObservatoryDebuggerUri( + [Duration timeout = const Duration(seconds: 60)]) { + _ensureRunningProcess(); + final completer = Completer(); + StreamSubscription sub; + sub = _processStdoutStream + .timeout(timeout, + onTimeout: (_) => completer.completeError(TimeoutException( + "Time out while wait for observatory debugger uri", timeout))) + .listen((logLine) { + if (_observatoryDebuggerUriRegex.hasMatch(logLine)) { + sub?.cancel(); + completer.complete( + _observatoryDebuggerUriRegex.firstMatch(logLine).group(1)); + } + }); + + return completer.future; + } + + void _ensureRunningProcess() { + if (_runningProcess == null) { + throw new Exception( + "FlutterRunProcessHandler: flutter run process is not active"); + } + } +} diff --git a/lib/src/flutter/flutter_test_configuration.dart b/lib/src/flutter/flutter_test_configuration.dart new file mode 100644 index 0000000..39a766a --- /dev/null +++ b/lib/src/flutter/flutter_test_configuration.dart @@ -0,0 +1,49 @@ +import 'dart:io'; +import 'package:flutter_gherkin/flutter_gherkin.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/steps/given_i_open_the_drawer_step.dart'; +import 'package:flutter_gherkin/src/flutter/steps/then_expect_element_to_have_value_step.dart'; +import 'package:flutter_gherkin/src/flutter/steps/when_tap_widget_step.dart'; +import 'package:flutter_driver/flutter_driver.dart'; + +class FlutterTestConfiguration extends TestConfiguration { + String _observatoryDebuggerUri; + + /// 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/app.dart" + String targetAppPath = "lib/app.dart"; + + FlutterTestConfiguration() : super() { + createWorld = (config) async => await createFlutterWorld(config); + stepDefinitions = [ + ThenExpectElementToHaveValue(), + WhenTapWidget(), + GivenOpenDrawer() + ]; + hooks = [FlutterAppRunnerHook()]; + } + + void setObservatoryDebuggerUri(String uri) => _observatoryDebuggerUri = uri; + + Future createFlutterDriver([String dartVmServiceUrl]) async { + dartVmServiceUrl = (dartVmServiceUrl ?? _observatoryDebuggerUri) ?? + Platform.environment['VM_SERVICE_URL']; + final driver = await FlutterDriver.connect( + dartVmServiceUrl: dartVmServiceUrl, + isolateReadyTimeout: Duration(seconds: 30)); + return driver; + } + + Future createFlutterWorld(TestConfiguration config) async { + final world = new FlutterWorld(); + final driver = await createFlutterDriver(); + world.setFlutterDriver(driver); + return world; + } +} diff --git a/lib/src/flutter/flutter_world.dart b/lib/src/flutter/flutter_world.dart new file mode 100644 index 0000000..7211dee --- /dev/null +++ b/lib/src/flutter/flutter_world.dart @@ -0,0 +1,17 @@ +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; +import 'package:flutter_driver/flutter_driver.dart'; + +class FlutterWorld extends World { + FlutterDriver _driver; + + FlutterDriver get driver => _driver; + + setFlutterDriver(FlutterDriver flutterDriver) { + _driver = flutterDriver; + } + + @override + void dispose() { + _driver.close(); + } +} diff --git a/lib/src/flutter/hooks/app_runner_hook.dart b/lib/src/flutter/hooks/app_runner_hook.dart new file mode 100644 index 0000000..aafa41f --- /dev/null +++ b/lib/src/flutter/hooks/app_runner_hook.dart @@ -0,0 +1,57 @@ +import 'package:flutter_gherkin/src/configuration.dart'; +import 'package:flutter_gherkin/src/flutter/flutter_run_process_handler.dart'; +import 'package:flutter_gherkin/src/flutter/flutter_test_configuration.dart'; +import 'package:flutter_gherkin/src/hooks/hook.dart'; + +/// A hook that manages running the arget flutter application +/// that is under test +class FlutterAppRunnerHook extends Hook { + FlutterRunProcessHandler _flutterAppProcess; + bool haveRunFirstScenario = false; + + int get priority => 999999; + + Future onBeforeRun(TestConfiguration config) async { + await _runApp(_castConfig(config)); + } + + Future onAfterRun(TestConfiguration config) async => + await _terminateApp(); + + Future onBeforeScenario( + TestConfiguration config, String scenario) async { + final flutterConfig = _castConfig(config); + if (_flutterAppProcess == null) { + await _runApp(flutterConfig); + } + } + + Future onAfterScenario( + TestConfiguration config, String scenario) async { + final flutterConfig = _castConfig(config); + haveRunFirstScenario = true; + if (_flutterAppProcess != null && + flutterConfig.restartAppBetweenScenarios) { + await _terminateApp(); + } + } + + Future _runApp(FlutterTestConfiguration config) async { + _flutterAppProcess = new FlutterRunProcessHandler(); + _flutterAppProcess.setApplicationTargetFile(config.targetAppPath); + await _flutterAppProcess.run(); + final observatoryUri = + await _flutterAppProcess.waitForObservatoryDebuggerUri(); + config.setObservatoryDebuggerUri(observatoryUri); + } + + Future _terminateApp() async { + if (_flutterAppProcess != null) { + await _flutterAppProcess.terminate(); + _flutterAppProcess = null; + } + } + + FlutterTestConfiguration _castConfig(TestConfiguration config) => + config as FlutterTestConfiguration; +} 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 new file mode 100644 index 0000000..c50050d --- /dev/null +++ b/lib/src/flutter/steps/given_i_open_the_drawer_step.dart @@ -0,0 +1,30 @@ +import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/flutter/utils/driver_utils.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/given.dart'; +import 'package:flutter_driver/flutter_driver.dart'; + +/// Opens the applications main drawer +/// +/// Examples: +/// +/// `Given I open the drawer` +class GivenOpenDrawer extends Given1WithWorld { + @override + RegExp get pattern => RegExp(r"I (open|close) the drawer"); + + @override + Future executeStep(String action) async { + final drawerFinder = find.byType("Drawer"); + final isOpen = + await FlutterDriverUtils().isPresent(drawerFinder, world.driver); + // https://github.com/flutter/flutter/issues/9002#issuecomment-293660833 + if (isOpen && action == "close") { + // Swipe to the left across the whole app to close the drawer + await world.driver + .scroll(drawerFinder, -300.0, 0.0, Duration(milliseconds: 300)); + } else if (!isOpen && action == "open") { + final locator = find.byTooltip("Open navigation menu"); + await world.driver.tap(locator, timeout: timeout); + } + } +} 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 new file mode 100644 index 0000000..c023ee3 --- /dev/null +++ b/lib/src/flutter/steps/then_expect_element_to_have_value_step.dart @@ -0,0 +1,33 @@ +import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/then.dart'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_driver/flutter_driver.dart'; + +/// Expects the element found with the given control key to have the given string value. +/// +/// Parameters: +/// 1 - {string} the control key +/// 2 - {string} the value of the control +/// +/// Examples: +/// +/// `Then I expect "controlKey" to be "Hello World"` +/// `And I expect "controlKey" to be "Hello World"` +class ThenExpectElementToHaveValue + extends Then2WithWorld { + @override + RegExp get pattern => RegExp(r"I expect the {string} to be {string}"); + + @override + Future executeStep(String key, String value) async { + final locator = find.byValueKey(key); + try { + final text = await world.driver.getText(locator, timeout: timeout); + expect(text, value); + } catch (e) { + await reporter.message( + "Step error '${pattern.pattern}': $e", MessageLevel.error); + rethrow; + } + } +} diff --git a/lib/src/flutter/steps/when_tap_widget_step.dart b/lib/src/flutter/steps/when_tap_widget_step.dart new file mode 100644 index 0000000..f675633 --- /dev/null +++ b/lib/src/flutter/steps/when_tap_widget_step.dart @@ -0,0 +1,29 @@ +import 'package:flutter_gherkin/src/flutter/flutter_world.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/when.dart'; +import 'package:flutter_driver/flutter_driver.dart'; + +/// Taps the widget found with the given control key. +/// +/// Parameters: +/// 1 - {string} the control key +/// +/// Examples: +/// +/// `When I tap "controlKey" button"` +/// `When I tap "controlKey" element"` +/// `When I tap "controlKey" label"` +/// `When I tap "controlKey" icon"` +/// `When I tap "controlKey" field"` +/// `When I tap "controlKey" text"` +/// `When I tap "controlKey" widget"` +class WhenTapWidget extends When1WithWorld { + @override + RegExp get pattern => RegExp( + r"I tap the {string} [button|element|label|icon|field|text|widget]"); + + @override + Future executeStep(String key) async { + final locator = find.byValueKey(key); + await world.driver.tap(locator, timeout: timeout); + } +} diff --git a/lib/src/flutter/utils/driver_utils.dart b/lib/src/flutter/utils/driver_utils.dart new file mode 100644 index 0000000..2ad7b19 --- /dev/null +++ b/lib/src/flutter/utils/driver_utils.dart @@ -0,0 +1,13 @@ +import 'package:flutter_driver/flutter_driver.dart'; + +class FlutterDriverUtils { + Future isPresent(SerializableFinder finder, FlutterDriver driver, + {Duration timeout = const Duration(seconds: 1)}) async { + try { + await driver.waitFor(finder, timeout: timeout); + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/src/gherkin/exceptions/parameter_count_mismatch_error.dart b/lib/src/gherkin/exceptions/parameter_count_mismatch_error.dart new file mode 100644 index 0000000..5ffb54e --- /dev/null +++ b/lib/src/gherkin/exceptions/parameter_count_mismatch_error.dart @@ -0,0 +1,14 @@ +class GherkinStepParameterMismatchException implements Exception { + final int expectParameterCount; + final int actualParameterCount; + final Type step; + final String message; + + GherkinStepParameterMismatchException( + this.step, this.expectParameterCount, this.actualParameterCount) + : message = "$step parameter count mismatch. Expect $expectParameterCount parameters but got $actualParameterCount. " + + "Ensure you are extending the correct step class which would be " + + "Given${actualParameterCount > 0 ? '$actualParameterCount<${List.generate(actualParameterCount, (i) => "TInputType$i").join(", ")}>' : ''}"; + + String toString() => message; +} diff --git a/lib/src/gherkin/exceptions/step_not_defined_error.dart b/lib/src/gherkin/exceptions/step_not_defined_error.dart new file mode 100644 index 0000000..f8c7d09 --- /dev/null +++ b/lib/src/gherkin/exceptions/step_not_defined_error.dart @@ -0,0 +1,5 @@ +class GherkinStepNotDefinedException implements Exception { + final String message; + + GherkinStepNotDefinedException(this.message); +} diff --git a/lib/src/gherkin/exceptions/syntax_error.dart b/lib/src/gherkin/exceptions/syntax_error.dart new file mode 100644 index 0000000..61c7c03 --- /dev/null +++ b/lib/src/gherkin/exceptions/syntax_error.dart @@ -0,0 +1,5 @@ +class GherkinSyntaxException implements Exception { + final String message; + + GherkinSyntaxException(this.message); +} diff --git a/lib/src/gherkin/expressions/gherkin_expression.dart b/lib/src/gherkin/expressions/gherkin_expression.dart new file mode 100644 index 0000000..a2a56b3 --- /dev/null +++ b/lib/src/gherkin/expressions/gherkin_expression.dart @@ -0,0 +1,97 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/step_defined_parameter.dart'; + +class _SortedParameterPosition { + final int startPosition; + final CustomParameter parameter; + + _SortedParameterPosition(this.startPosition, this.parameter); +} + +class GherkinExpression { + final RegExp originalExpression; + final List<_SortedParameterPosition> _sortedParameterPositions = + List<_SortedParameterPosition>(); + RegExp _expression; + + GherkinExpression(this.originalExpression, + Iterable> customParameters) { + String pattern = originalExpression.pattern; + customParameters.forEach((p) { + if (originalExpression.pattern.contains(p.identifier)) { + // we need the index in the original pattern to be able to + // transform the parameter into the correct type later on + // so get that then modify the new matching pattern. + originalExpression.pattern.replaceAllMapped( + RegExp(_escapeIdentifier(p.identifier), + caseSensitive: true, multiLine: true), (m) { + _sortedParameterPositions.add(_SortedParameterPosition(m.start, p)); + }); + pattern = pattern.replaceAllMapped( + RegExp(_escapeIdentifier(p.identifier), + caseSensitive: true, multiLine: true), + (m) => p.pattern.pattern); + } + }); + + // check for any capture patterns that are not custom parameters + // but defined directly in the step definition for example: + // Given I (open|close) the drawer(s) + // note that we should ignore the predefined (s) plural parameter + bool inCustomBracketSection = false; + int indexOfOpeningBracket; + for (var i = 0; i < originalExpression.pattern.length; i += 1) { + var char = originalExpression.pattern[i]; + if (char == "(") { + // look ahead and make sure we don't see "s)" which would + // indicate the plural parameter + if (originalExpression.pattern.length > i + 2) { + final justAhead = originalExpression.pattern[i + 1] + + originalExpression.pattern[i + 2]; + if (justAhead != "s)") { + inCustomBracketSection = true; + indexOfOpeningBracket = i; + } + } + } else if (char == ")" && inCustomBracketSection) { + _sortedParameterPositions.add(_SortedParameterPosition( + indexOfOpeningBracket, UserDefinedStepParameterParameter())); + inCustomBracketSection = false; + indexOfOpeningBracket = 0; + } + } + + _sortedParameterPositions.sort((a, b) => a.startPosition - b.startPosition); + _expression = RegExp(pattern, + caseSensitive: originalExpression.isCaseSensitive, + multiLine: originalExpression.isMultiLine); + } + + String _escapeIdentifier(String identifier) => + identifier.replaceAll("(", "\\(").replaceAll(")", "\\)"); + + bool isMatch(String input) { + return _expression.hasMatch(input); + } + + Iterable getParameters(String input) { + List stringValues = List(); + List values = List(); + _expression.allMatches(input).forEach((m) { + // the first group is always the input string + final indicies = + List.generate(m.groupCount, (i) => i + 1, growable: false).toList(); + stringValues.addAll(m.groups(indicies)); + }); + + for (int i = 0; i < stringValues.length; i += 1) { + final val = stringValues.elementAt(i); + final cp = _sortedParameterPositions.elementAt(i); + if (cp.parameter.includeInParameterList) { + values.add(cp.parameter.transformer(val)); + } + } + + return values; + } +} diff --git a/lib/src/gherkin/expressions/tag_expression.dart b/lib/src/gherkin/expressions/tag_expression.dart new file mode 100644 index 0000000..74ea9c5 --- /dev/null +++ b/lib/src/gherkin/expressions/tag_expression.dart @@ -0,0 +1,121 @@ +import 'dart:collection'; +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; + +/// Evaluates tag expression lexicon such as +/// @smoke and @perf +/// @smoke and not @perf +/// not @smoke and not @perf +/// not (@smoke or @perf) +/// @smoke and (@perf or @android) +/// see https://docs.cucumber.io/cucumber/tag-expressions/ +/// +/// We can tackle these infix expressions with good old reverse polish notation +/// which incidently was one of the first algorithms I coded when I got my first +/// programing job. +class TagExpressionEvaluator { + static const String openingBracket = "("; + static const String closingBracket = ")"; + static final Map _operatorPrededence = { + "not": 4, + "or": 2, + "and": 2, + "(": 0, + }; + + bool evaluate(String tagExpression, List tags) { + bool match = true; + final rpn = _convertInfixToPostfixExpression(tagExpression); + match = _evaluateRpn(rpn, tags); + return match; + } + + bool _evaluateRpn(Queue rpn, List tags) { + Queue stack = Queue(); + for (var token in rpn) { + if (_isTag(token)) { + stack.addFirst(tags.contains(token.replaceFirst(RegExp("@"), ""))); + } else { + switch (token) { + case "and": + { + final a = stack.removeFirst(); + final b = stack.removeFirst(); + stack.addFirst(a && b); + break; + } + case "or": + { + final a = stack.removeFirst(); + final b = stack.removeFirst(); + stack.addFirst(a || b); + break; + } + case "not": + { + final a = stack.removeFirst(); + stack.addFirst(!a); + break; + } + } + } + } + + return stack.removeFirst(); + } + + Queue _convertInfixToPostfixExpression(String infixExpression) { + final expressionParts = RegExp( + r"(\()|(or)|(and)|(not)|(@{1}\w{1}[^\s&\)]*)|(\))", + caseSensitive: false) + .allMatches(infixExpression) + .map((m) => m.group(0)); + final rpn = Queue(); + final operatorQueue = ListQueue(); + + for (var part in expressionParts) { + if (_isTag(part)) { + rpn.add(part); + } else if (part == openingBracket) { + operatorQueue.addLast(part); + } else if (part == closingBracket) { + while ( + operatorQueue.length > 0 && operatorQueue.last != openingBracket) { + rpn.add(operatorQueue.removeLast()); + } + operatorQueue.removeLast(); + } else if (_isOperator(part)) { + final precendence = _operatorPrededence[part.toLowerCase()]; + + while (operatorQueue.length > 0 && + _operatorPrededence[operatorQueue.last] >= precendence) { + rpn.add(operatorQueue.removeLast()); + } + operatorQueue.addLast(part); + } else { + throw new GherkinSyntaxException( + "Tag expression '$infixExpression' is not valid. Unknown token '$part'. Known tokens are '@tag', 'and', 'or', 'not' '(' and ')'"); + } + } + + while (operatorQueue.length > 0) { + rpn.add(operatorQueue.removeLast()); + } + + return rpn; + } + + bool _isTag(String token) => + RegExp(r"^@\w{1}.*", caseSensitive: false).hasMatch(token); + + bool _isOperator(String token) { + switch (token.toLowerCase()) { + case "and": + case "or": + case "not": + case openingBracket: + return true; + default: + return false; + } + } +} diff --git a/lib/src/gherkin/feature.dart b/lib/src/gherkin/feature.dart new file mode 100644 index 0000000..f54f747 --- /dev/null +++ b/lib/src/gherkin/feature.dart @@ -0,0 +1,7 @@ +class Feature { + String name; + String language; + Iterable tags; + + Feature(); +} diff --git a/lib/src/gherkin/models/table.dart b/lib/src/gherkin/models/table.dart new file mode 100644 index 0000000..c7bcddc --- /dev/null +++ b/lib/src/gherkin/models/table.dart @@ -0,0 +1,8 @@ +import 'package:flutter_gherkin/src/gherkin/models/table_row.dart'; + +class Table { + final Iterable rows; + final TableRow header; + + Table(this.rows, this.header); +} diff --git a/lib/src/gherkin/models/table_row.dart b/lib/src/gherkin/models/table_row.dart new file mode 100644 index 0000000..d3e5bc0 --- /dev/null +++ b/lib/src/gherkin/models/table_row.dart @@ -0,0 +1,7 @@ +class TableRow { + final bool isHeaderRow; + final int rowIndex; + final Iterable columns; + + TableRow(this.columns, this.rowIndex, this.isHeaderRow); +} diff --git a/lib/src/gherkin/parameters/custom_parameter.dart b/lib/src/gherkin/parameters/custom_parameter.dart new file mode 100644 index 0000000..eb0abb2 --- /dev/null +++ b/lib/src/gherkin/parameters/custom_parameter.dart @@ -0,0 +1,36 @@ +typedef TValue Transformer(String value); + +/// A class used to define and parse custom parameters in step definitions +/// see https://docs.cucumber.io/cucumber/cucumber-expressions/#custom-parameter-types +abstract class CustomParameter { + /// the name in the step definition to search for. This is combined with the identifier prefix / suffix to create a replacable token + /// that signals this parameter for example "My name is {string}" so the name would be "string". + final String name; + + /// the regex pattern that can parse the step string + /// For example: + /// Template: "My name is {string}" + /// Step: "My name is 'Jon'" + /// Regex: "['|\"](.*)['|\"]" + /// The above regex would pull out the work "Jon from the step" + final RegExp pattern; + + /// A transformer function that takes a string and return the correct type of this parameter + final Transformer transformer; + + /// The prefix used for the name token to identify this parameter. Defaults to "{". + final String identifierPrefix; + + /// The suffix used for the name token to identify this parameter. Defaults to "}". + final String identifierSuffix; + + /// If this parameter should be included in the list of step arguments. Defaults to true. + final bool includeInParameterList; + + String get identifier => "$identifierPrefix$name$identifierSuffix"; + + CustomParameter(this.name, this.pattern, this.transformer, + {this.identifierPrefix = "{", + this.identifierSuffix = "}", + this.includeInParameterList = true}); +} diff --git a/lib/src/gherkin/parameters/float_parameter.dart b/lib/src/gherkin/parameters/float_parameter.dart new file mode 100644 index 0000000..3235515 --- /dev/null +++ b/lib/src/gherkin/parameters/float_parameter.dart @@ -0,0 +1,25 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class FloatParameterBase extends CustomParameter { + FloatParameterBase(String name) + : super(name, RegExp("([0-9]+.[0-9]+)"), (String input) { + final n = num.parse(input); + return n; + }); +} + +class FloatParameterLower extends FloatParameterBase { + FloatParameterLower() : super("float"); +} + +class FloatParameterCamel extends FloatParameterBase { + FloatParameterCamel() : super("Float"); +} + +class NumParameterLower extends FloatParameterBase { + NumParameterLower() : super("num"); +} + +class NumParameterCamel extends FloatParameterBase { + NumParameterCamel() : super("Num"); +} diff --git a/lib/src/gherkin/parameters/int_parameter.dart b/lib/src/gherkin/parameters/int_parameter.dart new file mode 100644 index 0000000..f82eaff --- /dev/null +++ b/lib/src/gherkin/parameters/int_parameter.dart @@ -0,0 +1,17 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class IntParameterBase extends CustomParameter { + IntParameterBase(String name) + : super(name, RegExp("([0-9]+)"), (String input) { + final n = int.parse(input, radix: 10); + return n; + }); +} + +class IntParameterLower extends IntParameterBase { + IntParameterLower() : super("int"); +} + +class IntParameterCamel extends IntParameterBase { + IntParameterCamel() : super("Int"); +} diff --git a/lib/src/gherkin/parameters/plural_parameter.dart b/lib/src/gherkin/parameters/plural_parameter.dart new file mode 100644 index 0000000..c219151 --- /dev/null +++ b/lib/src/gherkin/parameters/plural_parameter.dart @@ -0,0 +1,9 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class PluralParameter extends CustomParameter { + PluralParameter() + : super("s", RegExp("(s)?"), (String input) => null, + identifierPrefix: "(", + identifierSuffix: ")", + includeInParameterList: false); +} diff --git a/lib/src/gherkin/parameters/step_defined_parameter.dart b/lib/src/gherkin/parameters/step_defined_parameter.dart new file mode 100644 index 0000000..fe6b16c --- /dev/null +++ b/lib/src/gherkin/parameters/step_defined_parameter.dart @@ -0,0 +1,6 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class UserDefinedStepParameterParameter extends CustomParameter { + UserDefinedStepParameterParameter() + : super("", RegExp(""), (String input) => input); +} diff --git a/lib/src/gherkin/parameters/string_parameter.dart b/lib/src/gherkin/parameters/string_parameter.dart new file mode 100644 index 0000000..847d87e --- /dev/null +++ b/lib/src/gherkin/parameters/string_parameter.dart @@ -0,0 +1,14 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class StringParameterBase extends CustomParameter { + StringParameterBase(String name) + : super(name, RegExp("['|\"](.*)['|\"]"), (String input) => input); +} + +class StringParameterLower extends StringParameterBase { + StringParameterLower() : super("string"); +} + +class StringParameterCamel extends StringParameterBase { + StringParameterCamel() : super("String"); +} diff --git a/lib/src/gherkin/parameters/word_parameter.dart b/lib/src/gherkin/parameters/word_parameter.dart new file mode 100644 index 0000000..5e9dcb4 --- /dev/null +++ b/lib/src/gherkin/parameters/word_parameter.dart @@ -0,0 +1,14 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; + +class WordParameterBase extends CustomParameter { + WordParameterBase(String name) + : super(name, RegExp("['|\"](\\w+)['|\"]"), (String input) => input); +} + +class WordParameterLower extends WordParameterBase { + WordParameterLower() : super("word"); +} + +class WordParameterCamel extends WordParameterBase { + WordParameterCamel() : super("Word"); +} diff --git a/lib/src/gherkin/parser.dart b/lib/src/gherkin/parser.dart new file mode 100644 index 0000000..62740a4 --- /dev/null +++ b/lib/src/gherkin/parser.dart @@ -0,0 +1,84 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/background_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/empty_line_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/feature_file_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/feature_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/language_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/scenario_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/step_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/table_line_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/tag_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/text_line_syntax.dart'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; + +class GherkinParser { + final Iterable syntaxMatchers = [ + LanguageSyntax(), + CommentSyntax(), + FeatureSyntax(), + BackgroundSyntax(), + TagSyntax(), + ScenarioSyntax(), + StepSyntax(), + MultilineStringSyntax(), + EmptyLineSyntax(), + TableLineSyntax(), + TextLineSyntax() + ]; + + Future parseFeatureFile( + String contents, String path, Reporter reporter) async { + final featureFile = FeatureFile(RunnableDebugInformation(path, 0, null)); + await reporter.message("Parsing feature file: '$path'", MessageLevel.debug); + final lines = + contents.trim().split(RegExp(r"(\r\n|\r|\n)", multiLine: true)); + try { + _parseBlock(FeatureFileSyntax(), featureFile, lines, 0, 0); + } catch (e) { + await reporter.message( + "Error while parsing feature file: '$path'\n$e", MessageLevel.error); + rethrow; + } + return featureFile; + } + + num _parseBlock(SyntaxMatcher parentSyntaxBlock, RunnableBlock parentBlock, + Iterable lines, int lineNumber, int depth) { + for (int i = lineNumber; i < lines.length; i += 1) { + final line = lines.elementAt(i).trim(); + // print("$depth - $line"); + final matcher = syntaxMatchers + .firstWhere((matcher) => matcher.isMatch(line), orElse: () => null); + if (matcher != null) { + if (parentSyntaxBlock.hasBlockEnded(matcher)) { + switch (parentSyntaxBlock.endBlockHandling(matcher)) { + case EndBlockHandling.ignore: + return i; + case EndBlockHandling.continueProcessing: + return i - 1; + } + } + + final runnable = + matcher.toRunnable(line, parentBlock.debug.copyWith(i, line)); + if (runnable is RunnableBlock) { + i = _parseBlock(matcher, runnable, lines, i + 1, depth + 1); + } + + parentBlock.addChild(runnable); + } else { + throw new GherkinSyntaxException( + "Unknown or un-implemented syntax: '$line', file: '${parentBlock.debug.filePath}"); + } + } + + return lines.length; + } +} diff --git a/lib/src/gherkin/runnables/background.dart b/lib/src/gherkin/runnables/background.dart new file mode 100644 index 0000000..5c1adf2 --- /dev/null +++ b/lib/src/gherkin/runnables/background.dart @@ -0,0 +1,7 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; + +class BackgroundRunnable extends ScenarioRunnable { + BackgroundRunnable(String name, RunnableDebugInformation debug) + : super(name, debug); +} diff --git a/lib/src/gherkin/runnables/comment_line.dart b/lib/src/gherkin/runnables/comment_line.dart new file mode 100644 index 0000000..e76ba36 --- /dev/null +++ b/lib/src/gherkin/runnables/comment_line.dart @@ -0,0 +1,12 @@ +import './debug_information.dart'; +import './runnable.dart'; + +class CommentLineRunnable extends Runnable { + final String comment; + + @override + String get name => "Comment Line"; + + CommentLineRunnable(this.comment, RunnableDebugInformation debug) + : super(debug); +} diff --git a/lib/src/gherkin/runnables/debug_information.dart b/lib/src/gherkin/runnables/debug_information.dart new file mode 100644 index 0000000..0cf170a --- /dev/null +++ b/lib/src/gherkin/runnables/debug_information.dart @@ -0,0 +1,11 @@ +class RunnableDebugInformation { + final String filePath; + final int lineNumber; + final String lineText; + + RunnableDebugInformation(this.filePath, this.lineNumber, this.lineText); + + RunnableDebugInformation copyWith(int lineNumber, String line) { + return RunnableDebugInformation(this.filePath, lineNumber, line); + } +} diff --git a/lib/src/gherkin/runnables/empty_line.dart b/lib/src/gherkin/runnables/empty_line.dart new file mode 100644 index 0000000..764c349 --- /dev/null +++ b/lib/src/gherkin/runnables/empty_line.dart @@ -0,0 +1,9 @@ +import './debug_information.dart'; +import './runnable.dart'; + +class EmptyLineRunnable extends Runnable { + @override + String get name => "Empty Line"; + + EmptyLineRunnable(RunnableDebugInformation debug) : super(debug); +} diff --git a/lib/src/gherkin/runnables/feature.dart b/lib/src/gherkin/runnables/feature.dart new file mode 100644 index 0000000..d58ee8c --- /dev/null +++ b/lib/src/gherkin/runnables/feature.dart @@ -0,0 +1,59 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/background.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; + +class FeatureRunnable extends RunnableBlock { + String _name; + String description; + List tags = List(); + BackgroundRunnable background; + List scenarios = List(); + + Map> _tagMap = Map>(); + + FeatureRunnable(this._name, RunnableDebugInformation debug) : super(debug); + + @override + String get name => _name; + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case TextLineRunnable: + description = + "${description == null ? "" : "$description\n"}${(child as TextLineRunnable).text}"; + break; + case TagsRunnable: + tags.addAll((child as TagsRunnable).tags); + _tagMap.putIfAbsent( + child.debug.lineNumber, () => (child as TagsRunnable).tags); + break; + case ScenarioRunnable: + scenarios.add(child); + if (_tagMap.containsKey(child.debug.lineNumber - 1)) { + (child as ScenarioRunnable).addChild( + TagsRunnable(null)..tags = _tagMap[child.debug.lineNumber - 1]); + } + break; + case BackgroundRunnable: + if (background == null) { + background = child; + } else { + throw new GherkinSyntaxException( + "Feature file can only contain one backgroung block. File'${debug.filePath}' :: line '${child.debug.lineNumber}'"); + } + break; + case EmptyLineRunnable: + break; + default: + throw new Exception( + "Unknown runnable child given to Feature '${child.runtimeType}'"); + } + } +} diff --git a/lib/src/gherkin/runnables/feature_file.dart b/lib/src/gherkin/runnables/feature_file.dart new file mode 100644 index 0000000..f87f5ca --- /dev/null +++ b/lib/src/gherkin/runnables/feature_file.dart @@ -0,0 +1,36 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/language.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; + +class FeatureFile extends RunnableBlock { + String _language = "en"; + + List features = new List(); + + FeatureFile(RunnableDebugInformation debug) : super(debug); + + String get langauge => _language; + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case LanguageRunnable: + _language = (child as LanguageRunnable).language; + break; + case FeatureRunnable: + features.add(child); + break; + case EmptyLineRunnable: + break; + default: + throw new Exception( + "Unknown runnable child given to FeatureFile '${child.runtimeType}'"); + } + } + + @override + String get name => debug.filePath; +} diff --git a/lib/src/gherkin/runnables/language.dart b/lib/src/gherkin/runnables/language.dart new file mode 100644 index 0000000..63ff8e2 --- /dev/null +++ b/lib/src/gherkin/runnables/language.dart @@ -0,0 +1,11 @@ +import './debug_information.dart'; +import './runnable.dart'; + +class LanguageRunnable extends Runnable { + String language; + + @override + String get name => "Language"; + + LanguageRunnable(RunnableDebugInformation debug) : super(debug); +} diff --git a/lib/src/gherkin/runnables/multi_line_string.dart b/lib/src/gherkin/runnables/multi_line_string.dart new file mode 100644 index 0000000..7236164 --- /dev/null +++ b/lib/src/gherkin/runnables/multi_line_string.dart @@ -0,0 +1,28 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; + +class MultilineStringRunnable extends RunnableBlock { + List lines = List(); + + @override + String get name => "Multiline String"; + + MultilineStringRunnable(RunnableDebugInformation debug) : super(debug); + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case TextLineRunnable: + lines.add((child as TextLineRunnable).text); + break; + case EmptyLineRunnable: + break; + default: + throw new Exception( + "Unknown runnable child given to Multiline string '${child.runtimeType}'"); + } + } +} diff --git a/lib/src/gherkin/runnables/runnable.dart b/lib/src/gherkin/runnables/runnable.dart new file mode 100644 index 0000000..698bfd8 --- /dev/null +++ b/lib/src/gherkin/runnables/runnable.dart @@ -0,0 +1,9 @@ +import './debug_information.dart'; + +abstract class Runnable { + final RunnableDebugInformation debug; + + Runnable(this.debug); + + String get name; +} diff --git a/lib/src/gherkin/runnables/runnable_block.dart b/lib/src/gherkin/runnables/runnable_block.dart new file mode 100644 index 0000000..869c9c8 --- /dev/null +++ b/lib/src/gherkin/runnables/runnable_block.dart @@ -0,0 +1,9 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; + +import './runnable.dart'; + +abstract class RunnableBlock extends Runnable { + RunnableBlock(RunnableDebugInformation debug) : super(debug); + + void addChild(Runnable child); +} diff --git a/lib/src/gherkin/runnables/runnable_result.dart b/lib/src/gherkin/runnables/runnable_result.dart new file mode 100644 index 0000000..f7d34fd --- /dev/null +++ b/lib/src/gherkin/runnables/runnable_result.dart @@ -0,0 +1,9 @@ +enum RunnableResultState { ignored, skipped, failed, passed } + +class RunnableResult { + final RunnableResultState state; + final dynamic result; + final Exception error; + + RunnableResult(this.state, {this.result, this.error}); +} diff --git a/lib/src/gherkin/runnables/scenario.dart b/lib/src/gherkin/runnables/scenario.dart new file mode 100644 index 0000000..1eb9159 --- /dev/null +++ b/lib/src/gherkin/runnables/scenario.dart @@ -0,0 +1,36 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/comment_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; + +class ScenarioRunnable extends RunnableBlock { + String _name; + List tags = List(); + List steps = new List(); + + ScenarioRunnable(this._name, RunnableDebugInformation debug) : super(debug); + + @override + String get name => _name; + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case StepRunnable: + steps.add(child); + break; + case TagsRunnable: + tags.addAll((child as TagsRunnable).tags); + break; + case CommentLineRunnable: + case EmptyLineRunnable: + break; + default: + throw new Exception( + "Unknown runnable child given to Scenario '${child.runtimeType}'"); + } + } +} diff --git a/lib/src/gherkin/runnables/step.dart b/lib/src/gherkin/runnables/step.dart new file mode 100644 index 0000000..8c00ecf --- /dev/null +++ b/lib/src/gherkin/runnables/step.dart @@ -0,0 +1,39 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; +import 'package:flutter_gherkin/src/gherkin/models/table.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/multi_line_string.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/table.dart'; + +class StepRunnable extends RunnableBlock { + String _name; + String description; + List multilineStrings = List(); + Table table; + + StepRunnable(this._name, RunnableDebugInformation debug) : super(debug); + + @override + String get name => _name; + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case MultilineStringRunnable: + multilineStrings + .add((child as MultilineStringRunnable).lines.join("\n")); + break; + case TableRunnable: + if (table != null) + throw new GherkinSyntaxException( + "Only a single table can be added to the step '$name'"); + + table = (child as TableRunnable).toTable(); + break; + default: + throw new Exception( + "Unknown runnable child given to Step '${child.runtimeType}'"); + } + } +} diff --git a/lib/src/gherkin/runnables/table.dart b/lib/src/gherkin/runnables/table.dart new file mode 100644 index 0000000..e8e1fae --- /dev/null +++ b/lib/src/gherkin/runnables/table.dart @@ -0,0 +1,53 @@ +import 'package:flutter_gherkin/src/gherkin/models/table.dart'; +import 'package:flutter_gherkin/src/gherkin/models/table_row.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/comment_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable_block.dart'; + +class TableRunnable extends RunnableBlock { + final List rows = List(); + + @override + String get name => "Table"; + + TableRunnable(RunnableDebugInformation debug) : super(debug); + + @override + void addChild(Runnable child) { + switch (child.runtimeType) { + case TableRunnable: + rows.addAll((child as TableRunnable).rows); + break; + case CommentLineRunnable: + break; + default: + throw new Exception( + "Unknown runnable child given to Table '${child.runtimeType}'"); + } + } + + Table toTable() { + TableRow header; + List tableRows = List(); + if (rows.length > 1) { + header = _toRow(rows.first, 0, true); + } + + for (var i = (header == null ? 0 : 1); i < rows.length; i += 1) { + tableRows.add(_toRow(rows.elementAt(i), i)); + } + + return Table(tableRows, header); + } + + TableRow _toRow(String raw, int rowIndex, [isHeaderRow = false]) { + return TableRow( + raw + .split(RegExp(r"\|")) + .map((c) => c.trim()) + .where((c) => c.isNotEmpty), + rowIndex, + isHeaderRow); + } +} diff --git a/lib/src/gherkin/runnables/tags.dart b/lib/src/gherkin/runnables/tags.dart new file mode 100644 index 0000000..18ed12d --- /dev/null +++ b/lib/src/gherkin/runnables/tags.dart @@ -0,0 +1,11 @@ +import './debug_information.dart'; +import './runnable.dart'; + +class TagsRunnable extends Runnable { + Iterable tags; + + @override + String get name => "Tags"; + + TagsRunnable(RunnableDebugInformation debug) : super(debug); +} diff --git a/lib/src/gherkin/runnables/text_line.dart b/lib/src/gherkin/runnables/text_line.dart new file mode 100644 index 0000000..6d4f934 --- /dev/null +++ b/lib/src/gherkin/runnables/text_line.dart @@ -0,0 +1,11 @@ +import './debug_information.dart'; +import './runnable.dart'; + +class TextLineRunnable extends Runnable { + String text; + + @override + String get name => "Language"; + + TextLineRunnable(RunnableDebugInformation debug) : super(debug); +} diff --git a/lib/src/gherkin/step_template_matcher.dart b/lib/src/gherkin/step_template_matcher.dart new file mode 100644 index 0000000..74aa551 --- /dev/null +++ b/lib/src/gherkin/step_template_matcher.dart @@ -0,0 +1,28 @@ +// import 'package:flutter_gherkin/src/gherkin/steps/step_definition.dart'; + +// class MatchedStepDefinition { +// final StepDefinitionGeneric stepDefinition; +// final Iterable args; + +// MatchedStepDefinition(this.stepDefinition, this.args); +// } + +// class CustomMatchExpression { + +// } + +// class StepDefinitionMatcher { +// final Map(); + +// void registerStepDefintiion(StepDefinitionGeneric definition) { +// _stepDefinitions.add(definition); +// } + +// MatchedStepDefinition matchToStep(String stepText) { +// final stepDefinition = _stepDefinitions +// .firstWhere((definition) => definition.pattern.hasMatch(stepText)); +// if (stepDefinition != null) { +// } else {} +// } +// } diff --git a/lib/src/gherkin/steps/and.dart b/lib/src/gherkin/steps/and.dart new file mode 100644 index 0000000..d90d963 --- /dev/null +++ b/lib/src/gherkin/steps/and.dart @@ -0,0 +1,95 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class And extends StepDefinition { + And([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class And1WithWorld + extends StepDefinition1 { + And1WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode1 get code => (a) async => await executeStep(a); + + Future executeStep(TInput1 input1); +} + +abstract class And1 extends And1WithWorld { + And1([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class And2WithWorld + extends StepDefinition2 { + And2WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode2 get code => + (a, b) async => await executeStep(a, b); + + Future executeStep(TInput1 input1, TInput2 input2); +} + +abstract class And2 + extends And2WithWorld { + And2([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class And3WithWorld + extends StepDefinition3 { + And3WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode3 get code => + (a, b, c) async => await executeStep(a, b, c); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3); +} + +abstract class And3 + extends And3WithWorld { + And3([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class And4WithWorld + extends StepDefinition4 { + And4WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode4 get code => + (a, b, c, d) async => await executeStep(a, b, c, d); + + Future executeStep( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); +} + +abstract class And4 + extends And4WithWorld { + And4([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class And5WithWorld + extends StepDefinition5 { + And5WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode5 get code => + (a, b, c, d, e) async => await executeStep(a, b, c, d, e); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3, + TInput4 input4, TInput5 input5); +} + +abstract class And5 + extends And5WithWorld { + And5([StepDefinitionConfiguration configuration]) : super(configuration); +} diff --git a/lib/src/gherkin/steps/but.dart b/lib/src/gherkin/steps/but.dart new file mode 100644 index 0000000..4f83fe0 --- /dev/null +++ b/lib/src/gherkin/steps/but.dart @@ -0,0 +1,95 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class But extends StepDefinition { + But([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class But1WithWorld + extends StepDefinition1 { + But1WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode1 get code => (a) async => await executeStep(a); + + Future executeStep(TInput1 input1); +} + +abstract class But1 extends But1WithWorld { + But1([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class But2WithWorld + extends StepDefinition2 { + But2WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode2 get code => + (a, b) async => await executeStep(a, b); + + Future executeStep(TInput1 input1, TInput2 input2); +} + +abstract class But2 + extends But2WithWorld { + But2([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class But3WithWorld + extends StepDefinition3 { + But3WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode3 get code => + (a, b, c) async => await executeStep(a, b, c); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3); +} + +abstract class But3 + extends But3WithWorld { + But3([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class But4WithWorld + extends StepDefinition4 { + But4WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode4 get code => + (a, b, c, d) async => await executeStep(a, b, c, d); + + Future executeStep( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); +} + +abstract class But4 + extends But4WithWorld { + But4([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class But5WithWorld + extends StepDefinition5 { + But5WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode5 get code => + (a, b, c, d, e) async => await executeStep(a, b, c, d, e); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3, + TInput4 input4, TInput5 input5); +} + +abstract class But5 + extends But5WithWorld { + But5([StepDefinitionConfiguration configuration]) : super(configuration); +} diff --git a/lib/src/gherkin/steps/exectuable_step.dart b/lib/src/gherkin/steps/exectuable_step.dart new file mode 100644 index 0000000..22c02d6 --- /dev/null +++ b/lib/src/gherkin/steps/exectuable_step.dart @@ -0,0 +1,10 @@ +import 'package:flutter_gherkin/src/gherkin/expressions/gherkin_expression.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +class ExectuableStep { + final GherkinExpression expression; + final StepDefinitionGeneric step; + + ExectuableStep(this.expression, this.step); +} diff --git a/lib/src/gherkin/steps/given.dart b/lib/src/gherkin/steps/given.dart new file mode 100644 index 0000000..b57dff0 --- /dev/null +++ b/lib/src/gherkin/steps/given.dart @@ -0,0 +1,106 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class GivenWithWorld + extends StepDefinition { + GivenWithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode get code => () async => await executeStep(); + + Future executeStep(); +} + +abstract class Given extends GivenWithWorld { + Given([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Given1WithWorld + extends StepDefinition1 { + Given1WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode1 get code => (a) async => await executeStep(a); + + Future executeStep(TInput1 input1); +} + +abstract class Given1 extends Given1WithWorld { + Given1([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Given2WithWorld + extends StepDefinition2 { + Given2WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode2 get code => + (a, b) async => await executeStep(a, b); + + Future executeStep(TInput1 input1, TInput2 input2); +} + +abstract class Given2 + extends Given2WithWorld { + Given2([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Given3WithWorld + extends StepDefinition3 { + Given3WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode3 get code => + (a, b, c) async => await executeStep(a, b, c); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3); +} + +abstract class Given3 + extends Given3WithWorld { + Given3([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Given4WithWorld + extends StepDefinition4 { + Given4WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode4 get code => + (a, b, c, d) async => await executeStep(a, b, c, d); + + Future executeStep( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); +} + +abstract class Given4 + extends Given4WithWorld { + Given4([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Given5WithWorld + extends StepDefinition5 { + Given5WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode5 get code => + (a, b, c, d, e) async => await executeStep(a, b, c, d, e); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3, + TInput4 input4, TInput5 input5); +} + +abstract class Given5 + extends Given5WithWorld { + Given5([StepDefinitionConfiguration configuration]) : super(configuration); +} diff --git a/lib/src/gherkin/steps/step_configuration.dart b/lib/src/gherkin/steps/step_configuration.dart new file mode 100644 index 0000000..c510171 --- /dev/null +++ b/lib/src/gherkin/steps/step_configuration.dart @@ -0,0 +1,3 @@ +class StepDefinitionConfiguration { + Duration timeout; +} diff --git a/lib/src/gherkin/steps/step_definition.dart b/lib/src/gherkin/steps/step_definition.dart new file mode 100644 index 0000000..307efdf --- /dev/null +++ b/lib/src/gherkin/steps/step_definition.dart @@ -0,0 +1,59 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/parameter_count_mismatch_error.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_run_result.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; +import 'package:flutter_gherkin/src/utils/perf.dart'; +import 'package:test/test.dart'; +import 'dart:async'; + +abstract class StepDefinitionGeneric { + final StepDefinitionConfiguration config; + final int _expectParameterCount; + TWorld _world; + Reporter _reporter; + Duration _timeout; + RegExp get pattern; + + StepDefinitionGeneric(this.config, this._expectParameterCount) { + this._timeout = this.config?.timeout; + } + + TWorld get world => _world; + Duration get timeout => _timeout; + Reporter get reporter => _reporter; + + Future run(TWorld world, Reporter reporter, + Duration defaultTimeout, Iterable parameters) async { + _ensureParameterCount(parameters.length, _expectParameterCount); + int elapsedMilliseconds; + try { + await Perf.measure(() async { + _world = world; + _reporter = reporter; + _timeout = _timeout ?? defaultTimeout; + final result = await onRun(parameters).timeout(_timeout); + return result; + }, (ms) => elapsedMilliseconds = ms); + } on TestFailure catch (tf) { + return StepResult( + elapsedMilliseconds, StepExecutionResult.fail, tf.message); + } on TimeoutException catch (te, st) { + return ErroredStepResult( + elapsedMilliseconds, StepExecutionResult.timeout, te, st); + } catch (e, st) { + return ErroredStepResult( + elapsedMilliseconds, StepExecutionResult.error, e, st); + } + + return StepResult(elapsedMilliseconds, StepExecutionResult.pass); + } + + Future onRun(Iterable parameters); + + void _ensureParameterCount(int actual, int expected) { + if (actual != expected) + throw GherkinStepParameterMismatchException( + this.runtimeType, expected, actual); + } +} diff --git a/lib/src/gherkin/steps/step_definition_implementations.dart b/lib/src/gherkin/steps/step_definition_implementations.dart new file mode 100644 index 0000000..2a42d17 --- /dev/null +++ b/lib/src/gherkin/steps/step_definition_implementations.dart @@ -0,0 +1,91 @@ +import 'package:flutter_gherkin/src/expect/expect_mimic.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class StepDefinitionBase + extends StepDefinitionGeneric { + TStepDefinitionCode get code; + + StepDefinitionBase( + StepDefinitionConfiguration config, int expectParameterCount) + : super(config, expectParameterCount); + + void expect(actual, matcher, {String reason}) => + ExpectMimic().expect(actual, matcher, reason: reason); + + void expectA(actual, matcher, {String reason}) => + ExpectMimic().expect(actual, matcher, reason: reason); + + void expectMatch(actual, matcher, {String reason}) => + expect(actual, matcher, reason: reason); +} + +abstract class StepDefinition + extends StepDefinitionBase { + StepDefinition([StepDefinitionConfiguration configuration]) + : super(configuration, 0); + + Future onRun(Iterable parameters) async => await code(); +} + +abstract class StepDefinition1 + extends StepDefinitionBase> { + StepDefinition1([StepDefinitionConfiguration configuration]) + : super(configuration, 1); + + Future onRun(Iterable parameters) async => + await code(parameters.elementAt(0)); +} + +abstract class StepDefinition2 + extends StepDefinitionBase> { + StepDefinition2([StepDefinitionConfiguration configuration]) + : super(configuration, 2); + + Future onRun(Iterable parameters) async => + await code(parameters.elementAt(0), parameters.elementAt(1)); +} + +abstract class StepDefinition3 + extends StepDefinitionBase> { + StepDefinition3([StepDefinitionConfiguration configuration]) + : super(configuration, 3); + + Future onRun(Iterable parameters) async => await code( + parameters.elementAt(0), + parameters.elementAt(1), + parameters.elementAt(2)); +} + +abstract class StepDefinition4 + extends StepDefinitionBase> { + StepDefinition4([StepDefinitionConfiguration configuration]) + : super(configuration, 4); + + Future onRun(Iterable parameters) async => await code( + parameters.elementAt(0), + parameters.elementAt(1), + parameters.elementAt(2), + parameters.elementAt(3)); +} + +abstract class StepDefinition5 + extends StepDefinitionBase> { + StepDefinition5([StepDefinitionConfiguration configuration]) + : super(configuration, 5); + + Future onRun(Iterable parameters) async => await code( + parameters.elementAt(0), + parameters.elementAt(1), + parameters.elementAt(2), + parameters.elementAt(3), + parameters.elementAt(4)); +} diff --git a/lib/src/gherkin/steps/step_functions.dart b/lib/src/gherkin/steps/step_functions.dart new file mode 100644 index 0000000..fe694c1 --- /dev/null +++ b/lib/src/gherkin/steps/step_functions.dart @@ -0,0 +1,21 @@ +typedef Future StepDefinitionCode(); + +typedef Future StepDefinitionCode1(TInput1 input1); + +typedef Future StepDefinitionCode2( + TInput1 input1, TInput2 input2); + +typedef Future StepDefinitionCode3( + TInput1 input1, TInput2 input2, TInput3 input3); + +typedef Future StepDefinitionCode4( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); + +typedef Future StepDefinitionCode5( + TInput1 input1, + TInput2 input2, + TInput3 input3, + TInput4 input4, + TInput5 input5); + +// at this point a table should be considered diff --git a/lib/src/gherkin/steps/step_run_result.dart b/lib/src/gherkin/steps/step_run_result.dart new file mode 100644 index 0000000..2311397 --- /dev/null +++ b/lib/src/gherkin/steps/step_run_result.dart @@ -0,0 +1,22 @@ +enum StepExecutionResult { pass, fail, skipped, timeout, error } + +class StepResult { + /// the duration in milliseconds the step took to run + final int elapsedMilliseconds; + + /// the result of executing the step + final StepExecutionResult result; + // a reason for the result. This would be a failure message if the result failed. This field can be null + final String resultReason; + + StepResult(this.elapsedMilliseconds, this.result, [this.resultReason]); +} + +class ErroredStepResult extends StepResult { + final Exception exception; + final StackTrace stackTrace; + + ErroredStepResult(int elapsedMilliseconds, StepExecutionResult result, + this.exception, this.stackTrace) + : super(elapsedMilliseconds, result); +} diff --git a/lib/src/gherkin/steps/then.dart b/lib/src/gherkin/steps/then.dart new file mode 100644 index 0000000..47a47cf --- /dev/null +++ b/lib/src/gherkin/steps/then.dart @@ -0,0 +1,95 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class Then extends StepDefinition { + Then([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Then1WithWorld + extends StepDefinition1 { + Then1WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode1 get code => (a) async => await executeStep(a); + + Future executeStep(TInput1 input1); +} + +abstract class Then1 extends Then1WithWorld { + Then1([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Then2WithWorld + extends StepDefinition2 { + Then2WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode2 get code => + (a, b) async => await executeStep(a, b); + + Future executeStep(TInput1 input1, TInput2 input2); +} + +abstract class Then2 + extends Then2WithWorld { + Then2([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Then3WithWorld + extends StepDefinition3 { + Then3WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode3 get code => + (a, b, c) async => await executeStep(a, b, c); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3); +} + +abstract class Then3 + extends Then3WithWorld { + Then3([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Then4WithWorld + extends StepDefinition4 { + Then4WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode4 get code => + (a, b, c, d) async => await executeStep(a, b, c, d); + + Future executeStep( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); +} + +abstract class Then4 + extends Then4WithWorld { + Then4([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class Then5WithWorld + extends StepDefinition5 { + Then5WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode5 get code => + (a, b, c, d, e) async => await executeStep(a, b, c, d, e); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3, + TInput4 input4, TInput5 input5); +} + +abstract class Then5 + extends Then5WithWorld { + Then5([StepDefinitionConfiguration configuration]) : super(configuration); +} diff --git a/lib/src/gherkin/steps/when.dart b/lib/src/gherkin/steps/when.dart new file mode 100644 index 0000000..eccb06b --- /dev/null +++ b/lib/src/gherkin/steps/when.dart @@ -0,0 +1,95 @@ +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition_implementations.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_functions.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/world.dart'; + +abstract class When extends StepDefinition { + When([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class When1WithWorld + extends StepDefinition1 { + When1WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + @override + StepDefinitionCode1 get code => (a) async => await executeStep(a); + + Future executeStep(TInput1 input1); +} + +abstract class When1 extends When1WithWorld { + When1([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class When2WithWorld + extends StepDefinition2 { + When2WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode2 get code => + (a, b) async => await executeStep(a, b); + + Future executeStep(TInput1 input1, TInput2 input2); +} + +abstract class When2 + extends When2WithWorld { + When2([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class When3WithWorld + extends StepDefinition3 { + When3WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode3 get code => + (a, b, c) async => await executeStep(a, b, c); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3); +} + +abstract class When3 + extends When3WithWorld { + When3([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class When4WithWorld + extends StepDefinition4 { + When4WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode4 get code => + (a, b, c, d) async => await executeStep(a, b, c, d); + + Future executeStep( + TInput1 input1, TInput2 input2, TInput3 input3, TInput4 input4); +} + +abstract class When4 + extends When4WithWorld { + When4([StepDefinitionConfiguration configuration]) : super(configuration); +} + +abstract class When5WithWorld + extends StepDefinition5 { + When5WithWorld([StepDefinitionConfiguration configuration]) + : super(configuration); + + @override + StepDefinitionCode5 get code => + (a, b, c, d, e) async => await executeStep(a, b, c, d, e); + + Future executeStep(TInput1 input1, TInput2 input2, TInput3 input3, + TInput4 input4, TInput5 input5); +} + +abstract class When5 + extends When5WithWorld { + When5([StepDefinitionConfiguration configuration]) : super(configuration); +} diff --git a/lib/src/gherkin/steps/world.dart b/lib/src/gherkin/steps/world.dart new file mode 100644 index 0000000..cfb6175 --- /dev/null +++ b/lib/src/gherkin/steps/world.dart @@ -0,0 +1,3 @@ +class World { + void dispose() {} +} diff --git a/lib/src/gherkin/syntax/background_syntax.dart b/lib/src/gherkin/syntax/background_syntax.dart new file mode 100644 index 0000000..2a44ea9 --- /dev/null +++ b/lib/src/gherkin/syntax/background_syntax.dart @@ -0,0 +1,29 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/background.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/empty_line_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/scenario_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/tag_syntax.dart'; + +class BackgroundSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp(r"^\s*Background:\s*(.+)\s*$", + multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) => + syntax is ScenarioSyntax || + syntax is EmptyLineSyntax || + syntax is TagSyntax; + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final name = pattern.firstMatch(line).group(1); + final runnable = new BackgroundRunnable(name, debug); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/comment_syntax.dart b/lib/src/gherkin/syntax/comment_syntax.dart new file mode 100644 index 0000000..172f30c --- /dev/null +++ b/lib/src/gherkin/syntax/comment_syntax.dart @@ -0,0 +1,12 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/comment_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; + +class CommentSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp("^#", multiLine: false, caseSensitive: false); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) => + CommentLineRunnable(line.trim(), debug); +} diff --git a/lib/src/gherkin/syntax/empty_line_syntax.dart b/lib/src/gherkin/syntax/empty_line_syntax.dart new file mode 100644 index 0000000..5bcca72 --- /dev/null +++ b/lib/src/gherkin/syntax/empty_line_syntax.dart @@ -0,0 +1,13 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; + +class EmptyLineSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = + RegExp(r"^\s*$", multiLine: false, caseSensitive: false); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) => + EmptyLineRunnable(debug); +} diff --git a/lib/src/gherkin/syntax/feature_file_syntax.dart b/lib/src/gherkin/syntax/feature_file_syntax.dart new file mode 100644 index 0000000..21125f4 --- /dev/null +++ b/lib/src/gherkin/syntax/feature_file_syntax.dart @@ -0,0 +1,14 @@ +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; + +class FeatureFileSyntax extends SyntaxMatcher { + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) => false; + + @override + bool isMatch(String line) { + return false; + } +} diff --git a/lib/src/gherkin/syntax/feature_syntax.dart b/lib/src/gherkin/syntax/feature_syntax.dart new file mode 100644 index 0000000..160b685 --- /dev/null +++ b/lib/src/gherkin/syntax/feature_syntax.dart @@ -0,0 +1,23 @@ +import '../runnables/debug_information.dart'; +import '../runnables/runnable.dart'; +import '../runnables/feature.dart'; +import './syntax_matcher.dart'; +import './regex_matched_syntax.dart'; + +class FeatureSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = + RegExp(r"^Feature:\s*(.+)\s*", multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) => false; + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final name = pattern.firstMatch(line).group(1); + final runnable = new FeatureRunnable(name, debug); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/language_syntax.dart b/lib/src/gherkin/syntax/language_syntax.dart new file mode 100644 index 0000000..b04fc53 --- /dev/null +++ b/lib/src/gherkin/syntax/language_syntax.dart @@ -0,0 +1,17 @@ +import '../runnables/debug_information.dart'; +import '../runnables/language.dart'; +import '../runnables/runnable.dart'; +import './regex_matched_syntax.dart'; + +/// see https://docs.cucumber.io/gherkin/reference/#gherkin-dialects +class LanguageSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp(r"^\s*#\s*language:\s*([a-z]{2,7})\s*$", + multiLine: false, caseSensitive: false); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new LanguageRunnable(debug); + runnable.language = pattern.firstMatch(line).group(1); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/multiline_string_syntax.dart b/lib/src/gherkin/syntax/multiline_string_syntax.dart new file mode 100644 index 0000000..246a8ab --- /dev/null +++ b/lib/src/gherkin/syntax/multiline_string_syntax.dart @@ -0,0 +1,39 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/multi_line_string.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/text_line_syntax.dart'; + +class MultilineStringSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp(r"^\s*(" + '"""' + r"|'''|```)\s*$", + multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) { + if (syntax is MultilineStringSyntax) { + return true; + } else if (!(syntax is TextLineSyntax || syntax is CommentSyntax)) { + throw GherkinSyntaxException( + "Multiline string block does not expect ${syntax.runtimeType} syntax. Expects a text line"); + } + return false; + } + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new MultilineStringRunnable(debug); + return runnable; + } + + @override + EndBlockHandling endBlockHandling(SyntaxMatcher syntax) => + syntax is MultilineStringSyntax + ? EndBlockHandling.ignore + : EndBlockHandling.continueProcessing; +} diff --git a/lib/src/gherkin/syntax/regex_matched_syntax.dart b/lib/src/gherkin/syntax/regex_matched_syntax.dart new file mode 100644 index 0000000..ef80309 --- /dev/null +++ b/lib/src/gherkin/syntax/regex_matched_syntax.dart @@ -0,0 +1,11 @@ +import './syntax_matcher.dart'; + +abstract class RegExMatchedGherkinSyntax extends SyntaxMatcher { + RegExp get pattern; + + @override + bool isMatch(String line) { + final match = pattern.hasMatch(line); + return match; + } +} diff --git a/lib/src/gherkin/syntax/scenario_syntax.dart b/lib/src/gherkin/syntax/scenario_syntax.dart new file mode 100644 index 0000000..949e167 --- /dev/null +++ b/lib/src/gherkin/syntax/scenario_syntax.dart @@ -0,0 +1,25 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/tag_syntax.dart'; + +class ScenarioSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp(r"^\s*Scenario:\s*(.+)\s*$", + multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) => + syntax is ScenarioSyntax || syntax is TagSyntax; + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final name = pattern.firstMatch(line).group(1); + final runnable = new ScenarioRunnable(name, debug); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/step_syntax.dart b/lib/src/gherkin/syntax/step_syntax.dart new file mode 100644 index 0000000..e93d4ad --- /dev/null +++ b/lib/src/gherkin/syntax/step_syntax.dart @@ -0,0 +1,25 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/table_line_syntax.dart'; + +class StepSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp(r"^(given|then|when|and|but)\s.*", + multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) => + !(syntax is MultilineStringSyntax || syntax is TableLineSyntax); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new StepRunnable(line, debug); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/syntax_matcher.dart b/lib/src/gherkin/syntax/syntax_matcher.dart new file mode 100644 index 0000000..09c8dee --- /dev/null +++ b/lib/src/gherkin/syntax/syntax_matcher.dart @@ -0,0 +1,13 @@ +import '../runnables/debug_information.dart'; +import '../runnables/runnable.dart'; + +enum EndBlockHandling { ignore, continueProcessing } + +abstract class SyntaxMatcher { + bool isMatch(String line); + bool get isBlockSyntax => false; + bool hasBlockEnded(SyntaxMatcher syntax) => true; + EndBlockHandling endBlockHandling(SyntaxMatcher syntax) => + EndBlockHandling.continueProcessing; + Runnable toRunnable(String line, RunnableDebugInformation debug) => null; +} diff --git a/lib/src/gherkin/syntax/table_line_syntax.dart b/lib/src/gherkin/syntax/table_line_syntax.dart new file mode 100644 index 0000000..cb0e3ed --- /dev/null +++ b/lib/src/gherkin/syntax/table_line_syntax.dart @@ -0,0 +1,29 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/table.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/syntax_matcher.dart'; + +class TableLineSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = + RegExp(r"^\s*\|.*\|\s*$", multiLine: false, caseSensitive: false); + + @override + bool get isBlockSyntax => true; + + @override + bool hasBlockEnded(SyntaxMatcher syntax) { + if (syntax is TableLineSyntax || syntax is CommentSyntax) { + return false; + } + return true; + } + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new TableRunnable(debug); + runnable.rows.add(line.trim()); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/tag_syntax.dart b/lib/src/gherkin/syntax/tag_syntax.dart new file mode 100644 index 0000000..1307225 --- /dev/null +++ b/lib/src/gherkin/syntax/tag_syntax.dart @@ -0,0 +1,19 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; + +class TagSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = RegExp("^@", multiLine: false, caseSensitive: false); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new TagsRunnable(debug); + runnable.tags = line + .trim() + .split(RegExp("@")) + .map((t) => t.trim()) + .where((t) => t != null && t.isNotEmpty); + return runnable; + } +} diff --git a/lib/src/gherkin/syntax/text_line_syntax.dart b/lib/src/gherkin/syntax/text_line_syntax.dart new file mode 100644 index 0000000..e5ccc12 --- /dev/null +++ b/lib/src/gherkin/syntax/text_line_syntax.dart @@ -0,0 +1,16 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/regex_matched_syntax.dart'; + +class TextLineSyntax extends RegExMatchedGherkinSyntax { + final RegExp pattern = + RegExp(r"^\s*(?!#)\w+[\s|\w]*$", multiLine: false, caseSensitive: false); + + @override + Runnable toRunnable(String line, RunnableDebugInformation debug) { + final runnable = new TextLineRunnable(debug); + runnable.text = line.trim(); + return runnable; + } +} diff --git a/lib/src/hooks/aggregated_hook.dart b/lib/src/hooks/aggregated_hook.dart new file mode 100644 index 0000000..a390d25 --- /dev/null +++ b/lib/src/hooks/aggregated_hook.dart @@ -0,0 +1,35 @@ +import 'package:flutter_gherkin/src/configuration.dart'; +import 'package:flutter_gherkin/src/hooks/hook.dart'; + +class AggregatedHook extends Hook { + Iterable _orderedHooks; + + void addHooks(Iterable hooks) { + _orderedHooks = hooks.toList()..sort((a, b) => b.priority - a.priority); + } + + Future onBeforeRun(TestConfiguration config) async => + await _invokeHooks((h) => h.onBeforeRun(config)); + + /// Run after all scenerios in a test run have completed + Future onAfterRun(TestConfiguration config) async => + await _invokeHooks((h) => h.onAfterRun(config)); + + /// Run before a scenario and it steps are executed + Future onBeforeScenario( + TestConfiguration config, String scenario) async => + await _invokeHooks((h) => h.onBeforeScenario(config, scenario)); + + /// Run after a scenario has executed + Future onAfterScenario( + TestConfiguration config, String scenario) async => + await _invokeHooks((h) => h.onAfterScenario(config, scenario)); + + Future _invokeHooks(Future invoke(Hook h)) async { + if (_orderedHooks != null && _orderedHooks.length > 0) { + for (var hook in _orderedHooks) { + await invoke(hook); + } + } + } +} diff --git a/lib/src/hooks/hook.dart b/lib/src/hooks/hook.dart new file mode 100644 index 0000000..f87f404 --- /dev/null +++ b/lib/src/hooks/hook.dart @@ -0,0 +1,23 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +/// A hook that is run during certain points in the execution cycle +/// You can override any or none of the methods +abstract class 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 + int get priority => 0; + + /// Run before any scenario in a test run have executed + Future onBeforeRun(TestConfiguration config) => Future.value(null); + + /// Run after all scenerios in a test run have completed + Future onAfterRun(TestConfiguration config) => Future.value(null); + + /// Run before a scenario and it steps are executed + Future onBeforeScenario(TestConfiguration config, String scenario) => + Future.value(null); + + /// Run after a scenario has executed + Future onAfterScenario(TestConfiguration config, String scenario) => + Future.value(null); +} diff --git a/lib/src/processes/process_handler.dart b/lib/src/processes/process_handler.dart new file mode 100644 index 0000000..2cf77c7 --- /dev/null +++ b/lib/src/processes/process_handler.dart @@ -0,0 +1,4 @@ +abstract class ProcessHandler { + Future run(); + Future terminate(); +} diff --git a/lib/src/reporters/aggregated_reporter.dart b/lib/src/reporters/aggregated_reporter.dart new file mode 100644 index 0000000..c92d741 --- /dev/null +++ b/lib/src/reporters/aggregated_reporter.dart @@ -0,0 +1,75 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; + +class AggregatedReporter extends Reporter { + final List _reporters = new List(); + + void addReporter(Reporter reporter) => _reporters.add(reporter); + + @override + Future message(String message, MessageLevel level) async { + await _invokeReporters((r) async => await r.message(message, level)); + } + + @override + Future onTestRunStarted() async { + await _invokeReporters((r) async => await r.onTestRunStarted()); + } + + @override + Future onTestRunfinished() async { + await _invokeReporters((r) async => await r.onTestRunfinished()); + } + + @override + Future onFeatureStarted(StartedMessage message) async { + await _invokeReporters((r) async => await r.onFeatureStarted(message)); + } + + @override + Future onFeatureFinished(FinishedMessage message) async { + await _invokeReporters((r) async => await r.onFeatureFinished(message)); + } + + @override + Future onScenarioStarted(StartedMessage message) async { + await _invokeReporters((r) async => await r.onScenarioStarted(message)); + } + + @override + Future onScenarioFinished(FinishedMessage message) async { + await _invokeReporters((r) async => await r.onScenarioFinished(message)); + } + + @override + Future onStepStarted(StartedMessage message) async { + await _invokeReporters((r) async => await r.onStepStarted(message)); + } + + @override + Future onStepFinished(StepFinishedMessage message) async { + await _invokeReporters((r) async => await r.onStepFinished(message)); + } + + @override + Future onException(Exception exception, StackTrace stackTrace) async { + await _invokeReporters( + (r) async => await r.onException(exception, stackTrace)); + } + + @override + Future dispose() async { + await _invokeReporters((r) async => await r.dispose()); + } + + Future _invokeReporters(Future invoke(Reporter r)) async { + if (_reporters != null && _reporters.length > 0) { + for (var reporter in _reporters) { + try { + await invoke(reporter); + } catch (e) {} + } + } + } +} diff --git a/lib/src/reporters/message_level.dart b/lib/src/reporters/message_level.dart new file mode 100644 index 0000000..ae8ff00 --- /dev/null +++ b/lib/src/reporters/message_level.dart @@ -0,0 +1 @@ +enum MessageLevel { verbose, debug, info, warning, error } diff --git a/lib/src/reporters/messages.dart b/lib/src/reporters/messages.dart new file mode 100644 index 0000000..ce24fdb --- /dev/null +++ b/lib/src/reporters/messages.dart @@ -0,0 +1,28 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_run_result.dart'; + +enum Target { run, feature, scenario, step } + +class StartedMessage { + final Target target; + final String name; + final RunnableDebugInformation context; + + StartedMessage(this.target, this.name, this.context); +} + +class FinishedMessage { + final Target target; + final String name; + final RunnableDebugInformation context; + + FinishedMessage(this.target, this.name, this.context); +} + +class StepFinishedMessage extends FinishedMessage { + final StepResult result; + + StepFinishedMessage( + String name, RunnableDebugInformation context, this.result) + : super(Target.step, name, context); +} diff --git a/lib/src/reporters/progress_reporter.dart b/lib/src/reporters/progress_reporter.dart new file mode 100644 index 0000000..fa3770f --- /dev/null +++ b/lib/src/reporters/progress_reporter.dart @@ -0,0 +1,52 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/messages.dart'; + +class ProgressReporter extends StdoutReporter { + Future onStepFinished(FinishedMessage message) async {} + + Future message(String message, MessageLevel level) async { + // ignore messages + } + + String getStatePrefixIcon() { + return "√|×|e!"; + } + + String getContext(RunnableDebugInformation context) { + return "# ${context.filePath}:${context.lineNumber}"; + } +} +// √ And I click on the "change job" link # src\step-definitions\interactions\click-on-element.step.ts:13 +// × And I fill the "finish date" field with "1 December 2020" # src\step-definitions\interactions\fill-the-field-with.step.ts:11 +// WebDriverError: unknown error: cannot focus element +// (Session info: chrome=70.0.3538.67) +// (Driver info: chromedriver=2.43.600210 (68dcf5eebde37173d4027fa8635e332711d2874a),platform=Windows NT 10.0.16299 x86_64) +// at Object.checkLegacyResponse (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\error.js:546:15) +// at parseHttpResponse (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\http.js:509:13) +// at doSend.then.response (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\http.js:441:30) +// at process._tickCallback (internal/process/next_tick.js:68:7) +// From: Task: WebElement.sendKeys() +// at thenableWebDriverProxy.schedule (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\webdriver.js:807:17) +// at WebElement.schedule_ (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\webdriver.js:2010:25) +// at WebElement.sendKeys (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\webdriver.js:2174:19) +// at actionFn (C:\easilog\webapp-tests\node_modules\protractor\built\element.js:89:44) +// at Array.map () +// at actionResults.getWebElements.then (C:\easilog\webapp-tests\node_modules\protractor\built\element.js:461:65) +// at ManagedPromise.invokeCallback_ (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\promise.js:1376:14) +// at TaskQueue.execute_ (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\promise.js:3084:14) +// at TaskQueue.executeNext_ (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\promise.js:3067:27) +// at asyncRun (C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\promise.js:2927:27) +// at C:\easilog\webapp-tests\node_modules\selenium-webdriver\lib\promise.js:668:7 +// at process._tickCallback (internal/process/next_tick.js:68:7)Error +// at ElementArrayFinder.applyAction_ (C:\easilog\webapp-tests\node_modules\protractor\built\element.js:459:27) +// at ElementArrayFinder.(anonymous function).args [as sendKeys] (C:\easilog\webapp-tests\node_modules\protractor\built\element.js:91:29) +// at ElementFinder.(anonymous function).args [as sendKeys] (C:\easilog\webapp-tests\node_modules\protractor\built\element.js:831:22) +// at LoginPageObject. (C:\easilog\webapp-tests\src\pages\base.page.ts:101:23) +// at step (C:\easilog\webapp-tests\src\pages\base.page.js:42:23) +// at Object.next (C:\easilog\webapp-tests\src\pages\base.page.js:23:53) +// at fulfilled (C:\easilog\webapp-tests\src\pages\base.page.js:14:58) +// at process._tickCallback (internal/process/next_tick.js:68:7) +// - And I fill the "started date" field with "1 October 2021" # src\step-definitions\interactions\fill-the-field-with.step.ts:11 +// - And I fill the "country" field with "United Kingdom" # src\step-definitions\interactions\fill-the-field-with.step.ts:11 diff --git a/lib/src/reporters/reporter.dart b/lib/src/reporters/reporter.dart new file mode 100644 index 0000000..f4a08d4 --- /dev/null +++ b/lib/src/reporters/reporter.dart @@ -0,0 +1,16 @@ +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/messages.dart'; + +abstract class Reporter { + Future onTestRunStarted() async {} + Future onTestRunfinished() async {} + Future onFeatureStarted(StartedMessage message) async {} + Future onFeatureFinished(FinishedMessage message) async {} + Future onScenarioStarted(StartedMessage message) async {} + Future onScenarioFinished(FinishedMessage message) async {} + Future onStepStarted(StartedMessage message) async {} + Future onStepFinished(StepFinishedMessage message) async {} + Future onException(Exception exception, StackTrace stackTrace) async {} + Future message(String message, MessageLevel level) async {} + Future dispose() async {} +} diff --git a/lib/src/reporters/stdout_reporter.dart b/lib/src/reporters/stdout_reporter.dart new file mode 100644 index 0000000..d0950e6 --- /dev/null +++ b/lib/src/reporters/stdout_reporter.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; + +class StdoutReporter extends Reporter { + static const String NEUTRAL_COLOR = "\u001b[33;34m"; // blue + static const String DEBUG_COLOR = "\u001b[1;30m"; // gray + static const String PASS_COLOR = "\u001b[33;32m"; // green + 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"; + + Future message(String message, MessageLevel level) async { + print(message, getColour(level)); + } + + String getColour(MessageLevel level) { + switch (level) { + case MessageLevel.verbose: + case MessageLevel.debug: + return DEBUG_COLOR; + case MessageLevel.error: + return FAIL_COLOR; + case MessageLevel.warning: + return WARN_COLOR; + case MessageLevel.info: + default: + return NEUTRAL_COLOR; + } + } + + void print(String message, [String colour]) { + stdout.writeln( + "${colour == null ? RESET_COLOR : colour}$message$RESET_COLOR"); + } +} diff --git a/lib/src/test_runner.dart b/lib/src/test_runner.dart new file mode 100644 index 0000000..cb047fe --- /dev/null +++ b/lib/src/test_runner.dart @@ -0,0 +1,114 @@ +import 'dart:io'; +import 'package:flutter_gherkin/src/configuration.dart'; +import 'package:flutter_gherkin/src/feature_file_runner.dart'; +import 'package:flutter_gherkin/src/gherkin/expressions/gherkin_expression.dart'; +import 'package:flutter_gherkin/src/gherkin/expressions/tag_expression.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/plural_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/word_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/string_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/int_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/float_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parser.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/exectuable_step.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_definition.dart'; +import 'package:flutter_gherkin/src/hooks/aggregated_hook.dart'; +import 'package:flutter_gherkin/src/hooks/hook.dart'; +import 'package:flutter_gherkin/src/reporters/aggregated_reporter.dart'; +import 'package:flutter_gherkin/src/reporters/message_level.dart'; +import 'package:flutter_gherkin/src/reporters/reporter.dart'; + +class GherkinRunner { + final _reporter = new AggregatedReporter(); + final _hook = new AggregatedHook(); + final _parser = new GherkinParser(); + final _tagExpressionEvaluator = new TagExpressionEvaluator(); + final List _executableSteps = new List(); + final List _customParameters = new List(); + + Future execute(TestConfiguration config) async { + _registerReporters(config.reporters); + _registerHooks(config.hooks); + _registerCustomParameters(config.customStepParameterDefinitions); + _registerStepDefinitions(config.stepDefinitions); + + List featureFiles = List(); + for (var glob in config.features) { + for (var entity in glob.listSync()) { + await _reporter.message( + "Found feature file '${entity.path}'", MessageLevel.verbose); + final contents = File(entity.path).readAsStringSync(); + final featureFile = + await _parser.parseFeatureFile(contents, entity.path, _reporter); + featureFiles.add(featureFile); + } + } + + await _reporter.message( + "Found ${featureFiles.length} feature file(s) to run", + MessageLevel.info); + + if (config.order == ExecutionOrder.random) { + await _reporter.message( + "Executing features in random order", MessageLevel.info); + featureFiles = featureFiles.toList()..shuffle(); + } + + await _hook.onBeforeRun(config); + + bool allFeaturesPassed = true; + try { + await _reporter.onTestRunStarted(); + for (var featureFile in featureFiles) { + final runner = new FeatureFileRunner(config, _tagExpressionEvaluator, + _executableSteps, _reporter, _hook); + await runner.run(featureFile); + } + } finally { + await _reporter.onTestRunfinished(); + } + + await _hook.onAfterRun(config); + await _reporter.dispose(); + + exitCode = allFeaturesPassed ? 0 : 1; + + if (config.exitAfterTestRun) exit(allFeaturesPassed ? 0 : 1); + } + + void _registerStepDefinitions( + Iterable stepDefinitions) { + stepDefinitions.forEach((s) { + _executableSteps.add( + ExectuableStep(GherkinExpression(s.pattern, _customParameters), s)); + }); + } + + void _registerCustomParameters(Iterable customParameters) { + _customParameters.add(new FloatParameterLower()); + _customParameters.add(new FloatParameterCamel()); + _customParameters.add(new NumParameterLower()); + _customParameters.add(new NumParameterCamel()); + _customParameters.add(new IntParameterLower()); + _customParameters.add(new IntParameterCamel()); + _customParameters.add(new StringParameterLower()); + _customParameters.add(new StringParameterCamel()); + _customParameters.add(new WordParameterLower()); + _customParameters.add(new WordParameterCamel()); + _customParameters.add(new PluralParameter()); + if (customParameters != null) _customParameters.addAll(customParameters); + } + + void _registerReporters(Iterable reporters) { + if (reporters != null) { + reporters.forEach((r) => _reporter.addReporter(r)); + } + } + + void _registerHooks(Iterable hooks) { + if (hooks != null) { + _hook.addHooks(hooks); + } + } +} diff --git a/lib/src/utils/perf.dart b/lib/src/utils/perf.dart new file mode 100644 index 0000000..92efdd1 --- /dev/null +++ b/lib/src/utils/perf.dart @@ -0,0 +1,15 @@ +import 'dart:async'; + +class Perf { + static Future measure( + Future action(), void logFn(int elapsedMilliseconds)) async { + final timer = new Stopwatch(); + timer.start(); + try { + return await action(); + } finally { + timer.stop(); + logFn(timer.elapsedMilliseconds); + } + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..b844bd8 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,396 @@ +# Generated by pub +# See https://www.dartlang.org/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.32.4" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.5" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.6" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + glob: + dependency: "direct main" + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.3+3" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+17" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.7" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.3+1" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + path: + dependency: "direct main" + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + plugin: + dependency: transitive + description: + name: plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.6" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.3+3" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2+4" + 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: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.7" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.8" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + test: + dependency: transitive + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + utf: + dependency: transitive + description: + name: utf + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0+5" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.9" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" +sdks: + dart: ">=2.0.0-dev.68.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..82f7310 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: flutter_gherkin +description: A Gherkin / Cucumber parser and test runner for Dart and Flutter +version: 0.0.1 +author: Jon Samwell +homepage: + +environment: + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + path: ^1.6.2 + glob: ^1.1.7 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + +flutter: diff --git a/test/feature_file_runner_test.dart b/test/feature_file_runner_test.dart new file mode 100644 index 0000000..446448c --- /dev/null +++ b/test/feature_file_runner_test.dart @@ -0,0 +1,379 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/configuration.dart'; +import 'package:flutter_gherkin/src/feature_file_runner.dart'; +import 'package:flutter_gherkin/src/gherkin/exceptions/step_not_defined_error.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/exectuable_step.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_run_result.dart'; +import 'package:test/test.dart'; +import 'mocks/gherkin_expression_mock.dart'; +import 'mocks/hook_mock.dart'; +import 'mocks/reporter_mock.dart'; +import 'mocks/step_definition_mock.dart'; +import 'mocks/tag_expression_evaluator_mock.dart'; +import 'mocks/world_mock.dart'; + +void main() { + final emptyDebuggable = RunnableDebugInformation("File Path", 0, "Line text"); + group("run", () { + test("run simple feature file scenario", () async { + final stepDefiniton = MockStepDefinition(); + final executableStep = + new ExectuableStep(MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + ReporterMock(), + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(stepDefiniton.runCount, 1); + }); + + test("world context is created and disposed", () async { + final worldMock = WorldMock(); + bool worldCreationFnInoked = false; + final stepDefiniton = MockStepDefinition(); + final executableStep = + new ExectuableStep(MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration() + ..createWorld = (_) async { + worldCreationFnInoked = true; + return worldMock; + }, + MockTagExpressionEvaluator(), + [executableStep], + ReporterMock(), + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(worldCreationFnInoked, true); + expect(worldMock.disposeFnInvoked, true); + }); + + test("steps are skipped if previous step failed", () async { + final stepTextOne = "Given I do a"; + final stepTextTwo = "Given I do b"; + final stepDefiniton = MockStepDefinition((_) => throw new Exception()); + final stepDefinitonTwo = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((s) => s == stepTextOne), stepDefiniton); + final executableStepTwo = new ExectuableStep( + MockGherkinExpression((s) => s == stepTextTwo), stepDefinitonTwo); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep, executableStepTwo], + ReporterMock(), + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, stepTextOne)); + final stepTwo = new StepRunnable( + "Step 2", RunnableDebugInformation("", 0, stepTextTwo)); + final scenario = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step) + ..steps.add(stepTwo); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(stepDefiniton.runCount, 1); + expect(stepDefinitonTwo.runCount, 0); + }); + + group("step matching", () { + test("exception throw when matching step definition not found", () async { + final stepDefiniton = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => false), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + ReporterMock(), + HookMock()); + + final step = new StepRunnable("Step 1", + RunnableDebugInformation("File Path", 2, "Given I do 'a'")); + final scenario = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + expect( + () async => await runner.run(featureFile), + throwsA(allOf( + (e) => e is GherkinStepNotDefinedException, + (e) => + e.message == + """ Step definition not found for text: + + 'Given I do 'a'' + + File path: File Path#2 + Line: Given I do 'a' + + --------------------------------------------- + + You must implement the step: + + /// The 'Given' class can be replaced with 'Then', 'When' 'And' or 'But' + /// All classes can take up to 5 input parameters anymore and you should probably us a table + /// For example: `When4` + /// You can also specify the type of world context you want + /// `When4WithWorld` + class Given_Given_I_do__a_ extends Given1 { + @override + RegExp get pattern => RegExp(r"Given I do 'a'"); + + @override + Future executeStep(String input1) async { + // If the step is "Given I do a 'windy pop'" + // in this example input1 would equal 'windy pop' + + // your code... + } + } + """))); + }); + }); + + group("hooks", () { + test("hook is called when starting and finishing scenarios", () async { + final hookMock = HookMock(); + final stepDefiniton = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + ReporterMock(), + hookMock); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final scenario2 = new ScenarioRunnable("Scenario: 2", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1) + ..scenarios.add(scenario2); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(hookMock.onBeforeScenarioInvocationCount, 2); + expect(hookMock.onAfterScenarioInvocationCount, 2); + }); + }); + + group("reporter", () { + test("reporter is called when starting and finishing runnable blocks", + () async { + final reporterMock = ReporterMock(); + final stepDefiniton = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + reporterMock, + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final scenario2 = new ScenarioRunnable("Scenario: 2", emptyDebuggable) + ..steps.add(step) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1) + ..scenarios.add(scenario2); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(reporterMock.onFeatureStartedInvocationCount, 1); + expect(reporterMock.onFeatureFinishedInvocationCount, 1); + expect(reporterMock.onFeatureStartedInvocationCount, 1); + expect(reporterMock.onFeatureFinishedInvocationCount, 1); + expect(reporterMock.onScenarioStartedInvocationCount, 2); + expect(reporterMock.onScenarioFinishedInvocationCount, 2); + expect(reporterMock.onStepStartedInvocationCount, 3); + expect(reporterMock.onStepFinishedInvocationCount, 3); + }); + + test("step reported with correct finishing value when passing", () async { + StepFinishedMessage finishedMessage; + final reporterMock = ReporterMock(); + reporterMock.onStepFinishedFn = (message) => finishedMessage = message; + final stepDefiniton = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + reporterMock, + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(finishedMessage, (m) => m.name == "Step 1"); + expect(finishedMessage, + (m) => m.result.result == StepExecutionResult.pass); + }); + + test("step reported with correct finishing value when failing", () async { + StepFinishedMessage finishedMessage; + final testFailureException = TestFailure("FAILED"); + final reporterMock = ReporterMock(); + reporterMock.onStepFinishedFn = (message) => finishedMessage = message; + final stepDefiniton = + MockStepDefinition((_) => throw testFailureException); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration(), + MockTagExpressionEvaluator(), + [executableStep], + reporterMock, + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(finishedMessage, (m) => m.name == "Step 1"); + expect(finishedMessage, + (m) => m.result.result == StepExecutionResult.fail); + }); + + test( + "step reported with correct finishing value when unhandled exception raised", + () async { + StepFinishedMessage finishedMessage; + final reporterMock = ReporterMock(); + reporterMock.onStepFinishedFn = (message) => finishedMessage = message; + final stepDefiniton = MockStepDefinition( + (_) async => await Future.delayed(Duration(seconds: 2))); + final executableStep = new ExectuableStep( + MockGherkinExpression((_) => true), stepDefiniton); + final runner = new FeatureFileRunner( + TestConfiguration()..defaultTimeout = Duration(milliseconds: 1), + MockTagExpressionEvaluator(), + [executableStep], + reporterMock, + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, "Given I do a")); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(finishedMessage, (m) => m.name == "Step 1"); + expect(finishedMessage, + (m) => m.result.result == StepExecutionResult.timeout); + }); + + test("skipped step reported correctly", () async { + final finishedMessages = List(); + final reporterMock = ReporterMock(); + reporterMock.onStepFinishedFn = + (message) => finishedMessages.add(message); + + final stepTextOne = "Given I do a"; + final stepTextTwo = "Given I do b"; + final stepTextThree = "Given I do c"; + final stepDefiniton = MockStepDefinition((_) => throw new Exception()); + final stepDefinitonTwo = MockStepDefinition(); + final stepDefinitonThree = MockStepDefinition(); + final executableStep = new ExectuableStep( + MockGherkinExpression((s) => s == stepTextOne), stepDefiniton); + final executableStepTwo = new ExectuableStep( + MockGherkinExpression((s) => s == stepTextTwo), stepDefinitonTwo); + final executableStepThree = new ExectuableStep( + MockGherkinExpression((s) => s == stepTextThree), + stepDefinitonThree); + final runner = new FeatureFileRunner( + TestConfiguration()..defaultTimeout = Duration(milliseconds: 1), + MockTagExpressionEvaluator(), + [executableStep, executableStepTwo, executableStepThree], + reporterMock, + HookMock()); + + final step = new StepRunnable( + "Step 1", RunnableDebugInformation("", 0, stepTextOne)); + final stepTwo = new StepRunnable( + "Step 2", RunnableDebugInformation("", 0, stepTextTwo)); + final stepThree = new StepRunnable( + "Step 3", RunnableDebugInformation("", 0, stepTextThree)); + final scenario1 = new ScenarioRunnable("Scenario: 1", emptyDebuggable) + ..steps.add(step) + ..steps.add(stepTwo) + ..steps.add(stepThree); + final feature = new FeatureRunnable("1", emptyDebuggable) + ..scenarios.add(scenario1); + final featureFile = new FeatureFile(emptyDebuggable) + ..features.add(feature); + await runner.run(featureFile); + expect(stepDefiniton.hasRun, true); + expect(finishedMessages.length, 3); + expect(finishedMessages.elementAt(0).result.result, + StepExecutionResult.error); + expect(finishedMessages.elementAt(1).result.result, + StepExecutionResult.skipped); + expect(finishedMessages.elementAt(2).result.result, + StepExecutionResult.skipped); + }); + }); + }); +} diff --git a/test/gherkin/expressions/gherkin_expression.dart b/test/gherkin/expressions/gherkin_expression.dart new file mode 100644 index 0000000..b32dc90 --- /dev/null +++ b/test/gherkin/expressions/gherkin_expression.dart @@ -0,0 +1,125 @@ +import 'package:flutter_gherkin/src/gherkin/expressions/gherkin_expression.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/int_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/string_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/word_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/float_parameter.dart'; +import 'package:flutter_gherkin/src/gherkin/parameters/plural_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group("GherkinExpression", () { + test('parse simple regex expression correctly', () async { + final parser = new GherkinExpression(RegExp('I (open|close) the drawer'), + [WordParameterLower(), WordParameterCamel()]); + + expect(parser.isMatch("I open the drawer"), equals(true)); + expect(parser.isMatch("I close the drawer"), equals(true)); + expect(parser.isMatch("I sausage the drawer"), equals(false)); + expect(parser.getParameters("I close the drawer"), equals(["close"])); + }); + + test('parse complex regex with custom parameters expression correctly', + () async { + final parser = new GherkinExpression( + RegExp( + 'I (open|close) the drawer {int} time(s) and find {word} which is (good|bad)'), + [WordParameterLower(), IntParameterLower(), PluralParameter()]); + + expect( + parser.isMatch( + "I open the drawer 2 times and find 'socks' which is good"), + equals(true)); + expect( + parser.isMatch( + "I close the drawer 1 time and find 'pants' which is good"), + equals(true)); + expect( + parser.isMatch( + "I sausage the drawer 919293 times and find 'parsley' which is good"), + equals(false)); + expect( + parser.getParameters( + "I open the drawer 6 times and find 'socks' which is bad"), + equals(["open", 6, "socks", "bad"])); + }); + + test('parse simple {word} expression correctly', () async { + final parser = new GherkinExpression(RegExp('I am {word} as {Word}'), + [WordParameterLower(), WordParameterCamel()]); + + expect(parser.isMatch("I am 'happy'"), equals(false)); + expect(parser.isMatch("I am 'happy' as 'Larry'"), equals(true)); + expect(parser.getParameters("I am 'happy' as 'Larry'"), + equals(["happy", "Larry"])); + }); + + test('parse simple {string} expression correctly', () async { + final parser = new GherkinExpression( + RegExp('I am {string}'), [StringParameterLower()]); + + expect(parser.isMatch("I am 'happy as Larry'"), equals(true)); + expect(parser.getParameters("I am 'happy as Larry'"), + equals(["happy as Larry"])); + }); + + test('parse simple {int} expression correctly', () async { + final parser = new GherkinExpression( + RegExp('I am {int} years and {Int} days old'), + [IntParameterLower(), IntParameterCamel()]); + + expect(parser.isMatch("I am 150 years and 19 days old"), equals(true)); + expect(parser.getParameters("I am 150 years and 19 days old"), + equals([150, 19])); + }); + + test('parse simple {float} expression correctly', () async { + final parser = new GherkinExpression( + RegExp('I am {float} years and {Float} days old'), + [FloatParameterLower(), FloatParameterCamel()]); + + expect( + parser.isMatch("I am 150.232 years and 19.4 days old"), equals(true)); + expect(parser.getParameters("I am 150.53 years and 19.00942 days old"), + equals([150.53, 19.00942])); + }); + + test('parse simple plural (s) expression correctly', () async { + final parser = new GherkinExpression( + RegExp('I have {int} cucumber(s) in my belly'), + [IntParameterLower(), PluralParameter()]); + + expect(parser.isMatch("I have 1 cucumber in my belly"), equals(true)); + expect(parser.isMatch("I have 42 cucumbers in my belly"), equals(true)); + expect( + parser.getParameters("I have 1 cucumber in my belly"), equals([1])); + expect(parser.getParameters("I have 42 cucumbers in my belly"), + equals([42])); + }); + + test('parse complex expression correctly', () async { + final parser = new GherkinExpression( + RegExp( + '{word} {int} {string} {int} (jon|laurie) {float} {word} {float} cucumber(s)'), + [ + WordParameterLower(), + StringParameterLower(), + IntParameterLower(), + FloatParameterLower(), + PluralParameter() + ]); + + expect( + parser.isMatch( + "'word' 22 'a string' 09 jon 3.14 'hello' 3.333 cucumber"), + equals(true)); + expect( + parser.isMatch( + "'word' 22 'a string' 09 laurie 3.14 'hello' 3.333 cucumbers"), + equals(true)); + expect( + parser.getParameters( + "'word' 22 'a string' 09 laurie 3.14 'hello' 3.333 cucumbers"), + equals(["word", 22, "a string", 9, "laurie", 3.14, "hello", 3.333])); + }); + }); +} diff --git a/test/gherkin/expressions/tag_expression.dart b/test/gherkin/expressions/tag_expression.dart new file mode 100644 index 0000000..b6a580b --- /dev/null +++ b/test/gherkin/expressions/tag_expression.dart @@ -0,0 +1,50 @@ +import "package:flutter_gherkin/src/gherkin/expressions/tag_expression.dart"; +import "package:test/test.dart"; + +void main() { + group("TagExpression", () { + test("evaluate simple single tag expression correctly", () async { + final evaluator = new TagExpressionEvaluator(); + final tags = ["a", "b", "c"]; + + expect(evaluator.evaluate("@a", tags), true); + expect(evaluator.evaluate("@b", tags), true); + expect(evaluator.evaluate("@d", tags), false); + }); + + test("evaluate complex and tag expression correctly", () async { + final evaluator = new TagExpressionEvaluator(); + final tags = ["a", "b", "c"]; + + expect(evaluator.evaluate("@a and @d", tags), false); + expect(evaluator.evaluate("(@a and not @d)", tags), true); + expect(evaluator.evaluate("(@a and not @c)", tags), false); + }); + + test("evaluate complex or tag expression correctly", () async { + final evaluator = new TagExpressionEvaluator(); + final tags = ["a", "b", "c"]; + + expect(evaluator.evaluate("(@a or @b)", tags), true); + expect(evaluator.evaluate("not @a or not @d", tags), true); + expect(evaluator.evaluate("not @d or not @e", tags), true); + }); + + test("evaluate complex bracket tag expression correctly", () async { + final evaluator = new TagExpressionEvaluator(); + final tags = ["a", "b", "c"]; + + expect(evaluator.evaluate("@a or (@b and @c)", tags), true); + expect(evaluator.evaluate("@a and (@d or @e)", tags), false); + expect(evaluator.evaluate("@a and ((@b or not @e) or (@b and @c))", tags), + true); + expect( + evaluator.evaluate( + "@a and ((@b and not @e) and (@b and @c))", ["a", "b", "c", "e"]), + false); + expect( + evaluator.evaluate("@a and ((@b or not @e) and (@b and @c))", tags), + true); + }); + }); +} diff --git a/test/gherkin/parameters/float_parameter.dart b/test/gherkin/parameters/float_parameter.dart new file mode 100644 index 0000000..8a03c3d --- /dev/null +++ b/test/gherkin/parameters/float_parameter.dart @@ -0,0 +1,26 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/float_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group("FloatParameter", () { + test("{float} parsed correctly", () { + final parameter = FloatParameterLower(); + expect(parameter.transformer("12.243"), equals(12.243)); + }); + + test("{Float} parsed correctly", () { + final parameter = FloatParameterCamel(); + expect(parameter.transformer("12.243"), equals(12.243)); + }); + + test("{num} parsed correctly", () { + final parameter = NumParameterLower(); + expect(parameter.transformer("12.243"), equals(12.243)); + }); + + test("{Num} parsed correctly", () { + final parameter = NumParameterCamel(); + expect(parameter.transformer("12.243"), equals(12.243)); + }); + }); +} diff --git a/test/gherkin/parameters/int_parameter.dart b/test/gherkin/parameters/int_parameter.dart new file mode 100644 index 0000000..db27764 --- /dev/null +++ b/test/gherkin/parameters/int_parameter.dart @@ -0,0 +1,16 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/int_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group("IntParameter", () { + test("{int} parsed correctly", () { + final parameter = IntParameterLower(); + expect(parameter.transformer("12"), equals(12)); + }); + + test("{Int} parsed correctly", () { + final parameter = IntParameterCamel(); + expect(parameter.transformer("12"), equals(12)); + }); + }); +} diff --git a/test/gherkin/parameters/string_parameter.dart b/test/gherkin/parameters/string_parameter.dart new file mode 100644 index 0000000..51d6802 --- /dev/null +++ b/test/gherkin/parameters/string_parameter.dart @@ -0,0 +1,16 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/string_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group("StringParameter", () { + test("{string} parsed correctly", () { + final parameter = StringParameterLower(); + expect(parameter.transformer("Jon Samwell"), equals("Jon Samwell")); + }); + + test("{String} parsed correctly", () { + final parameter = StringParameterCamel(); + expect(parameter.transformer("Jon Samwell"), equals("Jon Samwell")); + }); + }); +} diff --git a/test/gherkin/parameters/word_parameter.dart b/test/gherkin/parameters/word_parameter.dart new file mode 100644 index 0000000..5f68432 --- /dev/null +++ b/test/gherkin/parameters/word_parameter.dart @@ -0,0 +1,16 @@ +import 'package:flutter_gherkin/src/gherkin/parameters/string_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group("WordParameter", () { + test("{word} parsed correctly", () { + final parameter = StringParameterLower(); + expect(parameter.transformer("Jon"), equals("Jon")); + }); + + test("{Word} parsed correctly", () { + final parameter = StringParameterCamel(); + expect(parameter.transformer("Jon"), equals("Jon")); + }); + }); +} diff --git a/test/gherkin/parser_test.dart b/test/gherkin/parser_test.dart new file mode 100644 index 0000000..a7c947a --- /dev/null +++ b/test/gherkin/parser_test.dart @@ -0,0 +1,148 @@ +import 'package:flutter_gherkin/src/gherkin/parser.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:test/test.dart'; +import '../mocks/reporter_mock.dart'; + +void main() { + group("parse", () { + test('parses simple, single scenario correctly', () async { + final parser = new GherkinParser(); + final featureContents = """ + # language: en + Feature: The name of the feature + A multiine line description + Line two + Line three + + Background: Some background + Given I setup 1 + And I setup 2 + + @smoke + Scenario: When the user does some steps they see 'd' + Given I do step a + And I do step b + And I add the comment + ''' + A mutliline + comment + ''' + When I do step c + Then I expect to see d + """; + FeatureFile featureFile = + await parser.parseFeatureFile(featureContents, "", ReporterMock()); + expect(featureFile, isNot(null)); + expect(featureFile.langauge, equals("en")); + expect(featureFile.features.length, 1); + + final feature = featureFile.features.elementAt(0); + expect(feature.name, "The name of the feature"); + expect(feature.description, + "A multiine line description\nLine two\nLine three"); + expect(feature.tags, ["smoke"]); + expect(feature.scenarios.length, 1); + + final background = featureFile.features.elementAt(0).background; + expect(background.name, "Some background"); + expect(background.steps.length, 2); + expect(background.steps.elementAt(0).name, "Given I setup 1"); + expect(background.steps.elementAt(1).name, "And I setup 2"); + + final scenario = featureFile.features.elementAt(0).scenarios.elementAt(0); + expect(scenario.name, "When the user does some steps they see 'd'"); + expect(scenario.steps.length, 5); + + final steps = scenario.steps; + expect(steps.elementAt(0).name, "Given I do step a"); + expect(steps.elementAt(1).name, "And I do step b"); + expect(steps.elementAt(2).name, "And I add the comment"); + expect(steps.elementAt(3).name, "When I do step c"); + expect(steps.elementAt(4).name, "Then I expect to see d"); + + final commentStep = steps.elementAt(2); + expect(commentStep.multilineStrings.length, 1); + expect(commentStep.multilineStrings.elementAt(0), "A mutliline\ncomment"); + }); + + test('parses complex multi-scenario correctly', () async { + final parser = new GherkinParser(); + final featureContents = """ + # language: en + Feature: The name of the feature + A multiine line description + Line two + Line three + + Background: Some background + Given I setup 1 + And I setup 2 + + @smoke + Scenario: When the user does some steps they see 'd' + Given I do step a + And I do step b + And I add the comment + ''' + A mutliline + comment + ''' + And I add the people + | Firstname | Surname | Age | Gender | + | Woody | Johnson | 28 | Male | + | Edith | Summers | 23 | Female | + | Megan | Hill | 83 | Female | + When I do step c + # ignore the below step + # When I do step c.1 + Then I expect to see d + """; + FeatureFile featureFile = + await parser.parseFeatureFile(featureContents, "", ReporterMock()); + expect(featureFile, isNot(null)); + expect(featureFile.langauge, equals("en")); + expect(featureFile.features.length, 1); + + final feature = featureFile.features.elementAt(0); + expect(feature.name, "The name of the feature"); + expect(feature.description, + "A multiine line description\nLine two\nLine three"); + expect(feature.tags, ["smoke"]); + expect(feature.scenarios.length, 1); + + final background = featureFile.features.elementAt(0).background; + expect(background.name, "Some background"); + expect(background.steps.length, 2); + expect(background.steps.elementAt(0).name, "Given I setup 1"); + expect(background.steps.elementAt(1).name, "And I setup 2"); + + final scenario = featureFile.features.elementAt(0).scenarios.elementAt(0); + expect(scenario.name, "When the user does some steps they see 'd'"); + expect(scenario.tags, ["smoke"]); + expect(scenario.steps.length, 6); + + final steps = scenario.steps; + expect(steps.elementAt(0).name, "Given I do step a"); + expect(steps.elementAt(1).name, "And I do step b"); + expect(steps.elementAt(2).name, "And I add the comment"); + expect(steps.elementAt(3).name, "And I add the people"); + expect(steps.elementAt(4).name, "When I do step c"); + expect(steps.elementAt(5).name, "Then I expect to see d"); + + expect(steps.elementAt(3).table, isNotNull); + expect(steps.elementAt(3).table.header, isNotNull); + expect(steps.elementAt(3).table.header.columns, + ["Firstname", "Surname", "Age", "Gender"]); + expect(steps.elementAt(3).table.rows.elementAt(0).columns.toList(), + ["Woody", "Johnson", "28", "Male"]); + expect(steps.elementAt(3).table.rows.elementAt(1).columns.toList(), + ["Edith", "Summers", "23", "Female"]); + expect(steps.elementAt(3).table.rows.elementAt(2).columns.toList(), + ["Megan", "Hill", "83", "Female"]); + + final commentStep = steps.elementAt(2); + expect(commentStep.multilineStrings.length, 1); + expect(commentStep.multilineStrings.elementAt(0), "A mutliline\ncomment"); + }); + }); +} diff --git a/test/gherkin/runnables/feature_file_test.dart b/test/gherkin/runnables/feature_file_test.dart new file mode 100644 index 0000000..575c92c --- /dev/null +++ b/test/gherkin/runnables/feature_file_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature_file.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/language.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add LangaugeRunnable', () { + final runnable = new FeatureFile(debugInfo); + runnable.addChild(LanguageRunnable(debugInfo)..language = "en"); + expect(runnable.langauge, "en"); + }); + test('can add TagsRunnable', () { + final runnable = new FeatureFile(debugInfo); + runnable.addChild(FeatureRunnable("1", debugInfo)); + runnable.addChild(FeatureRunnable("2", debugInfo)); + runnable.addChild(FeatureRunnable("3", debugInfo)); + expect(runnable.features.length, 3); + }); + }); +} diff --git a/test/gherkin/runnables/feature_test.dart b/test/gherkin/runnables/feature_test.dart new file mode 100644 index 0000000..33bde02 --- /dev/null +++ b/test/gherkin/runnables/feature_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/background.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add TextLineRunnable', () { + final runnable = new FeatureRunnable("", debugInfo); + runnable.addChild(TextLineRunnable(debugInfo)..text = "text"); + runnable.addChild(TextLineRunnable(debugInfo)..text = "text line two"); + expect(runnable.description, "text\ntext line two"); + }); + test('can add TagsRunnable', () { + final runnable = new FeatureRunnable("", debugInfo); + runnable.addChild(TagsRunnable(debugInfo)..tags = ["one", "two"]); + runnable.addChild(TagsRunnable(debugInfo)..tags = ["three"]); + expect(runnable.tags, ["one", "two", "three"]); + }); + test('can add EmptyLineRunnable', () { + final runnable = new FeatureRunnable("", debugInfo); + runnable.addChild(EmptyLineRunnable(debugInfo)); + }); + test('can add ScenarioRunnable', () { + final runnable = new FeatureRunnable("", debugInfo); + runnable.addChild(ScenarioRunnable("1", debugInfo)); + runnable.addChild(ScenarioRunnable("2", debugInfo)); + runnable.addChild(ScenarioRunnable("3", debugInfo)); + expect(runnable.scenarios.length, 3); + expect(runnable.scenarios.elementAt(0).name, "1"); + expect(runnable.scenarios.elementAt(1).name, "2"); + expect(runnable.scenarios.elementAt(2).name, "3"); + }); + test('can add BackgroundRunnable', () { + final runnable = new FeatureRunnable("", debugInfo); + runnable.addChild(BackgroundRunnable("1", debugInfo)); + expect(runnable.background, isNotNull); + expect(runnable.background.name, "1"); + }); + }); +} diff --git a/test/gherkin/runnables/multi_line_string_test.dart b/test/gherkin/runnables/multi_line_string_test.dart new file mode 100644 index 0000000..21eb7bd --- /dev/null +++ b/test/gherkin/runnables/multi_line_string_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/multi_line_string.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add EmptyLineRunnable', () { + final runnable = new MultilineStringRunnable(debugInfo); + runnable.addChild(EmptyLineRunnable(debugInfo)); + }); + test('can add TextLineRunnable', () { + final runnable = new MultilineStringRunnable(debugInfo); + runnable.addChild(TextLineRunnable(debugInfo)..text = "1"); + runnable.addChild(TextLineRunnable(debugInfo)..text = "2"); + runnable.addChild(TextLineRunnable(debugInfo)..text = "3"); + expect(runnable.lines.length, 3); + expect(runnable.lines, ["1", "2", "3"]); + }); + }); +} diff --git a/test/gherkin/runnables/scenario_test.dart b/test/gherkin/runnables/scenario_test.dart new file mode 100644 index 0000000..a3ba89f --- /dev/null +++ b/test/gherkin/runnables/scenario_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/empty_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add EmptyLineRunnable', () { + final runnable = new ScenarioRunnable("", debugInfo); + runnable.addChild(EmptyLineRunnable(debugInfo)); + }); + test('can add StepRunnable', () { + final runnable = new ScenarioRunnable("", debugInfo); + runnable.addChild(StepRunnable("1", debugInfo)); + runnable.addChild(StepRunnable("2", debugInfo)); + runnable.addChild(StepRunnable("3", debugInfo)); + expect(runnable.steps.length, 3); + expect(runnable.steps.elementAt(0).name, "1"); + expect(runnable.steps.elementAt(1).name, "2"); + expect(runnable.steps.elementAt(2).name, "3"); + }); + test('can add TagsRunnable', () { + final runnable = new ScenarioRunnable("", debugInfo); + runnable.addChild(TagsRunnable(debugInfo)..tags = ["one", "two"]); + runnable.addChild(TagsRunnable(debugInfo)..tags = ["three"]); + expect(runnable.tags, ["one", "two", "three"]); + }); + }); +} diff --git a/test/gherkin/runnables/step_test.dart b/test/gherkin/runnables/step_test.dart new file mode 100644 index 0000000..f53198f --- /dev/null +++ b/test/gherkin/runnables/step_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_gherkin/src/gherkin/exceptions/syntax_error.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/multi_line_string.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/table.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add MultilineStringRunnable', () { + final runnable = new StepRunnable("", debugInfo); + runnable.addChild( + MultilineStringRunnable(debugInfo)..lines = ["1", "2", "3"].toList()); + runnable.addChild( + MultilineStringRunnable(debugInfo)..lines = ["3", "4", "5"].toList()); + expect(runnable.multilineStrings.length, 2); + expect(runnable.multilineStrings.elementAt(0), "1\n2\n3"); + expect(runnable.multilineStrings.elementAt(1), "3\n4\n5"); + }); + + test('can add TableRunnable', () { + final runnable = new StepRunnable("", debugInfo); + runnable.addChild(TableRunnable(debugInfo) + ..addChild(TableRunnable(debugInfo)..rows.add("|Col A|Col B|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|1|2|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|3|4|"))); + + expect(runnable.table, isNotNull); + expect(runnable.table.header, isNotNull); + expect(runnable.table.header.columns.length, 2); + expect(runnable.table.rows.length, 2); + }); + + test('can only add single TableRunnable', () { + final runnable = new StepRunnable("Step A", debugInfo); + runnable.addChild(TableRunnable(debugInfo) + ..addChild(TableRunnable(debugInfo)..rows.add("|Col A|Col B|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|1|2|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|3|4|"))); + + expect( + () => runnable.addChild(TableRunnable(debugInfo) + ..addChild(TableRunnable(debugInfo)..rows.add("|Col A|Col B|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|1|2|")) + ..addChild(TableRunnable(debugInfo)..rows.add("|3|4|"))), + throwsA((e) => + e is GherkinSyntaxException && + e.message == + "Only a single table can be added to the step 'Step A'")); + }); + }); +} diff --git a/test/gherkin/runnables/table_test.dart b/test/gherkin/runnables/table_test.dart new file mode 100644 index 0000000..f682f4b --- /dev/null +++ b/test/gherkin/runnables/table_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/comment_line.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/table.dart'; +import 'package:test/test.dart'; + +void main() { + final debugInfo = RunnableDebugInformation(null, 0, null); + group("addChild", () { + test('can add CommentLineRunnable', () { + final runnable = new TableRunnable(debugInfo); + runnable.addChild(CommentLineRunnable("", debugInfo)); + }); + test('can add TableRunnable', () { + final runnable = new TableRunnable(debugInfo); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("| Header 1 | Header 2 |")); + runnable.addChild(TableRunnable(debugInfo)..rows.add("| 1 | 2 |")); + runnable.addChild(TableRunnable(debugInfo)..rows.add("| 3 | 4 |")); + expect(runnable.rows.length, 3); + expect(runnable.rows, + ["| Header 1 | Header 2 |", "| 1 | 2 |", "| 3 | 4 |"]); + }); + }); + + group("to table", () { + test("single row table has no header row", () async { + final runnable = new TableRunnable(debugInfo); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("| one | two | three |")); + final table = runnable.toTable(); + expect(table.header, isNull); + expect(table.rows.length, 1); + expect(table.rows.first.columns, ["one", "two", "three"]); + }); + + test("two row table has header row", () async { + final runnable = new TableRunnable(debugInfo); + runnable.addChild(TableRunnable(debugInfo) + ..rows.add("| header one | header two | header three |")); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("| one | two | three |")); + final table = runnable.toTable(); + expect(table.header, isNotNull); + expect( + table.header.columns, ["header one", "header two", "header three"]); + expect(table.rows.length, 1); + expect(table.rows.elementAt(0).columns, ["one", "two", "three"]); + }); + + test("three row table has header row and correct rows", () async { + final runnable = new TableRunnable(debugInfo); + runnable.addChild(TableRunnable(debugInfo) + ..rows.add("| header one | header two | header three |")); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("| one | two | three |")); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("| four | five | six |")); + final table = runnable.toTable(); + expect(table.header, isNotNull); + expect( + table.header.columns, ["header one", "header two", "header three"]); + expect(table.rows.length, 2); + expect(table.rows.elementAt(0).columns, ["one", "two", "three"]); + expect(table.rows.elementAt(1).columns, ["four", "five", "six"]); + }); + + test("table removes columns leading and trailing spaces", () async { + final runnable = new TableRunnable(debugInfo); + runnable.addChild(TableRunnable(debugInfo) + ..rows.add("| header one | header two | header three |")); + runnable.addChild(TableRunnable(debugInfo) + ..rows.add("| one | two | three |")); + runnable.addChild( + TableRunnable(debugInfo)..rows.add("|four | five |six|")); + final table = runnable.toTable(); + expect(table.header, isNotNull); + expect( + table.header.columns, ["header one", "header two", "header three"]); + expect(table.rows.length, 2); + expect(table.rows.elementAt(0).columns, ["one", "two", "three"]); + expect(table.rows.elementAt(1).columns, ["four", "five", "six"]); + }); + }); +} diff --git a/test/gherkin/steps/step_definition_test.dart b/test/gherkin/steps/step_definition_test.dart new file mode 100644 index 0000000..1ce6381 --- /dev/null +++ b/test/gherkin/steps/step_definition_test.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/gherkin/exceptions/parameter_count_mismatch_error.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_configuration.dart'; +import 'package:flutter_gherkin/src/gherkin/steps/step_run_result.dart'; +import 'package:test/test.dart'; + +class StepDefinitionMock extends StepDefinitionGeneric { + int invocationCount = 0; + final Func0> code; + + StepDefinitionMock( + StepDefinitionConfiguration config, int expectParameterCount, + [this.code]) + : super(config, expectParameterCount); + + @override + Future onRun(Iterable parameters) async { + invocationCount += 1; + if (code != null) { + await code(); + } + } + + @override + RegExp get pattern => null; +} + +void main() { + group("onRun", () { + group("parameter gaurd", () { + test("throws exception when parameter counts mismatch", () async { + final step = StepDefinitionMock(StepDefinitionConfiguration(), 2); + expect( + () async => await step.run( + null, null, Duration(seconds: 1), Iterable.empty()), + throwsA((e) => + e is GherkinStepParameterMismatchException && + e.message == + "StepDefinitionMock parameter count mismatch. Expect 2 parameters but got 0. " + + "Ensure you are extending the correct step class which would be Given")); + expect(step.invocationCount, 0); + }); + + test( + "throws exception when parameter counts mismatch listing required step type", + () async { + final step = StepDefinitionMock(StepDefinitionConfiguration(), 2); + expect( + () async => await step.run(null, null, Duration(seconds: 1), [1]), + throwsA((e) => + e is GherkinStepParameterMismatchException && + e.message == + "StepDefinitionMock parameter count mismatch. Expect 2 parameters but got 1. " + + "Ensure you are extending the correct step class which would be Given1")); + expect(step.invocationCount, 0); + }); + + test("runs step when correct number of parameters provided", () async { + final step = StepDefinitionMock(StepDefinitionConfiguration(), 1); + await step.run(null, null, Duration(seconds: 1), [1]); + expect(step.invocationCount, 1); + }); + }); + + group("exception reported", () { + test("when exception is throw in test it is report as an error", + () async { + final step = StepDefinitionMock( + StepDefinitionConfiguration(), 0, () async => throw Exception("1")); + expect( + await step.run( + null, null, Duration(milliseconds: 1), Iterable.empty()), (r) { + return r is ErroredStepResult && + r.result == StepExecutionResult.error && + r.exception is Exception && + r.exception.toString() == "Exception: 1"; + }); + }); + }); + + group("expectation failures reported", () { + test("when an expectation fails the step is failed", () async { + final step = StepDefinitionMock(StepDefinitionConfiguration(), 0, + () async => throw TestFailure("1")); + expect( + await step.run( + null, null, Duration(milliseconds: 1), Iterable.empty()), (r) { + return r is StepResult && + r.result == StepExecutionResult.fail && + r.resultReason == "1"; + }); + }); + }); + }); +} diff --git a/test/gherkin/syntax/background_syntax_test.dart b/test/gherkin/syntax/background_syntax_test.dart new file mode 100644 index 0000000..5d87699 --- /dev/null +++ b/test/gherkin/syntax/background_syntax_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/background.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/background_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new BackgroundSyntax(); + expect(syntax.isMatch("Background: something"), true); + expect(syntax.isMatch(" Background: something"), true); + }); + + test('does not match', () { + final syntax = new BackgroundSyntax(); + expect(syntax.isMatch("Background something"), false); + expect(syntax.isMatch("#Background: something"), false); + }); + }); + + group("toRunnable", () { + test('creates BackgroundRunnable', () { + final syntax = new BackgroundSyntax(); + Runnable runnable = syntax.toRunnable("Background: A backgroun 123", + RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is BackgroundRunnable)); + expect(runnable.name, equals("A backgroun 123")); + }); + }); +} diff --git a/test/gherkin/syntax/comment_syntax_test.dart b/test/gherkin/syntax/comment_syntax_test.dart new file mode 100644 index 0000000..76d8215 --- /dev/null +++ b/test/gherkin/syntax/comment_syntax_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final keyword = new CommentSyntax(); + expect(keyword.isMatch("# I am a comment"), true); + expect(keyword.isMatch("#I am also a comment"), true); + expect(keyword.isMatch("## I am also a comment"), true); + expect(keyword.isMatch("# Language something"), true); + }); + + test('does not match', () { + final keyword = new CommentSyntax(); + // expect(keyword.isMatch("# language: en"), false); + expect(keyword.isMatch("I am not a comment"), false); + }); + }); +} diff --git a/test/gherkin/syntax/empty_line_syntax_test.dart b/test/gherkin/syntax/empty_line_syntax_test.dart new file mode 100644 index 0000000..0eb096e --- /dev/null +++ b/test/gherkin/syntax/empty_line_syntax_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_gherkin/src/gherkin/syntax/empty_line_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final keyword = new EmptyLineSyntax(); + expect(keyword.isMatch(""), true); + expect(keyword.isMatch(" "), true); + expect(keyword.isMatch(" "), true); + expect(keyword.isMatch(" "), true); + }); + + test('does not match', () { + final keyword = new EmptyLineSyntax(); + expect(keyword.isMatch("a"), false); + expect(keyword.isMatch(" b"), false); + expect(keyword.isMatch(" c"), false); + expect(keyword.isMatch(" ,"), false); + }); + }); +} diff --git a/test/gherkin/syntax/feature_syntax_test.dart b/test/gherkin/syntax/feature_syntax_test.dart new file mode 100644 index 0000000..6a5d997 --- /dev/null +++ b/test/gherkin/syntax/feature_syntax_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/feature.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/feature_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final keyword = new FeatureSyntax(); + expect(keyword.isMatch("Feature: one"), true); + expect(keyword.isMatch("Feature:one"), true); + }); + + test('does not match', () { + final keyword = new FeatureSyntax(); + expect(keyword.isMatch("#Feature: no"), false); + expect(keyword.isMatch("# Feature no"), false); + }); + }); + + group("toRunnable", () { + test('creates FeatureRunnable', () { + final keyword = new FeatureSyntax(); + Runnable runnable = keyword.toRunnable( + "Feature: A feature 123", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is FeatureRunnable)); + expect(runnable.name, equals("A feature 123")); + }); + }); +} diff --git a/test/gherkin/syntax/language_syntax_test.dart b/test/gherkin/syntax/language_syntax_test.dart new file mode 100644 index 0000000..6aed3e6 --- /dev/null +++ b/test/gherkin/syntax/language_syntax_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/language.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/language_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final keyword = new LanguageSyntax(); + expect(keyword.isMatch("# language: en"), true); + expect(keyword.isMatch("#language: fr"), true); + expect(keyword.isMatch("#language:de"), true); + }); + + test('does not match', () { + final keyword = new LanguageSyntax(); + expect(keyword.isMatch("#language no"), false); + expect(keyword.isMatch("# language comment"), false); + }); + }); + + group("toRunnable", () { + test('creates LanguageRunnable', () { + final keyword = new LanguageSyntax(); + LanguageRunnable runnable = keyword.toRunnable( + "# language: de", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is LanguageRunnable)); + expect(runnable.language, equals("de")); + }); + }); +} diff --git a/test/gherkin/syntax/multiline_string_syntax_test.dart b/test/gherkin/syntax/multiline_string_syntax_test.dart new file mode 100644 index 0000000..9b2b4d8 --- /dev/null +++ b/test/gherkin/syntax/multiline_string_syntax_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/multi_line_string.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/text_line_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new MultilineStringSyntax(); + expect(syntax.isMatch('"""'), true); + expect(syntax.isMatch('```'), true); + expect(syntax.isMatch("'''"), true); + }); + + test('does not match', () { + final syntax = new MultilineStringSyntax(); + expect(syntax.isMatch('#"""'), false); + expect(syntax.isMatch('#```'), false); + expect(syntax.isMatch("#'''"), false); + expect(syntax.isMatch('"'), false); + expect(syntax.isMatch('`'), false); + expect(syntax.isMatch("'"), false); + }); + }); + group("block", () { + test("is block", () { + final syntax = new MultilineStringSyntax(); + expect(syntax.isBlockSyntax, true); + }); + + test("continue block if text line string", () { + final syntax = new MultilineStringSyntax(); + expect(syntax.hasBlockEnded(new TextLineSyntax()), false); + }); + + test("continue block if comment string", () { + final syntax = new MultilineStringSyntax(); + expect(syntax.hasBlockEnded(new CommentSyntax()), false); + }); + + test("end block if multiline string", () { + final syntax = new MultilineStringSyntax(); + expect(syntax.hasBlockEnded(new MultilineStringSyntax()), true); + }); + }); + + group("toRunnable", () { + test('creates TextLineRunnable', () { + final syntax = new MultilineStringSyntax(); + MultilineStringRunnable runnable = + syntax.toRunnable("'''", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is MultilineStringRunnable)); + expect(runnable.lines.length, 0); + }); + }); +} diff --git a/test/gherkin/syntax/scenario_syntax_test.dart b/test/gherkin/syntax/scenario_syntax_test.dart new file mode 100644 index 0000000..6521b2f --- /dev/null +++ b/test/gherkin/syntax/scenario_syntax_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/runnable.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/scenario.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/scenario_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new ScenarioSyntax(); + expect(syntax.isMatch("Scenario: something"), true); + expect(syntax.isMatch(" Scenario: something"), true); + }); + + test('does not match', () { + final syntax = new ScenarioSyntax(); + expect(syntax.isMatch("Scenario something"), false); + expect(syntax.isMatch("#Scenario: something"), false); + }); + }); + + group("toRunnable", () { + test('creates FeatureRunnable', () { + final keyword = new ScenarioSyntax(); + Runnable runnable = keyword.toRunnable( + "Scenario: A scenario 123", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is ScenarioRunnable)); + expect(runnable.name, equals("A scenario 123")); + }); + }); +} diff --git a/test/gherkin/syntax/step_syntax_test.dart b/test/gherkin/syntax/step_syntax_test.dart new file mode 100644 index 0000000..d45049c --- /dev/null +++ b/test/gherkin/syntax/step_syntax_test.dart @@ -0,0 +1,78 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/step.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/step_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/table_line_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches given correctly', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("Given a step"), true); + expect(syntax.isMatch("given a step"), true); + }); + + test('matches then correctly', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("Then a step"), true); + expect(syntax.isMatch("then a step"), true); + }); + + test('matches when correctly', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("When I do something"), true); + expect(syntax.isMatch("when I do something"), true); + }); + + test('matches and correctly', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("And something"), true); + expect(syntax.isMatch("and something"), true); + }); + + test('matches but correctly', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("but something"), true); + expect(syntax.isMatch("but something"), true); + }); + + test('does not match', () { + final syntax = new StepSyntax(); + expect(syntax.isMatch("#given something"), false); + }); + }); + + group("block", () { + test("is block", () { + final syntax = new StepSyntax(); + expect(syntax.isBlockSyntax, true); + }); + + test("continue block if multiline string", () { + final syntax = new StepSyntax(); + expect(syntax.hasBlockEnded(new MultilineStringSyntax()), false); + }); + + test("continue block if table", () { + final syntax = new StepSyntax(); + expect(syntax.hasBlockEnded(new TableLineSyntax()), false); + }); + + test("end block if not multiline string or table", () { + final syntax = new StepSyntax(); + expect(syntax.hasBlockEnded(new StepSyntax()), true); + }); + }); + + group("toRunnable", () { + test('creates StepRunnable', () { + final syntax = new StepSyntax(); + StepRunnable runnable = syntax.toRunnable( + "Given I do something", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is StepRunnable)); + expect(runnable.name, equals("Given I do something")); + }); + }); +} diff --git a/test/gherkin/syntax/table_line_string_syntax_test.dart b/test/gherkin/syntax/table_line_string_syntax_test.dart new file mode 100644 index 0000000..22edb53 --- /dev/null +++ b/test/gherkin/syntax/table_line_string_syntax_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/table.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/comment_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/multiline_string_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/step_syntax.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/table_line_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new TableLineSyntax(); + expect(syntax.isMatch('||'), true); + expect(syntax.isMatch(' | | '), true); + expect(syntax.isMatch(" |a|b|c| "), true); + }); + + test('does not match', () { + final syntax = new TableLineSyntax(); + expect(syntax.isMatch('#||'), false); + expect(syntax.isMatch(' | '), false); + expect(syntax.isMatch(" |a|b|c "), false); + }); + }); + + group("block", () { + test("is block", () { + final syntax = new TableLineSyntax(); + expect(syntax.isBlockSyntax, true); + }); + + test("continue block if table line string", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new TableLineSyntax()), false); + }); + + test("continue block if comment string", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new CommentSyntax()), false); + }); + + test("end block if not table line string", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new MultilineStringSyntax()), true); + }); + }); + + group("block", () { + test("is block", () { + final syntax = new TableLineSyntax(); + expect(syntax.isBlockSyntax, true); + }); + + test("continue block if table line", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new TableLineSyntax()), false); + }); + + test("continue block if comment string", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new CommentSyntax()), false); + }); + + test("end block if step", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new StepSyntax()), true); + }); + + test("end block if multiline string", () { + final syntax = new TableLineSyntax(); + expect(syntax.hasBlockEnded(new MultilineStringSyntax()), true); + }); + }); + + group("toRunnable", () { + test('creates TableRunnable', () { + final syntax = new TableLineSyntax(); + TableRunnable runnable = syntax.toRunnable( + " | Row One | Row Two | ", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is TableRunnable)); + expect(runnable.rows.elementAt(0), "| Row One | Row Two |"); + expect(runnable.rows.length, 1); + }); + }); +} diff --git a/test/gherkin/syntax/tag_syntax_test.dart b/test/gherkin/syntax/tag_syntax_test.dart new file mode 100644 index 0000000..a407d73 --- /dev/null +++ b/test/gherkin/syntax/tag_syntax_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/tags.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/tag_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new TagSyntax(); + expect(syntax.isMatch("@tagone @tagtow @tag_three"), true); + expect(syntax.isMatch("@tag"), true); + }); + + test('does not match', () { + final syntax = new TagSyntax(); + expect(syntax.isMatch("not a tag"), false); + expect(syntax.isMatch("#@tag @tag2"), false); + }); + }); + + group("toRunnable", () { + test('creates TextLineRunnable', () { + final syntax = new TagSyntax(); + TagsRunnable runnable = syntax.toRunnable( + "@tag1 @tag2 @tag3@tag_4", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is TagsRunnable)); + expect(runnable.tags, equals(["tag1", "tag2", "tag3", "tag_4"])); + }); + }); +} diff --git a/test/gherkin/syntax/text_line_syntax_test.dart b/test/gherkin/syntax/text_line_syntax_test.dart new file mode 100644 index 0000000..7b669f6 --- /dev/null +++ b/test/gherkin/syntax/text_line_syntax_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_gherkin/src/gherkin/runnables/debug_information.dart'; +import 'package:flutter_gherkin/src/gherkin/runnables/text_line.dart'; +import 'package:flutter_gherkin/src/gherkin/syntax/text_line_syntax.dart'; +import 'package:test/test.dart'; + +void main() { + group("isMatch", () { + test('matches correctly', () { + final syntax = new TextLineSyntax(); + expect(syntax.isMatch("Hello Jon"), true); + expect(syntax.isMatch(" Hello Jon"), true); + expect(syntax.isMatch(" Hello Jon"), true); + expect(syntax.isMatch(" h "), true); + }); + + test('does not match', () { + final syntax = new TextLineSyntax(); + expect(syntax.isMatch("#Hello Jon"), false); + expect(syntax.isMatch("# Hello Jon"), false); + expect(syntax.isMatch("# Hello Jon"), false); + expect(syntax.isMatch(" "), false); + expect(syntax.isMatch(" # h "), false); + }); + }); + + group("toRunnable", () { + test('creates TextLineRunnable', () { + final syntax = new TextLineSyntax(); + TextLineRunnable runnable = syntax.toRunnable( + " Some text ", RunnableDebugInformation(null, 0, null)); + expect(runnable, isNotNull); + expect(runnable, predicate((x) => x is TextLineRunnable)); + expect(runnable.text, equals("Some text")); + }); + }); +} diff --git a/test/hooks/aggregated_hook_test.dart b/test/hooks/aggregated_hook_test.dart new file mode 100644 index 0000000..26c4904 --- /dev/null +++ b/test/hooks/aggregated_hook_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_gherkin/src/hooks/aggregated_hook.dart'; +import 'package:test/test.dart'; +import '../mocks/hook_mock.dart'; + +void main() { + group("orders hooks", () { + test("executes hooks in correct order", () async { + final executionOrder = List(); + final hookOne = HookMock( + providedPriority: 0, onBeforeRunCode: () => executionOrder.add(3)); + final hookTwo = HookMock( + providedPriority: 10, onBeforeRunCode: () => executionOrder.add(2)); + final hookThree = HookMock( + providedPriority: 20, onBeforeRunCode: () => executionOrder.add(1)); + final hookFour = HookMock( + providedPriority: -1, onBeforeRunCode: () => executionOrder.add(4)); + + final aggregatedHook = AggregatedHook(); + aggregatedHook.addHooks([hookOne, hookTwo, hookThree, hookFour]); + await aggregatedHook.onBeforeRun(null); + expect(executionOrder, [1, 2, 3, 4]); + expect(hookOne.onBeforeRunInvocationCount, 1); + expect(hookTwo.onBeforeRunInvocationCount, 1); + expect(hookThree.onBeforeRunInvocationCount, 1); + expect(hookFour.onBeforeRunInvocationCount, 1); + await aggregatedHook.onAfterRun(null); + expect(hookOne.onAfterRunInvocationCount, 1); + expect(hookTwo.onAfterRunInvocationCount, 1); + expect(hookThree.onAfterRunInvocationCount, 1); + expect(hookFour.onAfterRunInvocationCount, 1); + await aggregatedHook.onBeforeScenario(null, null); + expect(hookOne.onBeforeScenarioInvocationCount, 1); + expect(hookTwo.onBeforeScenarioInvocationCount, 1); + expect(hookThree.onBeforeScenarioInvocationCount, 1); + expect(hookFour.onBeforeScenarioInvocationCount, 1); + await aggregatedHook.onAfterScenario(null, null); + expect(hookOne.onAfterScenarioInvocationCount, 1); + expect(hookTwo.onAfterScenarioInvocationCount, 1); + expect(hookThree.onAfterScenarioInvocationCount, 1); + expect(hookFour.onAfterScenarioInvocationCount, 1); + }); + }); +} diff --git a/test/mocks/gherkin_expression_mock.dart b/test/mocks/gherkin_expression_mock.dart new file mode 100644 index 0000000..fd7b022 --- /dev/null +++ b/test/mocks/gherkin_expression_mock.dart @@ -0,0 +1,18 @@ +import 'package:flutter_gherkin/src/gherkin/expressions/gherkin_expression.dart'; + +typedef bool IsMatchFn(String input); + +class MockGherkinExpression implements GherkinExpression { + final IsMatchFn isMatchFn; + + MockGherkinExpression(this.isMatchFn); + + @override + Iterable getParameters(String input) => Iterable.empty(); + + @override + bool isMatch(String input) => isMatchFn(input); + + @override + RegExp get originalExpression => null; +} diff --git a/test/mocks/hook_mock.dart b/test/mocks/hook_mock.dart new file mode 100644 index 0000000..3fafa95 --- /dev/null +++ b/test/mocks/hook_mock.dart @@ -0,0 +1,36 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +typedef void OnBeforeRunCode(); + +class HookMock extends Hook { + int onBeforeRunInvocationCount = 0; + int onAfterRunInvocationCount = 0; + int onBeforeScenarioInvocationCount = 0; + int onAfterScenarioInvocationCount = 0; + + final int providedPriority; + final OnBeforeRunCode onBeforeRunCode; + + @override + int get priority => providedPriority; + + HookMock({this.onBeforeRunCode, this.providedPriority = 0}); + + Future onBeforeRun(TestConfiguration config) async { + onBeforeRunInvocationCount += 1; + if (onBeforeRunCode != null) { + onBeforeRunCode(); + } + } + + Future onAfterRun(TestConfiguration config) async => + onAfterRunInvocationCount += 1; + + Future onBeforeScenario( + TestConfiguration config, String scenario) async => + onBeforeScenarioInvocationCount += 1; + + Future onAfterScenario( + TestConfiguration config, String scenario) async => + onAfterScenarioInvocationCount += 1; +} diff --git a/test/mocks/reporter_mock.dart b/test/mocks/reporter_mock.dart new file mode 100644 index 0000000..aff3414 --- /dev/null +++ b/test/mocks/reporter_mock.dart @@ -0,0 +1,43 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +typedef void OnStepFinished(StepFinishedMessage message); + +class ReporterMock extends Reporter { + int onTestRunStartedInvocationCount = 0; + int onTestRunfinishedInvocationCount = 0; + int onFeatureStartedInvocationCount = 0; + int onFeatureFinishedInvocationCount = 0; + int onScenarioStartedInvocationCount = 0; + int onScenarioFinishedInvocationCount = 0; + int onStepStartedInvocationCount = 0; + int onStepFinishedInvocationCount = 0; + int onExceptionInvocationCount = 0; + int messageInvocationCount = 0; + int disposeInvocationCount = 0; + + OnStepFinished onStepFinishedFn; + + Future onTestRunStarted() async => onTestRunStartedInvocationCount += 1; + Future onTestRunfinished() async => + onTestRunfinishedInvocationCount += 1; + Future onFeatureStarted(StartedMessage message) async => + onFeatureStartedInvocationCount += 1; + Future onFeatureFinished(FinishedMessage message) async => + onFeatureFinishedInvocationCount += 1; + Future onScenarioStarted(StartedMessage message) async => + onScenarioStartedInvocationCount += 1; + Future onScenarioFinished(FinishedMessage message) async => + onScenarioFinishedInvocationCount += 1; + Future onStepStarted(StartedMessage message) async => + onStepStartedInvocationCount += 1; + Future onStepFinished(StepFinishedMessage message) async { + if (onStepFinishedFn != null) onStepFinishedFn(message); + onStepFinishedInvocationCount += 1; + } + + Future onException(Exception exception, StackTrace stackTrace) async => + onExceptionInvocationCount += 1; + Future message(String message, MessageLevel level) async => + messageInvocationCount += 1; + Future dispose() async => disposeInvocationCount += 1; +} diff --git a/test/mocks/step_definition_mock.dart b/test/mocks/step_definition_mock.dart new file mode 100644 index 0000000..2d3e480 --- /dev/null +++ b/test/mocks/step_definition_mock.dart @@ -0,0 +1,23 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +typedef Future OnRunCode(Iterable parameters); + +class MockStepDefinition extends StepDefinitionGeneric { + bool hasRun = false; + int runCount = 0; + final OnRunCode code; + + MockStepDefinition([this.code]) : super(null, 0); + + @override + Future onRun(Iterable parameters) async { + hasRun = true; + runCount += 1; + if (code != null) { + await code(parameters); + } + } + + @override + RegExp get pattern => null; +} diff --git a/test/mocks/tag_expression_evaluator_mock.dart b/test/mocks/tag_expression_evaluator_mock.dart new file mode 100644 index 0000000..e7b69b1 --- /dev/null +++ b/test/mocks/tag_expression_evaluator_mock.dart @@ -0,0 +1,6 @@ +import 'package:flutter_gherkin/src/gherkin/expressions/tag_expression.dart'; + +class MockTagExpressionEvaluator implements TagExpressionEvaluator { + @override + bool evaluate(String tagExpression, List tags) => true; +} diff --git a/test/mocks/world_mock.dart b/test/mocks/world_mock.dart new file mode 100644 index 0000000..7544f36 --- /dev/null +++ b/test/mocks/world_mock.dart @@ -0,0 +1,8 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; + +class WorldMock extends World { + bool disposeFnInvoked = false; + + @override + void dispose() => disposeFnInvoked = true; +} diff --git a/test/reporters/aggregated_reporter_test.dart b/test/reporters/aggregated_reporter_test.dart new file mode 100644 index 0000000..264d049 --- /dev/null +++ b/test/reporters/aggregated_reporter_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:flutter_gherkin/src/reporters/aggregated_reporter.dart'; +import 'package:test/test.dart'; + +import '../mocks/reporter_mock.dart'; + +void main() { + group("reporters", () { + test("invokes all child reporters", () async { + final reporter1 = ReporterMock(); + final reporter2 = ReporterMock(); + + final aggregatedReporter = AggregatedReporter(); + aggregatedReporter.addReporter(reporter1); + aggregatedReporter.addReporter(reporter2); + + await aggregatedReporter.message("", MessageLevel.info); + expect(reporter1.messageInvocationCount, 1); + expect(reporter2.messageInvocationCount, 1); + + await aggregatedReporter.onException(null, null); + expect(reporter1.onExceptionInvocationCount, 1); + expect(reporter2.onExceptionInvocationCount, 1); + + await aggregatedReporter.onFeatureStarted(null); + expect(reporter1.onFeatureStartedInvocationCount, 1); + expect(reporter2.onFeatureStartedInvocationCount, 1); + + await aggregatedReporter.onFeatureFinished(null); + expect(reporter1.onFeatureFinishedInvocationCount, 1); + expect(reporter2.onFeatureFinishedInvocationCount, 1); + + await aggregatedReporter.onScenarioFinished(null); + expect(reporter1.onScenarioFinishedInvocationCount, 1); + expect(reporter2.onScenarioFinishedInvocationCount, 1); + + await aggregatedReporter.onScenarioStarted(null); + expect(reporter1.onScenarioStartedInvocationCount, 1); + expect(reporter2.onScenarioStartedInvocationCount, 1); + + await aggregatedReporter.onStepFinished(null); + expect(reporter1.onStepFinishedInvocationCount, 1); + expect(reporter2.onStepFinishedInvocationCount, 1); + + await aggregatedReporter.onStepStarted(null); + expect(reporter1.onStepStartedInvocationCount, 1); + expect(reporter2.onStepStartedInvocationCount, 1); + + await aggregatedReporter.onTestRunfinished(); + expect(reporter1.onTestRunfinishedInvocationCount, 1); + expect(reporter2.onTestRunfinishedInvocationCount, 1); + + await aggregatedReporter.onTestRunStarted(); + await aggregatedReporter.onTestRunStarted(); + expect(reporter1.onTestRunStartedInvocationCount, 2); + expect(reporter2.onTestRunStartedInvocationCount, 2); + + await aggregatedReporter.dispose(); + expect(reporter1.disposeInvocationCount, 1); + expect(reporter2.disposeInvocationCount, 1); + }); + }); +}