// 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. import 'dart:async'; import 'dart:convert'; /// A callback to use with [integrationDriver]. /// /// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)`, /// a PNG byte buffer representing the screenshot, and an optional `Map` of arguments. /// /// The callback returns `true` if the test passes or `false` otherwise. /// /// You can use this callback to store the bytes locally in a file or upload them to a service /// that compares the image against a gold or baseline version. /// /// The optional `Map` of arguments can be passed from the /// `binding.takeScreenshot(<name>, <args>)` callsite in the integration test, /// and then the arguments can be used in the `onScreenshot` handler that is defined by /// the Flutter driver. This `Map` should only contain values that can be serialized /// to JSON. /// /// Since the function is executed on the host driving the test, you can access any environment /// variable from it. typedef ScreenshotCallback = Future<bool> Function(String name, List<int> image, [Map<String, Object?>? args]); /// Classes shared between `integration_test.dart` and `flutter drive` based /// adoptor (ex: `integration_test_driver.dart`). /// An object sent from integration_test back to the Flutter Driver in response to /// `request_data` command. class Response { /// Constructor to use for positive response. Response.allTestsPassed({this.data}) : _allTestsPassed = true, _failureDetails = null; /// Constructor for failure response. Response.someTestsFailed(this._failureDetails, {this.data}) : _allTestsPassed = false; /// Constructor for failure response. Response.toolException({String? ex}) : _allTestsPassed = false, _failureDetails = <Failure>[Failure('ToolException', ex)]; /// Constructor for web driver commands response. Response.webDriverCommand({this.data}) : _allTestsPassed = false, _failureDetails = null; final List<Failure>? _failureDetails; final bool _allTestsPassed; /// The extra information to be added along side the test result. Map<String, dynamic>? data; /// Whether the test ran successfully or not. bool get allTestsPassed => _allTestsPassed; /// If the result are failures get the formatted details. String get formattedFailureDetails => _allTestsPassed ? '' : formatFailures(_failureDetails!); /// Failure details as a list. List<Failure>? get failureDetails => _failureDetails; /// Serializes this message to a JSON map. String toJson() => json.encode(<String, dynamic>{ 'result': allTestsPassed.toString(), 'failureDetails': _failureDetailsAsString(), if (data != null) 'data': data, }); /// Deserializes the result from JSON. static Response fromJson(String source) { final Map<String, dynamic> responseJson = json.decode(source) as Map<String, dynamic>; if ((responseJson['result'] as String?) == 'true') { return Response.allTestsPassed(data: responseJson['data'] as Map<String, dynamic>?); } else { return Response.someTestsFailed( _failureDetailsFromJson(responseJson['failureDetails'] as List<dynamic>), data: responseJson['data'] as Map<String, dynamic>?, ); } } /// Method for formatting the test failures' details. String formatFailures(List<Failure> failureDetails) { if (failureDetails.isEmpty) { return ''; } final StringBuffer sb = StringBuffer(); int failureCount = 1; for (final Failure failure in failureDetails) { sb.writeln('Failure in method: ${failure.methodName}'); sb.writeln(failure.details); sb.writeln('end of failure $failureCount\n\n'); failureCount++; } return sb.toString(); } /// Create a list of Strings from [_failureDetails]. List<String> _failureDetailsAsString() { final List<String> list = <String>[]; if (_failureDetails == null || _failureDetails!.isEmpty) { return list; } for (final Failure failure in _failureDetails!) { list.add(failure.toJson()); } return list; } /// Creates a [Failure] list using a json response. static List<Failure> _failureDetailsFromJson(List<dynamic> list) { return list.map((dynamic s) { return Failure.fromJsonString(s as String); }).toList(); } } /// Representing a failure includes the method name and the failure details. class Failure { /// Constructor requiring all fields during initialization. Failure(this.methodName, this.details); /// The name of the test method which failed. final String methodName; /// The details of the failure such as stack trace. final String? details; /// Serializes the object to JSON. String toJson() { return json.encode(<String, String?>{ 'methodName': methodName, 'details': details, }); } @override String toString() => toJson(); /// Decode a JSON string to create a Failure object. static Failure fromJsonString(String jsonString) { final Map<String, dynamic> failure = json.decode(jsonString) as Map<String, dynamic>; return Failure(failure['methodName'] as String, failure['details'] as String?); } } /// Message used to communicate between app side tests and driver tests. /// /// Not all `integration_tests` use this message. They are only used when app /// side tests are sending [WebDriverCommand]s to the driver side. /// /// These messages are used for the handshake since they carry information on /// the driver side test such as: status pending or tests failed. class DriverTestMessage { /// When tests are failed on the driver side. DriverTestMessage.error() : _isSuccess = false, _isPending = false; /// When driver side is waiting on [WebDriverCommand]s to be sent from the /// app side. DriverTestMessage.pending() : _isSuccess = false, _isPending = true; /// When driver side successfully completed executing the [WebDriverCommand]. DriverTestMessage.complete() : _isSuccess = true, _isPending = false; final bool _isSuccess; final bool _isPending; // /// Status of this message. // /// // /// The status will be use to notify `integration_test` of driver side's // /// state. // String get status => _status; /// Has the command completed successfully by the driver. bool get isSuccess => _isSuccess; /// Is the driver waiting for a command. bool get isPending => _isPending; /// Depending on the values of [isPending] and [isSuccess], returns a string /// to represent the [DriverTestMessage]. /// /// Used as an alternative method to converting the object to json since /// [RequestData] is only accepting string as `message`. @override String toString() { if (isPending) { return 'pending'; } else if (isSuccess) { return 'complete'; } else { return 'error'; } } /// Return a DriverTestMessage depending on `status`. static DriverTestMessage fromString(String status) { switch (status) { case 'error': return DriverTestMessage.error(); case 'pending': return DriverTestMessage.pending(); case 'complete': return DriverTestMessage.complete(); default: throw StateError('This type of status does not exist: $status'); } } } /// Types of different WebDriver commands that can be used in web integration /// tests. /// /// These commands are either commands that WebDriver can execute or used /// for the communication between `integration_test` and the driver test. enum WebDriverCommandType { /// Acknowledgement for the previously sent message. ack, /// No further WebDriver commands is requested by the app-side tests. noop, /// Asking WebDriver to take a screenshot of the Web page. screenshot, } /// Command for WebDriver to execute. /// /// Only works on Web when tests are run via `flutter driver` command. /// /// See: https://www.w3.org/TR/webdriver/ class WebDriverCommand { /// Constructor for [WebDriverCommandType.noop] command. WebDriverCommand.noop() : type = WebDriverCommandType.noop, values = <String, dynamic>{}; /// Constructor for [WebDriverCommandType.noop] screenshot. WebDriverCommand.screenshot(String screenshotName, [Map<String, Object?>? args]) : type = WebDriverCommandType.screenshot, values = <String, dynamic>{ 'screenshot_name': screenshotName, if (args != null) 'args': args, }; /// Type of the [WebDriverCommand]. /// /// Currently the only command that triggers a WebDriver API is `screenshot`. /// /// There are also `ack` and `noop` commands defined to manage the handshake /// during the communication. final WebDriverCommandType type; /// Used for adding extra values to the commands such as file name for /// `screenshot`. final Map<String, dynamic> values; /// Util method for converting [WebDriverCommandType] to a map entry. /// /// Used for converting messages to json format. static Map<String, dynamic> typeToMap(WebDriverCommandType type) => <String, dynamic>{ 'web_driver_command': '$type', }; } /// Template methods each class that responses the driver side inputs must /// implement. /// /// Depending on the platform the communication between `integration_tests` and /// the `driver_tests` can be different. /// /// For the web implementation [WebCallbackManager]. /// For the io implementation [IOCallbackManager]. abstract class CallbackManager { /// The callback function to response the driver side input. Future<Map<String, dynamic>> callback( Map<String, String> params, IntegrationTestResults testRunner); /// Takes a screenshot of the application. /// Returns the data that is sent back to the host. Future<Map<String, dynamic>> takeScreenshot(String screenshot, [Map<String, Object?>? args]); /// Android only. Converts the Flutter surface to an image view. Future<void> convertFlutterSurfaceToImage(); /// Cleanup and completers or locks used during the communication. void cleanup(); } /// Interface that surfaces test results of integration tests. /// /// Implemented by [IntegrationTestWidgetsFlutterBinding]s. /// /// Any class which needs to access the test results but do not want to create /// a cyclic dependency [IntegrationTestWidgetsFlutterBinding]s can use this /// interface. Example [CallbackManager]. abstract class IntegrationTestResults { /// Stores failure details. /// /// Failed test method's names used as key. List<Failure> get failureMethodsDetails; /// The extra data for the reported result. Map<String, dynamic>? get reportData; /// Whether all the test methods completed successfully. /// /// Completes when the tests have finished. The boolean value will be true if /// all tests have passed, and false otherwise. Completer<bool> get allTestsPassed; }