- 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:
parent
6736af335e
commit
9667db0df6
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
|
|
|
@ -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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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});"
|
|
@ -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({
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -43,7 +43,9 @@ class FlutterDriverAppDriverAdapter
|
|||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> screenshot() {
|
||||
Future<List<int>> screenshot({
|
||||
String? screenshotName,
|
||||
}) {
|
||||
return nativeDriver.screenshot();
|
||||
}
|
||||
|
||||
|
|
|
@ -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}',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
);
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue