Unverified Commit 0504fac7 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Android e2e screenshot (#84472)

parent 2b7b4bdc
......@@ -48,6 +48,26 @@ class MatchesGoldenFile extends AsyncMatcher {
@override
Future<String?> matchAsync(dynamic item) async {
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
Uint8List? buffer;
if (item is Future<List<int>>) {
buffer = Uint8List.fromList(await item);
} else if (item is List<int>) {
buffer = Uint8List.fromList(item);
}
if (buffer != null) {
if (autoUpdateGoldenFiles) {
await goldenFileComparator.update(testNameUri, buffer);
return null;
}
try {
final bool success = await goldenFileComparator.compare(buffer, testNameUri);
return success ? null : 'does not match';
} on TestFailure catch (ex) {
return ex.message;
}
}
Future<ui.Image?> imageFuture;
if (item is Future<ui.Image?>) {
imageFuture = item;
......@@ -62,11 +82,9 @@ class MatchesGoldenFile extends AsyncMatcher {
}
imageFuture = captureImage(elements.single);
} else {
throw 'must provide a Finder, Image, or Future<Image>';
throw 'must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>';
}
final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
return binding.runAsync<String?>(() async {
final ui.Image? image = await imageFuture;
......
......@@ -352,7 +352,6 @@ void main() {
});
group('matches', () {
testWidgets('if comparator succeeds', (WidgetTester tester) async {
await tester.pumpWidget(boilerplate(const Text('hello')));
final Finder finder = find.byType(Text);
......@@ -361,6 +360,20 @@ void main() {
expect(comparator.imageBytes, hasLength(greaterThan(0)));
expect(comparator.golden, Uri.parse('foo.png'));
});
testWidgets('list of integers', (WidgetTester tester) async {
await expectLater(<int>[1, 2], matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, equals(<int>[1, 2]));
expect(comparator.golden, Uri.parse('foo.png'));
});
testWidgets('future list of integers', (WidgetTester tester) async {
await expectLater(Future<List<int>>.value(<int>[1, 2]), matchesGoldenFile('foo.png'));
expect(comparator.invocation, _ComparatorInvocation.compare);
expect(comparator.imageBytes, equals(<int>[1, 2]));
expect(comparator.golden, Uri.parse('foo.png'));
});
});
group('does not match', () {
......
......@@ -95,6 +95,77 @@ flutter drive \
-d web-server
```
### Screenshots
You can use `integration_test` to take screenshots of the UI rendered on the mobile device or
Web browser at a specific time during the test.
This feature is currently supported on Android, and Web.
#### Android
**integration_test/screenshot_test.dart**
```dart
void main() {
final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('screenshot', (WidgetTester tester) async {
// Build the app.
app.main();
// This is required prior to taking the screenshot.
await binding.convertFlutterSurfaceToImage();
// Trigger a frame.
await tester.pumpAndSettle();
await binding.takeScreenshot('screenshot-1');
});
}
```
You can use a driver script to pull in the screenshot from the device.
This way, you can store the images locally on your computer.
**test_driver/integration_test.dart**
```dart
import 'dart:io';
import 'package:integration_test/integration_test_driver_extended.dart';
Future<void> main() async {
await integrationDriver(
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
final File image = File('$screenshotName.png');
image.writeAsBytesSync(screenshotBytes);
// Return false if the screenshot is invalid.
return true;
},
);
}
```
#### Web
**integration_test/screenshot_test.dart**
```dart
void main() {
final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('screenshot', (WidgetTester tester) async {
// Build the app.
app.main();
// Trigger a frame.
await tester.pumpAndSettle();
await binding.takeScreenshot('screenshot-1');
});
}
```
## Android Device Testing
Create an instrumentation test file in your application's
......
......@@ -39,6 +39,8 @@ android {
}
dependencies {
// TODO(egarciad): These dependencies should not be added to release builds.
// https://github.com/flutter/flutter/issues/56591
api 'junit:junit:4.12'
// https://developer.android.com/jetpack/androidx/releases/test/#1.2.0
......
// 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.
package dev.flutter.plugins.integration_test;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.view.Choreographer;
import android.view.PixelCopy;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.StringBuilder;
/**
* FlutterDeviceScreenshot is a utility class that allows to capture a screenshot
* that includes both Android views and the Flutter UI.
*
* To take screenshots, the rendering surface must be changed to {@code FlutterImageView},
* since surfaces like {@code FlutterSurfaceView} and {@code FlutterTextureView} are opaque
* when the view hierarchy is rendered to a bitmap.
*
* It's also necessary to ask the framework to schedule a frame, and then add a listener
* that waits for that frame to be presented by the Android framework.
*/
@TargetApi(19)
class FlutterDeviceScreenshot {
/**
* Finds the {@code FlutterView} added to the {@code activity} view hierarchy.
*
* <p> This assumes that there's only one {@code FlutterView} per activity, which
* is always the case.
*
* @param activity typically, {code FlutterActivity}.
* @return the Flutter view.
*/
@Nullable
private static FlutterView getFlutterView(@NonNull Activity activity) {
return (FlutterView)activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID);
}
/**
* Whether the app is run with instrumentation.
*
* @return true if the app is running with instrumentation.
*/
static boolean hasInstrumentation() {
// TODO(egarciad): InstrumentationRegistry requires the uiautomator dependency.
// However, Flutter adds test dependencies to release builds.
// As a result, disable screenshots with instrumentation until the issue is fixed.
// https://github.com/flutter/flutter/issues/56591
return false;
}
/**
* Captures a screenshot using ui automation.
*
* @return byte array containing the screenshot.
*/
static byte[] captureWithUiAutomation() throws IOException {
return new byte[0];
}
// Whether the flutter surface is already converted to an image.
private static boolean flutterSurfaceConvertedToImage = false;
/**
* Converts the Flutter surface to an image view.
* This allows to render the view hierarchy to a bitmap since
* {@code FlutterSurfaceView} and {@code FlutterTextureView} cannot be rendered to a bitmap.
*
* @param activity typically {@code FlutterActivity}.
*/
static void convertFlutterSurfaceToImage(@NonNull Activity activity) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView != null && !flutterSurfaceConvertedToImage) {
flutterView.convertToImageView();
flutterSurfaceConvertedToImage = true;
}
}
/**
* Restores the original Flutter surface.
* The new surface will either be {@code FlutterSurfaceView} or {@code FlutterTextureView}.
*
* @param activity typically {@code FlutterActivity}.
* @param onDone callback called once the surface has been restored.
*/
static void revertFlutterImage(@NonNull Activity activity) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView != null && flutterSurfaceConvertedToImage) {
flutterView.revertImageView(() -> {
flutterSurfaceConvertedToImage = false;
});
}
}
// Handlers use to capture a view.
private static Handler backgroundHandler;
private static Handler mainHandler;
/**
* Captures a screenshot by drawing the view to a Canvas.
*
* <p> {@code convertFlutterSurfaceToImage} must be called prior to capturing the view,
* otherwise the result is an error.
*
* @param activity this is {@link FlutterActivity}.
* @param methodChannel the method channel to call into Dart.
* @param result the result for the method channel that will contain the byte array.
*/
static void captureView(
@NonNull Activity activity, @NonNull MethodChannel methodChannel, @NonNull Result result) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView == null) {
result.error("Could not copy the pixels", "FlutterView is null", null);
return;
}
if (!flutterSurfaceConvertedToImage) {
result.error("Could not copy the pixels", "Flutter surface must be converted to image first", null);
return;
}
// Ask the framework to schedule a new frame.
methodChannel.invokeMethod("scheduleFrame", null);
if (backgroundHandler == null) {
final HandlerThread screenshotBackgroundThread = new HandlerThread("screenshot");
screenshotBackgroundThread.start();
backgroundHandler = new Handler(screenshotBackgroundThread.getLooper());
}
if (mainHandler == null) {
mainHandler = new Handler(Looper.getMainLooper());
}
takeScreenshot(backgroundHandler, mainHandler, flutterView, result);
}
/**
* Waits for the next Android frame.
*
* @param r a callback.
*/
private static void waitForAndroidFrame(Runnable r) {
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
r.run();
}
});
}
/**
* Waits until a Flutter frame is rendered by the Android OS.
*
* @param backgroundHandler the handler associated to a background thread.
* @param mainHandler the handler associated to the platform thread.
* @param view the flutter view.
* @param result the result that contains the byte array.
*/
private static void takeScreenshot(
@NonNull Handler backgroundHandler,
@NonNull Handler mainHandler,
@NonNull FlutterView view,
@NonNull Result result) {
final boolean acquired = view.acquireLatestImageViewFrame();
// The next frame may already have already been comitted.
// The next frame is guaranteed to have the Flutter image.
waitForAndroidFrame(
() -> {
waitForAndroidFrame(
() -> {
if (acquired) {
FlutterDeviceScreenshot.convertViewToBitmap(view, result, backgroundHandler);
} else {
takeScreenshot(backgroundHandler, mainHandler, view, result);
}
});
});
}
/**
* Renders {@code FlutterView} to a Bitmap.
*
* If successful, The byte array is provided in the result.
*
* @param flutterView the Flutter view.
* @param result the result that contains the byte array.
* @param backgroundHandler a background handler to avoid blocking the platform thread.
*/
private static void convertViewToBitmap(
@NonNull FlutterView flutterView, @NonNull Result result, @NonNull Handler backgroundHandler) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
final Bitmap bitmap =
Bitmap.createBitmap(
flutterView.getWidth(), flutterView.getHeight(), Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(bitmap);
flutterView.draw(canvas);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, /*quality=*/ 100, output);
result.success(output.toByteArray());
return;
}
final Bitmap bitmap =
Bitmap.createBitmap(
flutterView.getWidth(), flutterView.getHeight(), Bitmap.Config.ARGB_8888);
final int[] flutterViewLocation = new int[2];
flutterView.getLocationInWindow(flutterViewLocation);
final int flutterViewLeft = flutterViewLocation[0];
final int flutterViewTop = flutterViewLocation[1];
final Rect flutterViewRect =
new Rect(
flutterViewLeft,
flutterViewTop,
flutterViewLeft + flutterView.getWidth(),
flutterViewTop + flutterView.getHeight());
final Activity flutterActivity = (Activity) flutterView.getContext();
PixelCopy.request(
flutterActivity.getWindow(),
flutterViewRect,
bitmap,
(int copyResult) -> {
final Handler mainHandler = new Handler(Looper.getMainLooper());
if (copyResult == PixelCopy.SUCCESS) {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, /*quality=*/ 100, output);
mainHandler.post(
() -> {
result.success(output.toByteArray());
});
} else {
mainHandler.post(
() -> {
result.error("Could not copy the pixels", "result was " + copyResult, null);
});
}
},
backgroundHandler);
}
}
......@@ -4,26 +4,30 @@
package dev.flutter.plugins.integration_test;
import android.app.Activity;
import android.content.Context;
import com.google.common.util.concurrent.SettableFuture;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Future;
/** IntegrationTestPlugin */
public class IntegrationTestPlugin implements MethodCallHandler, FlutterPlugin {
private MethodChannel methodChannel;
public class IntegrationTestPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware {
private static final String CHANNEL = "plugins.flutter.io/integration_test";
private static final SettableFuture<Map<String, String>> testResultsSettable =
SettableFuture.create();
public static final Future<Map<String, String>> testResults = testResultsSettable;
private static final String CHANNEL = "plugins.flutter.io/integration_test";
private MethodChannel methodChannel;
private Activity flutterActivity;
public static final Future<Map<String, String>> testResults = testResultsSettable;
/** Plugin registration. */
@SuppressWarnings("deprecation")
......@@ -48,14 +52,70 @@ public class IntegrationTestPlugin implements MethodCallHandler, FlutterPlugin {
methodChannel = null;
}
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
flutterActivity = binding.getActivity();
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
flutterActivity = binding.getActivity();
}
@Override
public void onDetachedFromActivity() {
flutterActivity = null;
}
@Override
public void onDetachedFromActivityForConfigChanges() {
flutterActivity = null;
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("allTestsFinished")) {
Map<String, String> results = call.argument("results");
testResultsSettable.set(results);
result.success(null);
} else {
result.notImplemented();
switch (call.method) {
case "allTestsFinished":
final Map<String, String> results = call.argument("results");
testResultsSettable.set(results);
result.success(null);
return;
case "convertFlutterSurfaceToImage":
if (flutterActivity == null) {
result.error("Could not convert to image", "Activity not initialized", null);
return;
}
FlutterDeviceScreenshot.convertFlutterSurfaceToImage(flutterActivity);
result.success(null);
return;
case "revertFlutterImage":
if (flutterActivity == null) {
result.error("Could not revert Flutter image", "Activity not initialized", null);
return;
}
FlutterDeviceScreenshot.revertFlutterImage(flutterActivity);
result.success(null);
return;
case "captureScreenshot":
if (FlutterDeviceScreenshot.hasInstrumentation()) {
byte[] image;
try {
image = FlutterDeviceScreenshot.captureWithUiAutomation();
} catch (IOException exception) {
result.error("Could not capture screenshot", "UiAutomation failed", exception);
return;
}
result.success(image);
return;
}
if (flutterActivity == null) {
result.error("Could not capture screenshot", "Activity not initialized", null);
return;
}
FlutterDeviceScreenshot.captureView(flutterActivity, methodChannel, result);
return;
default:
result.notImplemented();
}
}
}
......@@ -14,12 +14,6 @@ found in the LICENSE file. -->
android:name="io.flutter.app.FlutterApplication"
android:label="integration_test_example"
android:icon="@mipmap/ic_launcher">
<activity android:name=".EmbedderV1Activity"
android:theme="@android:style/Theme.Black.NoTitleBar"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
......
// 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.
package com.example.integration_test_example;
import android.os.Bundle;
import dev.flutter.plugins.integration_test.IntegrationTestPlugin;
import io.flutter.app.FlutterActivity;
public class EmbedderV1Activity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
IntegrationTestPlugin.registerWith(
registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin"));
}
}
......@@ -10,6 +10,7 @@
// tree, read text, and verify that the values of widget properties are correct.
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
......@@ -17,17 +18,27 @@ import 'package:integration_test/integration_test.dart';
import 'package:integration_test_example/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
testWidgets('verify text', (WidgetTester tester) async {
// Build our app and trigger a frame.
// Build our app.
app.main();
// Trigger a frame.
// On Android, this is required prior to taking the screenshot.
await binding.convertFlutterSurfaceToImage();
// Pump a frame before taking the screenshot.
await tester.pumpAndSettle();
final List<int> firstPng = await binding.takeScreenshot('first');
expect(firstPng.isNotEmpty, isTrue);
// Pump another frame before taking the screenshot.
await tester.pumpAndSettle();
final List<int> secondPng = await binding.takeScreenshot('second');
expect(secondPng.isNotEmpty, isTrue);
// TODO(nturgut): https://github.com/flutter/flutter/issues/51890
// Add screenshot capability for mobile platforms.
expect(listEquals(firstPng, secondPng), isTrue);
// Verify that platform version is retrieved.
expect(
......
......@@ -4,8 +4,7 @@
// This is a Flutter widget test can take a screenshot.
//
// NOTE: Screenshots are only supported on Web for now. For Web, this needs to
// be executed with the `test_driver/integration_test_extended_driver.dart`.
// For Web, this needs to be executed with the `test_driver/integration_test_extended_driver.dart`.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
......
......@@ -10,6 +10,7 @@ Future<void> main() async {
await integrationDriver(
driver: driver,
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
// Return false if the screenshot is invalid.
return true;
},
);
......
......@@ -2,7 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'common.dart';
import 'src/channel.dart';
/// The dart:io implementation of [CallbackManager].
///
......@@ -54,9 +60,53 @@ class IOCallbackManager implements CallbackManager {
// comes up in the future. For example: `WebCallbackManager.cleanup`.
}
// Whether the Flutter surface uses an Image.
bool _usesFlutterImage = false;
@override
Future<void> convertFlutterSurfaceToImage() async {
assert(!_usesFlutterImage, 'Surface already converted to an image');
await integrationTestChannel.invokeMethod<void>(
'convertFlutterSurfaceToImage',
null,
);
_usesFlutterImage = true;
addTearDown(() async {
assert(_usesFlutterImage, 'Surface is not an image');
await integrationTestChannel.invokeMethod<void>(
'revertFlutterImage',
null,
);
_usesFlutterImage = false;
});
}
@override
Future<void> takeScreenshot(String screenshot) {
throw UnimplementedError(
'Screenshots are not implemented on this platform');
Future<Map<String, dynamic>> takeScreenshot(String screenshot) async {
if (!_usesFlutterImage) {
throw StateError('Call convertFlutterSurfaceToImage() before taking a screenshot');
}
integrationTestChannel.setMethodCallHandler(_onMethodChannelCall);
final List<int>? rawBytes = await integrationTestChannel.invokeMethod<List<int>>(
'captureScreenshot',
null,
);
if (rawBytes == null) {
throw StateError('Expected a list of bytes, but instead captureScreenshot returned null');
}
return <String, dynamic>{
'screenshotName': screenshot,
'bytes': rawBytes,
};
}
Future<dynamic> _onMethodChannelCall(MethodCall call) async {
switch (call.method) {
case 'scheduleFrame':
window.scheduleFrame();
break;
}
return null;
}
}
......@@ -44,8 +44,15 @@ class WebCallbackManager implements CallbackManager {
///
/// See: https://www.w3.org/TR/webdriver/#screen-capture.
@override
Future<void> takeScreenshot(String screenshotName) async {
Future<Map<String, dynamic>> takeScreenshot(String screenshotName) async {
await _sendWebDriverCommand(WebDriverCommand.screenshot(screenshotName));
// Flutter Web doesn't provide the bytes.
return const <String, dynamic>{'bytes': <int>[]};
}
@override
Future<void> convertFlutterSurfaceToImage() async {
// Noop on Web.
}
Future<void> _sendWebDriverCommand(WebDriverCommand command) async {
......
......@@ -5,6 +5,20 @@
import 'dart:async';
import 'dart:convert';
/// A callback to use with [integrationDriver].
///
/// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)` and
/// a PNG byte buffer.
///
/// 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.
///
/// 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);
/// Classes shared between `integration_test.dart` and `flutter drive` based
/// adoptor (ex: `integration_test_driver.dart`).
......@@ -270,8 +284,12 @@ abstract class CallbackManager {
Future<Map<String, dynamic>> callback(
Map<String, String> params, IntegrationTestResults testRunner);
/// Request to take a screenshot of the application.
Future<void> takeScreenshot(String screenshot);
/// Takes a screenshot of the application.
/// Returns the data that is sent back to the host.
Future<Map<String, dynamic>> takeScreenshot(String screenshot);
/// Android only. Converts the Flutter surface to an image view.
Future<void> convertFlutterSurfaceToImage();
/// Cleanup and completers or locks used during the communication.
void cleanup();
......
......@@ -17,6 +17,7 @@ import 'package:vm_service/vm_service_io.dart' as vm_io;
import '_callback_io.dart' if (dart.library.html) '_callback_web.dart' as driver_actions;
import '_extension_io.dart' if (dart.library.html) '_extension_web.dart';
import 'common.dart';
import 'src/channel.dart';
const String _success = 'success';
......@@ -51,7 +52,7 @@ class IntegrationTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding
}
try {
await _channel.invokeMethod<void>(
await integrationTestChannel.invokeMethod<void>(
'allTestsFinished',
<String, dynamic>{
'results': results.map<String, dynamic>((String name, Object result) {
......@@ -144,9 +145,6 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
return WidgetsBinding.instance!;
}
static const MethodChannel _channel =
MethodChannel('plugins.flutter.io/integration_test');
/// Test results that will be populated after the tests have completed.
///
/// Keys are the test descriptions, and values are either [_success] or
......@@ -167,11 +165,29 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
/// side.
final CallbackManager callbackManager = driver_actions.callbackManager;
/// Taking a screenshot.
/// Takes a screenshot.
///
/// On Android, you need to call `convertFlutterSurfaceToImage()`, and
/// pump a frame before taking a screenshot.
Future<List<int>> takeScreenshot(String screenshotName) async {
reportData ??= <String, dynamic>{};
reportData!['screenshots'] ??= <dynamic>[];
final Map<String, dynamic> data = await callbackManager.takeScreenshot(screenshotName);
assert(data.containsKey('bytes'));
(reportData!['screenshots']! as List<dynamic>).add(data);
return data['bytes']! as List<int>;
}
/// Android only. Converts the Flutter surface to an image view.
/// Be aware that if you are conducting a perf test, you may not want to call
/// this method since the this is an expensive operation that affects the
/// rendering of a Flutter app.
///
/// Called by test methods. Implementation differs for each platform.
Future<void> takeScreenshot(String screenshotName) async {
await callbackManager.takeScreenshot(screenshotName);
/// Once the screenshot is taken, call `revertFlutterImage()` to restore
/// the original Flutter surface.
Future<void> convertFlutterSurfaceToImage() async {
await callbackManager.convertFlutterSurfaceToImage();
}
/// The callback function to response the driver side input.
......
......@@ -45,13 +45,6 @@ Future<void> writeResponseData(
/// Adaptor to run an integration test using `flutter drive`.
///
/// `timeout` controls the longest time waited before the test ends.
/// It is not necessarily the execution time for the test app: the test may
/// finish sooner than the `timeout`.
///
/// `responseDataCallback` is the handler for processing [Response.data].
/// The default value is `writeResponseData`.
///
/// 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:
///
......@@ -63,6 +56,21 @@ Future<void> writeResponseData(
/// Future<void> main() async => integrationDriver();
///
/// ```
///
/// ## Parameters:
///
/// `timeout` controls the longest time waited before the test ends.
/// It is not necessarily the execution time for the test app: the test may
/// finish sooner than the `timeout`.
///
/// `responseDataCallback` is the handler for processing [Response.data].
/// The default value is `writeResponseData`.
///
/// `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.
Future<void> integrationDriver({
Duration timeout = const Duration(minutes: 20),
ResponseDataCallback? responseDataCallback = writeResponseData,
......@@ -70,6 +78,7 @@ Future<void> integrationDriver({
final FlutterDriver driver = await FlutterDriver.connect();
final String jsonResult = await driver.requestData(null, timeout: timeout);
final Response response = Response.fromJson(jsonResult);
await driver.close();
if (response.allTestsPassed) {
......
......@@ -9,11 +9,36 @@ import 'package:flutter_driver/flutter_driver.dart';
import 'common.dart';
/// A callback to use with [integrationDriver].
typedef ScreenshotCallback = Future<bool> Function(String name, List<int> image);
/// Example Integration Test which can also run WebDriver command depending on
/// the requests coming from the test methods.
/// 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:integration_test/integration_test_driver_extended.dart';
///
/// Future<void> main() async {
/// final FlutterDriver driver = await FlutterDriver.connect();
/// await integrationDriver(
/// driver: driver,
/// onScreenshot: (String screenshotName, List<int> screenshotBytes) 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.
Future<void> integrationDriver(
{FlutterDriver? driver, ScreenshotCallback? onScreenshot}) async {
driver ??= await FlutterDriver.connect();
......@@ -66,6 +91,30 @@ Future<void> integrationDriver(
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 = 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) {
......
// 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 'package:flutter/services.dart';
/// The method channel used to report the result of the tests to the platform.
/// On Android, this is relevant when running instrumented tests.
const MethodChannel integrationTestChannel = MethodChannel('plugins.flutter.io/integration_test');
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment