- Fix #257 - fixed issue when generating a step with a '$' sign in

- Fix #256 - Ensure all exceptions generated when running a step are logged
  - Fix #253 - Ensure features with descriptions that span more than one line are parsed correctly
  - Fix #252 - Ensure all async code is awaited
  - When taking a screenshot on the web use the render element rather than relying on native code that does not work
This commit is contained in:
Jon 2022-07-25 17:08:05 +10:00
parent 4378a2447d
commit eff82509c7
17 changed files with 252 additions and 1329 deletions

View File

@ -1,3 +1,10 @@
## [3.0.0-rc.17] - 25/07/2022
- Fix #257 - fixed issue when generating a step with a '$' sign in
- Fix #256 - Ensure all exceptions generated when running a step are logged
- Fix #253 - Ensure features with descriptions that span more than one line are parsed correctly
- Fix #252 - Ensure all async code is awaited
- When taking a screenshot on the web use the render element rather than relying on native code that does not work
## [3.0.0-rc.16] - 01/07/2022
- Fix #231 - using local coordinate system when taking a screenshot on Android (thanks to @youssef-t for the solution)
- Fix #216 - ensure step exceptions and `expect` failure results are added as errors to the json report

View File

@ -1,14 +1,11 @@
@debug
Feature: Expect failure
Feature: Expect failures
Ensure that when a test fails the exception or test failure is reported
@failure-expected
Scenario: Exception should be added to json report
Given I expect the todo list
| Todo |
| Buy blueberries |
When I tap the "add" button
And I fill the "todo" field with "Buy hannah's apples"
When I tap the "button is not here but exception should be logged in report" button
@failure-expected
Scenario: Failed expect() should be added to json report
Description for this scenario!
When I tap the "add" button

View File

@ -0,0 +1,9 @@
@debug
Feature: Parsing
Complex description:
- Line "one".
- Line two, more text
- Line three
Scenario: Parsing a
Given the text "^[A-Z]{3}\\d{5}\$"

View File

