mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue.* - https://github.com/flutter/flutter/issues/128381 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
195 lines
7.2 KiB
Dart
195 lines
7.2 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// This is a CLI library; we use prints as part of the interface.
|
|
// ignore_for_file: avoid_print
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_driver/flutter_driver.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'common.dart';
|
|
|
|
/// Flutter Driver test output directory.
|
|
///
|
|
/// Tests should write any output files to this directory. Defaults to the path
|
|
/// set in the FLUTTER_TEST_OUTPUTS_DIR environment variable, or `build` if
|
|
/// unset.
|
|
String testOutputsDirectory =
|
|
Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? 'build';
|
|
|
|
/// The callback type to handle [Response.data] after the test
|
|
/// succeeds.
|
|
typedef ResponseDataCallback = FutureOr<void> Function(Map<String, dynamic>?);
|
|
|
|
/// Writes a json-serializable data to
|
|
/// [testOutputsDirectory]/`testOutputFilename.json`.
|
|
///
|
|
/// This is the default `responseDataCallback` in [integrationDriver].
|
|
Future<void> writeResponseData(
|
|
Map<String, dynamic>? data, {
|
|
String testOutputFilename = 'integration_response_data',
|
|
String? destinationDirectory,
|
|
}) async {
|
|
destinationDirectory ??= testOutputsDirectory;
|
|
await fs.directory(destinationDirectory).create(recursive: true);
|
|
final File file = fs.file(path.join(
|
|
destinationDirectory,
|
|
'$testOutputFilename.json',
|
|
));
|
|
final String resultString = _encodeJson(data, true);
|
|
await file.writeAsString(resultString);
|
|
}
|
|
|
|
/// Adaptor to run an integration test using `flutter drive`.
|
|
///
|
|
/// To an integration test `<test_name>.dart` using `flutter drive`, put a file named
|
|
/// `<test_name>_test.dart` in the app's `test_driver` directory:
|
|
///
|
|
/// ```dart
|
|
/// import 'dart:async';
|
|
///
|
|
/// import 'package:flutter_driver/flutter_driver.dart';
|
|
/// import 'package:integration_test/integration_test_driver_extended.dart';
|
|
///
|
|
/// Future<void> main() async {
|
|
/// final FlutterDriver driver = await FlutterDriver.connect();
|
|
/// await integrationDriver(
|
|
/// driver: driver,
|
|
/// onScreenshot: (String name, List<int> image, [Map<String, Object?>? args]) async {
|
|
/// return true;
|
|
/// },
|
|
/// );
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## Parameters:
|
|
///
|
|
/// `driver` A custom driver. Defaults to `FlutterDriver.connect()`.
|
|
///
|
|
/// `onScreenshot` can be used to process the screenshots taken during the test.
|
|
/// An example could be that this callback compares the byte array against a baseline image,
|
|
/// and it returns `true` if both images are equal.
|
|
///
|
|
/// As a result, returning `false` from `onScreenshot` will make the test fail.
|
|
///
|
|
/// `responseDataCallback` is the handler for processing [Response.data].
|
|
/// The default value is `writeResponseData`.
|
|
///
|
|
/// `writeResponseOnFailure` determines whether the `responseDataCallback`
|
|
/// function will be called to process the [Response.data] when a test fails.
|
|
/// The default value is `false`.
|
|
Future<void> integrationDriver({
|
|
FlutterDriver? driver,
|
|
ScreenshotCallback? onScreenshot,
|
|
ResponseDataCallback? responseDataCallback = writeResponseData,
|
|
bool writeResponseOnFailure = false,
|
|
}) async {
|
|
driver ??= await FlutterDriver.connect();
|
|
// Test states that it's waiting on web driver commands.
|
|
// [DriverTestMessage] is converted to string since json format causes an
|
|
// error if it's used as a message for requestData.
|
|
String jsonResponse = await driver.requestData(DriverTestMessage.pending().toString());
|
|
|
|
final Map<String, bool> onScreenshotResults = <String, bool>{};
|
|
|
|
Response response = Response.fromJson(jsonResponse);
|
|
|
|
// Until `integration_test` returns a [WebDriverCommandType.noop], keep
|
|
// executing WebDriver commands.
|
|
while (response.data != null &&
|
|
response.data!['web_driver_command'] != null &&
|
|
response.data!['web_driver_command'] != '${WebDriverCommandType.noop}') {
|
|
final String? webDriverCommand = response.data!['web_driver_command'] as String?;
|
|
if (webDriverCommand == '${WebDriverCommandType.screenshot}') {
|
|
assert(onScreenshot != null, 'screenshot command requires an onScreenshot callback');
|
|
// Use `driver.screenshot()` method to get a screenshot of the web page.
|
|
final List<int> screenshotImage = await driver.screenshot();
|
|
final String screenshotName = response.data!['screenshot_name']! as String;
|
|
final Map<String, Object?>? args = (response.data!['args'] as Map<String, Object?>?)?.cast<String, Object?>();
|
|
|
|
final bool screenshotSuccess = await onScreenshot!(screenshotName, screenshotImage, args);
|
|
onScreenshotResults[screenshotName] = screenshotSuccess;
|
|
if (screenshotSuccess) {
|
|
jsonResponse = await driver.requestData(DriverTestMessage.complete().toString());
|
|
} else {
|
|
jsonResponse =
|
|
await driver.requestData(DriverTestMessage.error().toString());
|
|
}
|
|
|
|
response = Response.fromJson(jsonResponse);
|
|
} else if (webDriverCommand == '${WebDriverCommandType.ack}') {
|
|
// Previous command completed ask for a new one.
|
|
jsonResponse =
|
|
await driver.requestData(DriverTestMessage.pending().toString());
|
|
|
|
response = Response.fromJson(jsonResponse);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If No-op command is sent, ask for the result of all tests.
|
|
if (response.data != null &&
|
|
response.data!['web_driver_command'] != null &&
|
|
response.data!['web_driver_command'] == '${WebDriverCommandType.noop}') {
|
|
jsonResponse = await driver.requestData(null);
|
|
|
|
response = Response.fromJson(jsonResponse);
|
|
print('result $jsonResponse');
|
|
}
|
|
|
|
if (response.data != null && response.data!['screenshots'] != null && onScreenshot != null) {
|
|
final List<dynamic> screenshots = response.data!['screenshots'] as List<dynamic>;
|
|
final List<String> failures = <String>[];
|
|
for (final dynamic screenshot in screenshots) {
|
|
final Map<String, dynamic> data = screenshot as Map<String, dynamic>;
|
|
final List<dynamic> screenshotBytes = data['bytes'] as List<dynamic>;
|
|
final String screenshotName = data['screenshotName'] as String;
|
|
|
|
bool ok = false;
|
|
try {
|
|
ok = onScreenshotResults[screenshotName] ??
|
|
await onScreenshot(screenshotName, screenshotBytes.cast<int>());
|
|
} catch (exception) {
|
|
throw StateError(
|
|
'Screenshot failure:\n'
|
|
'onScreenshot("$screenshotName", <bytes>) threw an exception: $exception',
|
|
);
|
|
}
|
|
if (!ok) {
|
|
failures.add(screenshotName);
|
|
}
|
|
}
|
|
if (failures.isNotEmpty) {
|
|
throw StateError('The following screenshot tests failed: ${failures.join(', ')}');
|
|
}
|
|
}
|
|
|
|
await driver.close();
|
|
|
|
if (response.allTestsPassed) {
|
|
print('All tests passed.');
|
|
if (responseDataCallback != null) {
|
|
await responseDataCallback(response.data);
|
|
}
|
|
exit(0);
|
|
} else {
|
|
print('Failure Details:\n${response.formattedFailureDetails}');
|
|
if (responseDataCallback != null && writeResponseOnFailure) {
|
|
await responseDataCallback(response.data);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
const JsonEncoder _prettyEncoder = JsonEncoder.withIndent(' ');
|
|
|
|
String _encodeJson(Map<String, dynamic>? jsonObject, bool pretty) {
|
|
return pretty ? _prettyEncoder.convert(jsonObject) : json.encode(jsonObject);
|
|
}
|