Android e2e screenshot (#84472)
This commit is contained in:
parent
2b7b4bdcab
commit
0504fac7e2
@ -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
|
||||
|
@ -0,0 +1,262 @@
|
||||
// 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"
|
||||
|
@ -1,18 +0,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 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"));
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
androidx.activity:activity:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.annotation:annotation:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.arch.core:core-common:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.arch.core:core-runtime:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.collection:collection:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.core:core:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.fragment:fragment:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-common-java8:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-livedata-core:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-livedata:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.savedstate:savedstate:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.versionedparcelable:versionedparcelable:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
androidx.webkit:webkit:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath
|
||||
com.android.tools.analytics-library:protos:27.1.3=lintClassPath
|
||||
com.android.tools.analytics-library:shared:27.1.3=lintClassPath
|
||||
com.android.tools.analytics-library:tracker:27.1.3=lintClassPath
|
||||
com.android.tools.build:aapt2-proto:4.1.0-alpha01-6193524=lintClassPath
|
||||
com.android.tools.build:aapt2:4.1.3-6503028=_internal_aapt2_binary
|
||||
com.android.tools.build:apksig:4.1.3=lintClassPath
|
||||
com.android.tools.build:apkzlib:4.1.3=lintClassPath
|
||||
com.android.tools.build:builder-model:4.1.3=lintClassPath
|
||||
com.android.tools.build:builder-test-api:4.1.3=lintClassPath
|
||||
com.android.tools.build:builder:4.1.3=lintClassPath
|
||||
com.android.tools.build:gradle-api:4.1.3=lintClassPath
|
||||
com.android.tools.build:manifest-merger:27.1.3=lintClassPath
|
||||
com.android.tools.ddms:ddmlib:27.1.3=lintClassPath
|
||||
com.android.tools.external.com-intellij:intellij-core:27.1.3=lintClassPath
|
||||
com.android.tools.external.com-intellij:kotlin-compiler:27.1.3=lintClassPath
|
||||
com.android.tools.external.org-jetbrains:uast:27.1.3=lintClassPath
|
||||
com.android.tools.layoutlib:layoutlib-api:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint-api:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint-checks:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint-gradle-api:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint-gradle:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint-model:27.1.3=lintClassPath
|
||||
com.android.tools.lint:lint:27.1.3=lintClassPath
|
||||
com.android.tools:annotations:27.1.3=lintClassPath
|
||||
com.android.tools:common:27.1.3=lintClassPath
|
||||
com.android.tools:dvlib:27.1.3=lintClassPath
|
||||
com.android.tools:repository:27.1.3=lintClassPath
|
||||
com.android.tools:sdk-common:27.1.3=lintClassPath
|
||||
com.android.tools:sdklib:27.1.3=lintClassPath
|
||||
com.android:signflinger:4.1.3=lintClassPath
|
||||
com.android:zipflinger:4.1.3=lintClassPath
|
||||
com.google.code.findbugs:jsr305:3.0.2=lintClassPath
|
||||
com.google.code.gson:gson:2.8.5=lintClassPath
|
||||
com.google.errorprone:error_prone_annotations:2.3.2=lintClassPath
|
||||
com.google.guava:failureaccess:1.0.1=lintClassPath
|
||||
com.google.guava:guava:28.1-jre=lintClassPath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=lintClassPath
|
||||
com.google.j2objc:j2objc-annotations:1.3=lintClassPath
|
||||
com.google.jimfs:jimfs:1.1=lintClassPath
|
||||
com.google.protobuf:protobuf-java:3.10.0=lintClassPath
|
||||
com.googlecode.json-simple:json-simple:1.1=lintClassPath
|
||||
com.squareup:javawriter:2.5.0=lintClassPath
|
||||
com.sun.activation:javax.activation:1.2.0=lintClassPath
|
||||
com.sun.istack:istack-commons-runtime:3.0.7=lintClassPath
|
||||
com.sun.xml.fastinfoset:FastInfoset:1.2.15=lintClassPath
|
||||
commons-codec:commons-codec:1.10=lintClassPath
|
||||
commons-logging:commons-logging:1.2=lintClassPath
|
||||
it.unimi.dsi:fastutil:7.2.0=lintClassPath
|
||||
javax.activation:javax.activation-api:1.2.0=lintClassPath
|
||||
javax.inject:javax.inject:1=lintClassPath
|
||||
javax.xml.bind:jaxb-api:2.3.1=lintClassPath
|
||||
net.sf.jopt-simple:jopt-simple:4.9=lintClassPath
|
||||
net.sf.kxml:kxml2:2.3.0=lintClassPath
|
||||
org.apache.commons:commons-compress:1.12=lintClassPath
|
||||
org.apache.httpcomponents:httpclient:4.5.6=lintClassPath
|
||||
org.apache.httpcomponents:httpcore:4.4.10=lintClassPath
|
||||
org.apache.httpcomponents:httpmime:4.5.6=lintClassPath
|
||||
org.bouncycastle:bcpkix-jdk15on:1.56=lintClassPath
|
||||
org.bouncycastle:bcprov-jdk15on:1.56=lintClassPath
|
||||
org.checkerframework:checker-qual:2.8.1=lintClassPath
|
||||
org.codehaus.groovy:groovy-all:2.4.15=lintClassPath
|
||||
org.codehaus.mojo:animal-sniffer-annotations:1.18=lintClassPath
|
||||
org.glassfish.jaxb:jaxb-runtime:2.3.1=lintClassPath
|
||||
org.glassfish.jaxb:txw2:2.3.1=lintClassPath
|
||||
org.jetbrains.kotlin:kotlin-reflect:1.3.72=lintClassPath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72=lintClassPath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72=lintClassPath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72=lintClassPath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.3.72=lintClassPath
|
||||
org.jetbrains.trove4j:trove4j:20160824=lintClassPath
|
||||
org.jetbrains:annotations:13.0=lintClassPath
|
||||
org.jvnet.staxex:stax-ex:1.8=lintClassPath
|
||||
org.ow2.asm:asm-analysis:7.0=lintClassPath
|
||||
org.ow2.asm:asm-commons:7.0=lintClassPath
|
||||
org.ow2.asm:asm-tree:7.0=lintClassPath
|
||||
org.ow2.asm:asm-util:7.0=lintClassPath
|
||||
org.ow2.asm:asm:7.0=lintClassPath
|
||||
empty=androidApis,androidTestUtil,compile,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAnnotationProcessorClasspath,debugUnitTestAnnotationProcessorClasspath,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileUnitTestAnnotationProcessorClasspath,releaseAnnotationProcessorClasspath,releaseUnitTestAnnotationProcessorClasspath,testCompile
|
@ -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.
|
||||
await tester.pumpAndSettle();
|
||||
// On Android, this is required prior to taking the screenshot.
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
|
||||
// TODO(nturgut): https://github.com/flutter/flutter/issues/51890
|
||||
// Add screenshot capability for mobile platforms.
|
||||
// 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);
|
||||
|
||||
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> takeScreenshot(String screenshot) {
|
||||
throw UnimplementedError(
|
||||
'Screenshots are not implemented on this platform');
|
||||
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<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.
|
||||
///
|
||||
/// Called by test methods. Implementation differs for each platform.
|
||||
Future<void> takeScreenshot(String screenshotName) async {
|
||||
await callbackManager.takeScreenshot(screenshotName);
|
||||
/// 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.
|
||||
///
|
||||
/// 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) {
|
||||
|
9
packages/integration_test/lib/src/channel.dart
Normal file
9
packages/integration_test/lib/src/channel.dart
Normal file
@ -0,0 +1,9 @@
|
||||
// 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');
|
Loading…
x
Reference in New Issue
Block a user