- Fix #235 - fix issue taking a screenshot on an Android device

- Resolved #170: Added example code to ensure json report is save to disk even when the test run fails. Also added script to generate a HTML report from a JSON report
This commit is contained in:
Jon 2022-06-27 12:39:31 +10:00
parent 6736af335e
commit 9667db0df6
19 changed files with 303 additions and 1330 deletions

View File

@ -1,3 +1,7 @@
## [3.0.0-rc.13] - 27/06/2022
- Fix #235 - fix issue taking a screenshot on an Android device
- Resolved #170: Added example code to ensure json report is save to disk even when the test run fails. Also added script to generate a HTML report from a JSON report
## [3.0.0-rc.12] - 24/06/2022
- Fix #222 - escape single quotation marks in data tables

View File

@ -7,6 +7,7 @@ Feature: Creating todos
Then I expect the todo list
| Todo |
| Buy spinach |
When I take a screenshot called 'Johnson'
Scenario: User can create multiple new todo items
Given I fill the "todo" field with "Buy carrots"

View File

@ -22,6 +22,7 @@ FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration(
],
hooks: [
ResetAppHook(),
// AttachScreenshotAfterStepHook(),
],
reporters: [
StdoutReporter(MessageLevel.error)
@ -33,6 +34,9 @@ FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration(
TestRunSummaryReporter()
..setWriteLineFn(print)
..setWriteFn(print),
JsonReporter(
writeReport: (_, __) => Future<void>.value(),
),
],
createWorld: (config) => Future.value(CustomWorld()),
);

View File

@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
class AttachScreenshotAfterStepHook extends Hook {
@override
Future<void> onAfterStep(
World world,
String step,
StepResult stepResult,
) async {
try {
final screenshotData = await takeScreenshot(world);
world.attach(screenshotData, 'image/png', step);
} catch (e, st) {
world.attach('Failed to take screenshot\n$e\n$st', 'text/plain', step);
}
return super.onAfterStep(world, step, stepResult);
}
}
Future<String> takeScreenshot(World world) async {
final bytes = await (world as FlutterWorld).appDriver.screenshot();
return base64Encode(bytes);
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -27,146 +27,6 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
}
void testFeature0() {
runFeature(
name: 'Swiping:',
tags: <String>['@tag'],
run: () {
runScenario(
name: 'User can swipe cards left and right',
path: '.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag', '@debug'],
steps: [
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name:
'Given I swipe right by 250 pixels on the "scrollable cards"`',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Then I expect the text "Page 2" to be present',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name:
'Given I swipe left by 250 pixels on the "scrollable cards"`',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Then I expect the text "Page 1" to be present',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag'],
),
);
},
);
}
void testFeature1() {
runFeature(
name: 'Checking data:',
tags: <String>['@tag'],
run: () {
runScenario(
name: 'User can have data',
path: '.\\integration_test\\features\\check.feature',
tags: <String>['@tag', '@tag1'],
steps: [
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Given I have item with data',
multiLineStrings: <String>[
"""{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": [
"GML",
"XML"
]
},
"GlossSee": "markup"
}
}
}
}
}"""
],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
tags: <String>['@tag'],
),
);
},
);
}
void testFeature2() {
runFeature(
name: 'Creating todos:',
tags: <String>['@tag'],
@ -212,6 +72,18 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'When I take a screenshot called \'Johnson\'',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Creating todos',
@ -370,6 +242,146 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
},
);
}
void testFeature1() {
runFeature(
name: 'Swiping:',
tags: <String>['@tag'],
run: () {
runScenario(
name: 'User can swipe cards left and right',
path: '.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag', '@debug'],
steps: [
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name:
'Given I swipe right by 250 pixels on the "scrollable cards"`',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Then I expect the text "Page 2" to be present',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name:
'Given I swipe left by 250 pixels on the "scrollable cards"`',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Then I expect the text "Page 1" to be present',
multiLineStrings: <String>[],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Swiping',
path: r'.\\integration_test\\features\\swiping.feature',
tags: <String>['@tag'],
),
);
},
);
}
void testFeature2() {
runFeature(
name: 'Checking data:',
tags: <String>['@tag'],
run: () {
runScenario(
name: 'User can have data',
path: '.\\integration_test\\features\\check.feature',
tags: <String>['@tag', '@tag1'],
steps: [
(
TestDependencies dependencies,
bool skip,
) async {
return await runStep(
name: 'Given I have item with data',
multiLineStrings: <String>[
"""{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": [
"GML",
"XML"
]
},
"GlossSee": "markup"
}
}
}
}
}"""
],
table: null,
dependencies: dependencies,
skip: skip,
);
},
],
onBefore: () async => onBeforeRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
tags: <String>['@tag'],
),
onAfter: () async => onAfterRunFeature(
name: 'Checking data',
path: r'.\\integration_test\\features\\check.feature',
tags: <String>['@tag'],
),
);
},
);
}
}
void executeTestSuite({

View File

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

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:integration_test/common.dart';
import 'package:integration_test/integration_test_driver.dart'
as integration_test_driver;
@ -22,7 +24,49 @@ Future<void> main() {
integration_test_driver.testOutputsDirectory =
'integration_test/gherkin/reports';
return integration_test_driver.integrationDriver(
timeout: const Duration(minutes: 90),
);
return integrationDriver();
}
// Rre-implement this rather than using `integration_test_driver.integrationDriver()`
// so that failed test runs will have reports saved to disk rather than just exiting
Future<void> integrationDriver({
Duration timeout = const Duration(minutes: 60),
}) async {
final FlutterDriver driver = await FlutterDriver.connect();
final String jsonResult = await driver.requestData(null, timeout: timeout);
final Response response = Response.fromJson(jsonResult);
await driver.close();
final reports = json.decode(response.data!['gherkin_reports'].toString())
as List<dynamic>;
await writeGherkinReports(reports);
if (response.allTestsPassed) {
exit(0);
} else {
print('Failure Details:\n${response.formattedFailureDetails}');
exit(1);
}
}
Future<void> writeGherkinReports(List<dynamic> reports) async {
final filenamePrefix =
DateTime.now().toIso8601String().split('.').first.replaceAll(':', '-');
for (var i = 0; i < reports.length; i += 1) {
final reportData = reports.elementAt(i) as List<dynamic>;
await fs
.directory(integration_test_driver.testOutputsDirectory)
.create(recursive: true);
File file = File(
'${integration_test_driver.testOutputsDirectory}/'
'$filenamePrefix'
'v${i + 1}.json',
);
await file.writeAsString(json.encode(reportData));
}
}

View File

@ -30,6 +30,7 @@ export 'src/flutter/steps/wait_until_key_exists_step.dart';
export 'src/flutter/steps/when_tap_the_back_button_step.dart';
export 'src/flutter/steps/wait_until_type_exists_step.dart';
export 'src/flutter/steps/wait_until_key_exists_step.dart';
export 'src/flutter/steps/take_a_screenshot_step.dart';
// Hooks
export 'src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart';

View File

@ -57,7 +57,9 @@ abstract class AppDriverAdapter<TNativeAdapter, TFinderType, TWidgetBaseType> {
ExpectedWidgetResultType expectResultType = ExpectedWidgetResultType.first,
]);
Future<List<int>> screenshot();
Future<List<int>> screenshot({
String? screenshotName,
});
Future<bool> isPresent(
TFinderType finder, {

View File

@ -43,7 +43,9 @@ class FlutterDriverAppDriverAdapter
}
@override
Future<List<int>> screenshot() {
Future<List<int>> screenshot({
String? screenshotName,
}) {
return nativeDriver.screenshot();
}

View File

@ -2,8 +2,10 @@ import 'dart:io' if (dart.library.html) 'dart:html';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'dart:ui' as ui show ImageByteFormat;
import 'app_driver_adapter.dart';
@ -66,15 +68,40 @@ class WidgetTesterAppDriverAdapter
}
}
Future<List<int>> screenshotOnAndroid({String? screenshotName}) {
RenderObject? renderObject = binding.renderViewElement?.renderObject;
if (renderObject != null) {
while (!renderObject!.isRepaintBoundary) {
renderObject = renderObject.parent as RenderObject?;
assert(renderObject != null);
}
final layer = renderObject.debugLayer as OffsetLayer;
return layer
.toImage(renderObject.paintBounds)
.then((value) => value.toByteData(format: ui.ImageByteFormat.png))
.then((value) => value!.buffer.asUint8List());
}
throw Exception('Unable to take screenshot on Android device');
}
@override
Future<List<int>> screenshot() async {
Future<List<int>> screenshot({String? screenshotName}) async {
if (!kIsWeb && Platform.isAndroid) {
await binding.convertFlutterSurfaceToImage();
await binding.pump();
return await screenshotOnAndroid(screenshotName: screenshotName);
// 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 binding.takeScreenshot(
'screenshot_${DateTime.now().millisecondsSinceEpoch}',
screenshotName ?? 'screenshot_${DateTime.now().millisecondsSinceEpoch}',
);
}

View File

@ -6,6 +6,8 @@ import 'package:flutter_gherkin/src/flutter/steps/when_long_press_widget_step.da
import 'package:flutter_test/flutter_test.dart';
import 'package:gherkin/gherkin.dart';
import '../steps/take_a_screenshot_step.dart';
class FlutterTestConfiguration extends TestConfiguration {
static final Iterable<CustomParameter<dynamic>> _wellKnownParameters = [
ExistenceParameter(),
@ -35,6 +37,7 @@ class FlutterTestConfiguration extends TestConfiguration {
textExistsWithinStep(),
waitUntilKeyExistsStep(),
waitUntilTypeExistsStep(),
takeScreenshot(),
];
/// Enable semantics in a test by creating a [SemanticsHandle].

View File

@ -0,0 +1,17 @@
import 'dart:convert';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
StepDefinitionGeneric takeScreenshot() {
return given1<String, FlutterWorld>(
'I take a screenshot called {String}',
(name, context) async {
final bytes = await context.world.appDriver.screenshot(
screenshotName: name,
);
context.world.attach(base64Encode(bytes), 'image/png');
},
);
}

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.12
version: 3.0.0-rc.13
homepage: https://github.com/jonsamwell/flutter_gherkin
environment:

View File

@ -18,7 +18,7 @@ void main() {
test('common steps definition added', () {
final config = FlutterDriverTestConfiguration();
expect(config.stepDefinitions, isNotNull);
expect(config.stepDefinitions!.length, 23);
expect(config.stepDefinitions!.length, 24);
expect(config.customStepParameterDefinitions, isNotNull);
expect(config.customStepParameterDefinitions!.length, 2);
});
@ -30,7 +30,7 @@ void main() {
);
expect(config.stepDefinitions, isNotNull);
expect(config.stepDefinitions!.length, 24);
expect(config.stepDefinitions!.length, 25);
expect(
config.stepDefinitions!.elementAt(0), (x) => x is MockStepDefinition);
expect(config.customStepParameterDefinitions, isNotNull);