@ -1,7 +1,6 @@
@tag
Feature: Swiping
@debug
Scenario: User can swipe cards left and right
Given I swipe right by 250 pixels on the "scrollable cards"`
Then I expect the text "Page 2" to be present

View File

@ -8,18 +8,20 @@ import 'package:gherkin/gherkin.dart';
import 'hooks/reset_app_hook.dart';
import 'steps/expect_failure.dart';
import 'steps/expect_todos_step.dart';
import 'steps/given_text.dart';
import 'steps/multiline_string_with_formatted_json.dart';
import 'steps/when_await_animation.dart';
import 'steps/when_step_has_timeout.dart';
import 'world/custom_world.dart';
FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration(
// tagExpression: '@debug', // can be used to limit the tests that are run
tagExpression: '@debug2', // can be used to limit the tests that are run
stepDefinitions: [
thenIExpectTheTodos,
whenAnAnimationIsAwaited,
whenStepHasTimeout,
givenTheData,
givenTheText,
thenIExpectFailure,
],
hooks: [

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ call npm init -y
call npm install --save-dev cucumber-html-reporter
node -e "require('cucumber-html-reporter').generate({theme: 'bootstrap', jsonFile: 'REPORT_NAME.json', output: 'report.html', reportSuiteAsScenarios: true, launchReport: false});"
node -e "require('cucumber-html-reporter').generate({theme: 'bootstrap', jsonFile: '{REPORT_NAME}.json', output: 'report.html', reportSuiteAsScenarios: true, launchReport: false});"

View File

@ -0,0 +1,9 @@
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
final givenTheText = given1<String, FlutterWidgetTesterWorld>(
'the text {String}',
(text, world) async {
print(text);
},
);

View File

@ -13,5 +13,14 @@ void main() {
executeTestSuite(
appMainFunction: appInitializationFn,
configuration: gherkinTestConfiguration,
// if you have lots of test you might need to increase the default timeout
// scenarioExecutionTimeout: Timeout(const Duration(minutes: 30)),
// if your app has lots of endless animations you might need to
// provide your own app lifecycle pump handler that doesn't pump
// at certain lifecycle stages
// appLifecyclePumpHandler: (appPhase, widgetTester) async => {},
// you can increase the performance of your tests at the cost of
// not drawing some frames but it might lead to unexpected consequences
// framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive,
);
}

View File

@ -27,6 +27,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
testFeature1();
testFeature2();
testFeature3();
testFeature4();
}
void testFeature0() {
@ -91,7 +92,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
],
onBefore: () async => onBeforeRunFeature(
name: 'Creating todos',
path: r'.\\integration_test\\features\\create.feature',
path: '.\\integration_test\\features\\create.feature',
description: null,
tags: <String>['@tag'],
),
@ -101,7 +102,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
name: 'User can create multiple new todo items',
description: null,
path: '.\\integration_test\\features\\create.feature',
tags: <String>['@tag'],
tags: <String>['@tag', '@debug2'],
steps: [
(
TestDependencies dependencies,
@ -241,7 +242,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
],
onAfter: () async => onAfterRunFeature(
name: 'Creating todos',
path: r'.\\integration_test\\features\\create.feature',
path: '.\\integration_test\\features\\create.feature',
description: null,
tags: <String>['@tag'],
),
@ -302,13 +303,13 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
],
onBefore: () async => onBeforeRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
path: '.\\integration_test\\features\\check.feature',
description: null,
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
path: '.\\integration_test\\features\\check.feature',
description: null,
tags: <String>['@tag'],
),
@ -326,7 +327,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
name: 'User can swipe cards left and right',
description: null,
path: '.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag', '@debug'],
tags: <String>['@tag'],
steps: [
(
TestDependencies dependencies,
@ -381,13 +382,13 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
],
onBefore: () async => onBeforeRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
path: '.\\integration_test\\features\\swiping.feature',
description: null,
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
path: '.\\integration_test\\features\\swiping.feature',
description: null,
tags: <String>['@tag'],
),
@ -398,13 +399,13 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
void testFeature3() {
runFeature(
name: 'Expect failure:',
name: 'Parsing:',
tags: <String>['@debug'],
run: () {
runScenario(
name: 'Exception should be added to json report',
name: 'Parsing a',
description: null,
path: '.\\integration_test\\features\\failure.feature',
path: '.\\integration_test\\features\\parsing.feature',
tags: <String>['@debug'],
steps: [
(
@ -412,31 +413,7 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
bool skip,
) async {
return await runStep(
name: 'Given I expect the todo list',
multiLineStrings: <String>[],
table: GherkinTable.fromJson('[{"Todo":"Buy blueberries"}]'),
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'When I tap the "add" button',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'And I fill the "todo" field with "Buy hannah\'s apples"',
name: 'Given the text "^[A-Z]{3}\\\\d{5}\\\$"',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
@ -445,19 +422,67 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Expect failure',
path: r'.\\integration_test\\features\\failure.feature',
description:
"Ensure that when a test fails the exception or test failure is reported",
name: 'Parsing',
path: '.\\integration_test\\features\\parsing.feature',
description: """Complex description:
- Line "one".
- Line two, more text
- Line three""",
tags: <String>['@debug'],
),
onAfter: () async => onAfterRunFeature(
name: 'Parsing',
path: '.\\integration_test\\features\\parsing.feature',
description: """Complex description:
- Line "one".
- Line two, more text
- Line three""",
tags: <String>['@debug'],
),
);
},
);
}
void testFeature4() {
runFeature(
name: 'Expect failures:',
tags: <String>[],
run: () {
runScenario(
name: 'Exception should be added to json report',
description: null,
path: '.\\integration_test\\features\\failure.feature',
tags: <String>['@failure-expected'],
steps: [
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name:
'When I tap the "button is not here but exception should be logged in report" button',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Expect failures',
path: '.\\integration_test\\features\\failure.feature',
description:
"""Ensure that when a test fails the exception or test failure is reported""",
tags: <String>[],
),
);
runScenario(
name: 'Failed expect() should be added to json report',
description: "Description for this scenario!",
path: '.\\integration_test\\features\\failure.feature',
tags: <String>['@debug'],
tags: <String>['@failure-expected'],
steps: [
(
TestDependencies dependencies,
@ -497,11 +522,11 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
},
],
onAfter: () async => onAfterRunFeature(
name: 'Expect failure',
path: r'.\\integration_test\\features\\failure.feature',
name: 'Expect failures',
path: '.\\integration_test\\features\\failure.feature',
description:
"Ensure that when a test fails the exception or test failure is reported",
tags: <String>['@debug'],
"""Ensure that when a test fails the exception or test failure is reported""",
tags: <String>[],
),
);
},

View File

@ -213,7 +213,7 @@ packages:
path: ".."
relative: true
source: path
version: "3.0.0-rc.16"
version: "3.0.0-rc.17"
flutter_simple_dependency_injection:
dependency: "direct main"
description:

View File

@ -72,7 +72,7 @@ class WidgetTesterAppDriverAdapter
}
}
Future<List<int>> screenshotOnAndroid() async {
Future<List<int>> takeScreenshotUsingRenderElement() async {
RenderObject? renderObject = binding.renderViewElement?.renderObject;
if (renderObject != null) {
while (!renderObject!.isRepaintBoundary) {
@ -99,22 +99,18 @@ class WidgetTesterAppDriverAdapter
Future<List<int>> screenshot({String? screenshotName}) async {
final name =
screenshotName ?? 'screenshot_${DateTime.now().millisecondsSinceEpoch}';
if (kIsWeb) {
return binding.takeScreenshot(name);
} else {
if (Platform.isAndroid) {
// try {
// // TODO: See https://github.com/flutter/flutter/issues/92381
// // we need to call `revertFlutterImage` once it has been implemented
// await binding.convertFlutterSurfaceToImage();
// await binding.pump();
// // ignore: no_leading_underscores_for_local_identifiers
// } catch (_, __) {}
if (kIsWeb || Platform.isAndroid) {
// try {
// // TODO: See https://github.com/flutter/flutter/issues/92381
// // we need to call `revertFlutterImage` once it has been implemented
// await binding.convertFlutterSurfaceToImage();
// await binding.pump();
// // ignore: no_leading_underscores_for_local_identifiers
// } catch (_, __) {}
return await screenshotOnAndroid();
} else {
return await binding.takeScreenshot(name);
}
return await takeScreenshotUsingRenderElement();
} else {
return await binding.takeScreenshot(name);
}
}

View File

@ -183,10 +183,18 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor {
);}
''';
static const String onBeforeScenarioRun = '''
onBefore: () async => onBeforeRunFeature(name:'{{feature_name}}', path:'{{path}}', description: {{feature_description}}, tags:{{feature_tags}},),
onBefore: () async => onBeforeRunFeature(
name:'{{feature_name}}',
path:'{{path}}',
description: {{feature_description}},
tags:{{feature_tags}},),
''';
static const String onAfterScenarioRun = '''
onAfter: () async => onAfterRunFeature(name:'{{feature_name}}', path:'{{path}}', description: {{feature_description}}, tags:{{feature_tags}},),
onAfter: () async => onAfterRunFeature(
name:'{{feature_name}}',
path:'{{path}}',
description: {{feature_description}},
tags:{{feature_tags}},),
''';
final StringBuffer _buffer = StringBuffer();
@ -275,7 +283,9 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor {
_currentScenarioCode = _replaceVariable(
_currentScenarioCode!,
'feature_description',
_escapeText(featureDescription == null ? null : '"$featureDescription"'),
_escapeText(
featureDescription == null ? null : '"""$featureDescription"""',
),
);
_currentScenarioCode = _replaceVariable(
_currentScenarioCode!,
@ -370,6 +380,8 @@ class FeatureFileTestGeneratorVisitor extends FeatureFileVisitor {
return content.replaceAll('{{$property}}', value ?? 'null');
}
String? _escapeText(String? text) =>
text?.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
String? _escapeText(String? text) => text
?.replaceAll("\\", "\\\\")
.replaceAll("'", "\\'")
.replaceAll(r"$", r"\$");
}

View File

@ -1,3 +1,5 @@
// ignore_for_file: avoid_print
import 'package:flutter_gherkin/flutter_gherkin_with_driver.dart';
import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart';
import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart';

View File

@ -83,24 +83,24 @@ abstract class GherkinIntegrationTestRunner {
_binding.framePolicy = framePolicy ?? _binding.framePolicy;
tearDownAll(
() {
onRunComplete();
() async {
await onRunComplete();
},
);
_safeInvokeFuture(() async => await hook.onBeforeRun(configuration));
_safeInvokeFuture(() async => await reporter.test.onStarted.invoke());
await hook.onBeforeRun(configuration);
await reporter.test.onStarted.invoke();
onRun();
}
void onRun();
void onRunComplete() {
_safeInvokeFuture(() async => await reporter.test.onFinished.invoke());
_safeInvokeFuture(() async => await hook.onAfterRun(configuration));
Future<void> onRunComplete() async {
await reporter.test.onFinished.invoke();
await hook.onAfterRun(configuration);
setTestResultData(_binding);
_safeInvokeFuture(() async => await reporter.dispose());
() async => await reporter.dispose();
}
void setTestResultData(IntegrationTestWidgetsFlutterBinding binding) {
@ -226,9 +226,11 @@ abstract class GherkinIntegrationTestRunner {
failed = true;
hasToSkip = true;
}
} catch (e) {
} catch (err, st) {
failed = true;
hasToSkip = true;
await reporter.onException(err, st);
}
}
} finally {
@ -258,18 +260,16 @@ abstract class GherkinIntegrationTestRunner {
tester,
);
cleanUpScenarioRun(dependencies);
await cleanUpScenarioRun(dependencies);
}
},
timeout: scenarioExecutionTimeout,
semanticsEnabled: configuration.semanticsEnabled,
);
} else {
_safeInvokeFuture(
() async => reporter.message(
'Ignoring scenario `$name` as tag expression `${configuration.tagExpression}` not satisfied',
MessageLevel.info,
),
reporter.message(
'Ignoring scenario `$name` as tag expression `${configuration.tagExpression}` not satisfied',
MessageLevel.info,
);
}
}
@ -325,52 +325,62 @@ abstract class GherkinIntegrationTestRunner {
required TestDependencies dependencies,
required bool skip,
}) async {
final executable = _executableSteps!.firstWhereOrNull(
(s) => s.expression.isMatch(name),
);
if (executable == null) {
final message = 'Step definition not found for text: `$name`';
throw GherkinStepNotDefinedException(message);
}
var parameters = _getStepParameters(
step: name,
multiLineStrings: multiLineStrings,
table: table,
code: executable,
);
await _onBeforeStepRun(
world: dependencies.world,
step: name,
table: table,
multiLineStrings: multiLineStrings,
);
StepResult? result;
if (skip) {
result = StepResult(
0,
StepExecutionResult.skipped,
resultReason: 'Previous step(s) failed',
try {
final executable = _executableSteps!.firstWhereOrNull(
(s) => s.expression.isMatch(name),
);
} else {
for (int i = 0; i < configuration.stepMaxRetries + 1; i++) {
result = await executable.step.run(
dependencies.world,
reporter,
configuration.defaultTimeout,
parameters,
if (executable == null) {
final message = 'Step definition not found for text: `$name`';
throw GherkinStepNotDefinedException(message);
}
var parameters = _getStepParameters(
step: name,
multiLineStrings: multiLineStrings,
table: table,
code: executable,
);
await _onBeforeStepRun(
world: dependencies.world,
step: name,
table: table,
multiLineStrings: multiLineStrings,
);
if (skip) {
result = StepResult(
0,
StepExecutionResult.skipped,
resultReason: 'Previous step(s) failed',
);
if (!_isNegativeResult(result.result) ||
configuration.stepMaxRetries == 0) {
break;
} else {
await Future.delayed(configuration.retryDelay);
} else {
for (int i = 0; i < configuration.stepMaxRetries + 1; i++) {
result = await executable.step.run(
dependencies.world,
reporter,
configuration.defaultTimeout,
parameters,
);
if (!_isNegativeResult(result.result) ||
configuration.stepMaxRetries == 0) {
break;
} else {
await Future.delayed(configuration.retryDelay);
}
}
}
} catch (err, st) {
result = ErroredStepResult(
0,
StepExecutionResult.error,
err,
st,
);
}
await _onAfterStepRun(
@ -383,13 +393,9 @@ abstract class GherkinIntegrationTestRunner {
}
@protected
void cleanUpScenarioRun(TestDependencies dependencies) {
_safeInvokeFuture(
() async => dependencies.attachmentManager.dispose(),
);
_safeInvokeFuture(
() async => dependencies.world.dispose(),
);
Future<void> cleanUpScenarioRun(TestDependencies dependencies) async {
dependencies.attachmentManager.dispose();
dependencies.world.dispose();
}
void _registerReporters(Iterable<Reporter>? reporters) {
@ -505,12 +511,6 @@ abstract class GherkinIntegrationTestRunner {
);
}
void _safeInvokeFuture(Future<void> Function() fn) async {
try {
await fn().catchError((_, __) {});
} catch (_) {}
}
bool _evaluateTagFilterExpression(
String? tagExpression,
Iterable<String>? tags,

View File

@ -1,6 +1,6 @@
name: flutter_gherkin
description: A Gherkin / Cucumber parser and test runner for Dart and Flutter
version: 3.0.0-rc.16
version: 3.0.0-rc.17
homepage: https://github.com/jonsamwell/flutter_gherkin
environment: