initial commit

This commit is contained in:
Jon Samwell 2018-10-26 21:09:22 +11:00
commit 23055b4092
190 changed files with 7208 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/
ios/.generated/
ios/Flutter/Generated.xcconfig
ios/Runner/GeneratedPluginRegistrant.*

20
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

23
.vscode/tasks.json vendored Normal file
View File

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

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## [0.0.1] - TODO: Add release date.
* Initial release

21
LICENSE Normal file
View File

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

425
README.md Normal file
View File

@ -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
<!-- TOC -->
- [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)
<!-- /TOC -->
## Getting Started
See <https://docs.cucumber.io/gherkin/> 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<String, int, FlutterWorld> {
@override
Future<void> 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 <https://docs.cucumber.io/gherkin/reference/#steps>.
#### 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<String> {
@override
Future<void> 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<int> {
@override
Future<void> executeStep(int count) async {
// example code
final actualCount = await _getActualCount();
expectMatch(actualCount, count);
}
@override
RegExp get pattern => RegExp(r"I expect {int} apple(s)");
}
```
**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<String, int, FlutterWorld> {
TapButtonNTimesStep()
: super(StepDefinitionConfiguration()..timeout = Duration(seconds: 10));
@override
Future<void> executeStep(String input1, int input2) async {
final locator = find.byValueKey(input1);
for (var i = 0; i < 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<String, String> {
@override
Future<void> executeStep(String commentType, String comment) async {
// TODO: implement executeStep
}
@override
RegExp get pattern => RegExp(r"I provide the following {string} comment");
}
```
#### 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<Table> {
@override
Future<void> 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 <https://docs.cucumber.io/cucumber/cucumber-expressions/#parameter-types>).
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<Color> {
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<Color> {
@override
Future<void> 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 <https://docs.cucumber.io/cucumber/api/#tags>
## 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"
}
]
}
```

71
example/.gitignore vendored Normal file
View File

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

8
example/.metadata Normal file
View File

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

8
example/README.md Normal file
View File

@ -0,0 +1,8 @@
# example
A new Flutter project.
## Getting Started
For help getting started with Flutter, view our online
[documentation](https://flutter.io/).

View File

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

View File

@ -0,0 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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);
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

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

View File

@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx1536M

View File

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

View File

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

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,6 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>example</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,9 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

99
example/lib/main.dart Normal file
View File

@ -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<MyHomePage> {
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: <Widget>[
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: <Widget>[
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),
),
);
}
}

74
example/pubspec.yaml Normal file
View File

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

View File

@ -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());
}

View File

@ -0,0 +1,13 @@
import 'dart:async';
import 'package:glob/glob.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
Future<void> 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);
}

View File

@ -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");
// });
// });
// }

View File

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

View File

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

View File

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

View File

@ -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<Color> {
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;
}
});
}

View File

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

View File

@ -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<Color> {
@override
Future<void> executeStep(Color input1) async {
// TODO: implement executeStep
}
@override
RegExp get pattern => RegExp(r"I pick a {colour}");
}

View File

@ -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<String, String> {
@override
Future<void> executeStep(String commentType, String comment) async {
// TODO: implement executeStep
}
@override
RegExp get pattern => RegExp(r"I provide the following {string} comment");
}

View File

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

19
flutter_cucumber.iml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart Packages" level="project" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>

31
lib/flutter_gherkin.dart Normal file
View File

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

View File

@ -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<World> CreateWorld(TestConfiguration config);
enum ExecutionOrder { sequential, random }
class TestConfiguration {
/// The glob path(s) to all the features
Iterable<Glob> 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<StepDefinitionBase> stepDefinitions;
/// Any user defined step parameters
Iterable<CustomParameter<dynamic>> customStepParameterDefinitions;
/// Hooks that are run at certain points in the execution cycle
Iterable<Hook> hooks;
/// a list of reporters to use.
/// Built-in reporters:
/// - StdoutReporter
///
/// Custom reporters can be created by extending (or implementing) Reporter.dart
Iterable<Reporter> 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;
}

View File

@ -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));
}
}
}

View File

@ -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();
}

View File

@ -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<ExectuableStep> _steps;
final Reporter _reporter;
final Hook _hook;
FeatureFileRunner(this._config, this._tagExpressionEvaluator, this._steps,
this._reporter, this._hook);
Future<bool> run(FeatureFile featureFile) async {
bool haveAllFeaturesPassed = true;
for (var feature in featureFile.features) {
haveAllFeaturesPassed &= await _runFeature(feature);
}
return haveAllFeaturesPassed;
}
Future<bool> _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<bool> _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<StepResult> _runStep(
StepRunnable step, World world, bool skipExecution) async {
StepResult result;
ExectuableStep code = _matchStepToExectuableStep(step);
Iterable<dynamic> 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<StepResult>(
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<T> _runWithinTest<T>(String name, Future<T> fn()) async {
// the timeout is handled indepedently from this
final completer = Completer<T>();
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<String, bool, int, num>`
/// You can also specify the type of world context you want
/// `When4WithWorld<String, bool, int, num, MyWorld>`
class Given_${step.debug.lineText.trim().replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_')} extends Given1<String> {
@override
RegExp get pattern => RegExp(r"${step.debug.lineText}");
@override
Future<void> 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<dynamic> _getStepParameters(StepRunnable step, ExectuableStep code) {
Iterable<dynamic> parameters =
code.expression.getParameters(step.debug.lineText);
if (step.multilineStrings.length > 0) {
parameters = parameters.toList()..addAll(step.multilineStrings);
}
return parameters;
}
Future<void> _log(String message, RunnableDebugInformation context,
MessageLevel level) async {
await _reporter.message(
"$message # ${context.filePath}:${context.lineNumber}", level);
}
}

View File

@ -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<String> _processStdoutStream;
List<StreamSubscription> _openSubscriptions = List<StreamSubscription>();
String _appTarget;
String _workingDirectory;
void setApplicationTargetFile(String targetPath) {
_appTarget = targetPath;
}
void setWorkingDirectory(String workingDirectory) {
_workingDirectory = workingDirectory;
}
@override
Future<void> 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<int> 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<String> waitForObservatoryDebuggerUri(
[Duration timeout = const Duration(seconds: 60)]) {
_ensureRunningProcess();
final completer = Completer<String>();
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");
}
}
}

View File

@ -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<FlutterDriver> 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<FlutterWorld> createFlutterWorld(TestConfiguration config) async {
final world = new FlutterWorld();
final driver = await createFlutterDriver();
world.setFlutterDriver(driver);
return world;
}
}

View File

@ -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();
}
}

View File

@ -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<void> onBeforeRun(TestConfiguration config) async {
await _runApp(_castConfig(config));
}
Future<void> onAfterRun(TestConfiguration config) async =>
await _terminateApp();
Future<void> onBeforeScenario(
TestConfiguration config, String scenario) async {
final flutterConfig = _castConfig(config);
if (_flutterAppProcess == null) {
await _runApp(flutterConfig);
}
}
Future<void> onAfterScenario(
TestConfiguration config, String scenario) async {
final flutterConfig = _castConfig(config);
haveRunFirstScenario = true;
if (_flutterAppProcess != null &&
flutterConfig.restartAppBetweenScenarios) {
await _terminateApp();
}
}
Future<void> _runApp(FlutterTestConfiguration config) async {
_flutterAppProcess = new FlutterRunProcessHandler();
_flutterAppProcess.setApplicationTargetFile(config.targetAppPath);
await _flutterAppProcess.run();
final observatoryUri =
await _flutterAppProcess.waitForObservatoryDebuggerUri();
config.setObservatoryDebuggerUri(observatoryUri);
}
Future<void> _terminateApp() async {
if (_flutterAppProcess != null) {
await _flutterAppProcess.terminate();
_flutterAppProcess = null;
}
}
FlutterTestConfiguration _castConfig(TestConfiguration config) =>
config as FlutterTestConfiguration;
}

View File

@ -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<String, FlutterWorld> {
@override
RegExp get pattern => RegExp(r"I (open|close) the drawer");
@override
Future<void> executeStep(String action) async {
final drawerFinder = find.byType("Drawer");
final isOpen =
await FlutterDriverUtils().isPresent(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);
}
}
}

View File

@ -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<String, String, FlutterWorld> {
@override
RegExp get pattern => RegExp(r"I expect the {string} to be {string}");
@override
Future<void> executeStep(String key, String value) async {
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;
}
}
}

View File

@ -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<String, FlutterWorld> {
@override
RegExp get pattern => RegExp(
r"I tap the {string} [button|element|label|icon|field|text|widget]");
@override
Future<void> executeStep(String key) async {
final locator = find.byValueKey(key);
await world.driver.tap(locator, timeout: timeout);
}
}

View File

@ -0,0 +1,13 @@
import 'package:flutter_driver/flutter_driver.dart';
class FlutterDriverUtils {
Future<bool> isPresent(SerializableFinder finder, FlutterDriver driver,
{Duration timeout = const Duration(seconds: 1)}) async {
try {
await driver.waitFor(finder, timeout: timeout);
return true;
} catch (e) {
return false;
}
}
}

View File

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

View File

@ -0,0 +1,5 @@
class GherkinStepNotDefinedException implements Exception {
final String message;
GherkinStepNotDefinedException(this.message);
}

View File

@ -0,0 +1,5 @@
class GherkinSyntaxException implements Exception {
final String message;
GherkinSyntaxException(this.message);
}

View File

@ -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<dynamic> parameter;
_SortedParameterPosition(this.startPosition, this.parameter);
}
class GherkinExpression {
final RegExp originalExpression;
final List<_SortedParameterPosition> _sortedParameterPositions =
List<_SortedParameterPosition>();
RegExp _expression;
GherkinExpression(this.originalExpression,
Iterable<CustomParameter<dynamic>> 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<dynamic> getParameters(String input) {
List<String> stringValues = List<String>();
List<dynamic> values = List<dynamic>();
_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;
}
}

View File

@ -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<String, int> _operatorPrededence = {
"not": 4,
"or": 2,
"and": 2,
"(": 0,
};
bool evaluate(String tagExpression, List<String> tags) {
bool match = true;
final rpn = _convertInfixToPostfixExpression(tagExpression);
match = _evaluateRpn(rpn, tags);
return match;
}
bool _evaluateRpn(Queue<String> rpn, List<String> tags) {
Queue<bool> stack = Queue<bool>();
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<String> _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<String>();
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;
}
}
}

View File

@ -0,0 +1,7 @@
class Feature {
String name;
String language;
Iterable<String> tags;
Feature();
}

View File

@ -0,0 +1,8 @@
import 'package:flutter_gherkin/src/gherkin/models/table_row.dart';
class Table {
final Iterable<TableRow> rows;
final TableRow header;
Table(this.rows, this.header);
}

View File

@ -0,0 +1,7 @@
class TableRow {
final bool isHeaderRow;
final int rowIndex;
final Iterable<String> columns;
TableRow(this.columns, this.rowIndex, this.isHeaderRow);
}

View File

@ -0,0 +1,36 @@
typedef TValue Transformer<TValue>(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<T> {
/// 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<T> 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});
}

View File

@ -0,0 +1,25 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class FloatParameterBase extends CustomParameter<num> {
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");
}

View File

@ -0,0 +1,17 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class IntParameterBase extends CustomParameter<int> {
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");
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class PluralParameter extends CustomParameter<String> {
PluralParameter()
: super("s", RegExp("(s)?"), (String input) => null,
identifierPrefix: "(",
identifierSuffix: ")",
includeInParameterList: false);
}

View File

@ -0,0 +1,6 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class UserDefinedStepParameterParameter extends CustomParameter<String> {
UserDefinedStepParameterParameter()
: super("", RegExp(""), (String input) => input);
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class StringParameterBase extends CustomParameter<String> {
StringParameterBase(String name)
: super(name, RegExp("['|\"](.*)['|\"]"), (String input) => input);
}
class StringParameterLower extends StringParameterBase {
StringParameterLower() : super("string");
}
class StringParameterCamel extends StringParameterBase {
StringParameterCamel() : super("String");
}

View File

@ -0,0 +1,14 @@
import 'package:flutter_gherkin/src/gherkin/parameters/custom_parameter.dart';
class WordParameterBase extends CustomParameter<String> {
WordParameterBase(String name)
: super(name, RegExp("['|\"](\\w+)['|\"]"), (String input) => input);
}
class WordParameterLower extends WordParameterBase {
WordParameterLower() : super("word");
}
class WordParameterCamel extends WordParameterBase {
WordParameterCamel() : super("Word");
}

View File

@ -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<SyntaxMatcher> syntaxMatchers = [
LanguageSyntax(),
CommentSyntax(),
FeatureSyntax(),
BackgroundSyntax(),
TagSyntax(),
ScenarioSyntax(),
StepSyntax(),
MultilineStringSyntax(),
EmptyLineSyntax(),
TableLineSyntax(),
TextLineSyntax()
];
Future<FeatureFile> 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<String> 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;
}
}

Some files were not shown because too many files have changed in this diff Show More