mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add a virtual-display (VD) platform view test, and refactor tests a bit. (#161349)
Towards https://github.com/flutter/flutter/issues/161261. Still need to add a HC (Hybrid Composition) variant, but figured I'd do this incrementally to make it easier to review.
This commit is contained in:
parent
99fe90a05e
commit
89b336109f
@ -12,6 +12,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.example.android_engine_test.extensions.NativeDriverSupportPlugin
|
||||
import com.example.android_engine_test.fixtures.BlueOrangeGradientPlatformViewFactory
|
||||
import com.example.android_engine_test.fixtures.BlueOrangeGradientSurfaceViewPlatformViewFactory
|
||||
import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory
|
||||
import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin
|
||||
import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin
|
||||
@ -35,6 +36,7 @@ class MainActivity : FlutterActivity() {
|
||||
.registry
|
||||
.apply {
|
||||
registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory())
|
||||
registerViewFactory("blue_orange_gradient_surface_view_platform_view", BlueOrangeGradientSurfaceViewPlatformViewFactory())
|
||||
registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory())
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
package com.example.android_engine_test.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import io.flutter.Log
|
||||
@ -44,6 +45,10 @@ class NativeDriverSupportPlugin :
|
||||
return
|
||||
}
|
||||
when (call.method) {
|
||||
"sdk_version" -> {
|
||||
val versionMap = mapOf("version" to Build.VERSION.SDK_INT)
|
||||
result.success(versionMap)
|
||||
}
|
||||
"ping" -> {
|
||||
result.success(null)
|
||||
}
|
||||
|
@ -0,0 +1,98 @@
|
||||
// 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.
|
||||
|
||||
@file:Suppress("PackageName")
|
||||
|
||||
package com.example.android_engine_test.fixtures
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.LinearGradient
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Shader
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
class BlueOrangeGradientSurfaceViewPlatformViewFactory : PlatformViewFactory(null) {
|
||||
override fun create(
|
||||
context: Context,
|
||||
viewId: Int,
|
||||
args: Any?
|
||||
): PlatformView = GradientSurfaceViewPlatformView(context)
|
||||
}
|
||||
|
||||
private class GradientSurfaceViewPlatformView(
|
||||
context: Context
|
||||
) : SurfaceView(context),
|
||||
PlatformView,
|
||||
SurfaceHolder.Callback {
|
||||
val paint = Paint()
|
||||
|
||||
init {
|
||||
layoutParams =
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
holder.addCallback(this)
|
||||
}
|
||||
|
||||
override fun getView(): View = this
|
||||
|
||||
override fun dispose() {}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
val canvas = holder.lockCanvas()
|
||||
if (canvas != null) {
|
||||
drawGradient(canvas)
|
||||
holder.unlockCanvasAndPost(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceChanged(
|
||||
holder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) {
|
||||
val canvas = holder.lockCanvas()
|
||||
if (canvas != null) {
|
||||
drawGradient(canvas)
|
||||
holder.unlockCanvasAndPost(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {}
|
||||
|
||||
private fun drawGradient(canvas: Canvas) {
|
||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(
|
||||
w: Int,
|
||||
h: Int,
|
||||
oldw: Int,
|
||||
oldh: Int
|
||||
) {
|
||||
paint.shader =
|
||||
LinearGradient(
|
||||
0f,
|
||||
0f,
|
||||
w.toFloat(),
|
||||
h.toFloat(),
|
||||
intArrayOf(
|
||||
Color.rgb(0x41, 0x69, 0xE1),
|
||||
Color.rgb(0xFF, 0xA5, 0x00)
|
||||
),
|
||||
null,
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ Future<int> _fetchTexture(int width, int height) async {
|
||||
}
|
||||
|
||||
void main() async {
|
||||
ensureAndroidOrIosDevice();
|
||||
ensureAndroidDevice();
|
||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||
|
||||
// Run on full screen.
|
||||
|
@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart';
|
||||
import 'src/allow_list_devices.dart';
|
||||
|
||||
void main() {
|
||||
ensureAndroidOrIosDevice();
|
||||
ensureAndroidDevice();
|
||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||
|
||||
// Run on full screen.
|
||||
|
@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
|
||||
import 'src/allow_list_devices.dart';
|
||||
import '../src/allow_list_devices.dart';
|
||||
|
||||
void main() async {
|
||||
ensureAndroidOrIosDevice();
|
||||
ensureAndroidDevice();
|
||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||
|
||||
// Run on full screen.
|
||||
@ -25,6 +25,11 @@ final class MainApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
// It is assumed:
|
||||
// - The Android SDK version is >= 23 (the test driver checks)
|
||||
// - This view does NOT use a SurfaceView
|
||||
//
|
||||
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||
home: AndroidView(viewType: 'blue_orange_gradient_platform_view'),
|
||||
);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
// 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:android_driver_extensions/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
|
||||
import '../src/allow_list_devices.dart';
|
||||
|
||||
void main() async {
|
||||
ensureAndroidDevice();
|
||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||
|
||||
// Run on full screen.
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
final class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
// It is assumed:
|
||||
// - The Android SDK version is >= 23 (the test driver checks)
|
||||
// - This view DOES use a SurfaceView
|
||||
//
|
||||
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||
home: AndroidView(viewType: 'blue_orange_gradient_surface_view_platform_view'),
|
||||
);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart';
|
||||
import 'src/allow_list_devices.dart';
|
||||
|
||||
void main() async {
|
||||
ensureAndroidOrIosDevice();
|
||||
ensureAndroidDevice();
|
||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||
|
||||
// Run on full screen.
|
||||
|
@ -6,11 +6,11 @@ import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Throws an [UnsupportedError] if the current platform is not Android or iOS.
|
||||
void ensureAndroidOrIosDevice() {
|
||||
if (kIsWeb || (!io.Platform.isAndroid && !io.Platform.isIOS)) {
|
||||
/// Throws an [UnsupportedError] if the current platform is not Android.
|
||||
void ensureAndroidDevice() {
|
||||
if (kIsWeb || !io.Platform.isAndroid) {
|
||||
throw UnsupportedError(
|
||||
'This app should only run on Android or iOS devices. It uses native '
|
||||
'This app should only run on Android devices. It uses native Android '
|
||||
'plugins that are not developed for other platforms, and would need to '
|
||||
'be adapted to run on other platforms. See the README.md for details.',
|
||||
);
|
||||
|
@ -0,0 +1,64 @@
|
||||
// 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:android_driver_extensions/native_driver.dart';
|
||||
import 'package:android_driver_extensions/skia_gold.dart';
|
||||
import 'package:flutter_driver/flutter_driver.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../_luci_skia_gold_prelude.dart';
|
||||
|
||||
void main() async {
|
||||
// To test the golden file generation locally, comment out the following line.
|
||||
// autoUpdateGoldenFiles = true;
|
||||
|
||||
const String goldenPrefix = 'texture_layer_hybrid_composition_platform_view';
|
||||
|
||||
late final FlutterDriver flutterDriver;
|
||||
late final NativeDriver nativeDriver;
|
||||
|
||||
setUpAll(() async {
|
||||
if (isLuci) {
|
||||
await enableSkiaGoldComparator();
|
||||
}
|
||||
flutterDriver = await FlutterDriver.connect();
|
||||
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
|
||||
await nativeDriver.configureForScreenshotTesting();
|
||||
await flutterDriver.waitUntilFirstFrameRasterized();
|
||||
|
||||
// Double check that we are really probably testing using TLHC.
|
||||
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||
if (await nativeDriver.sdkVersion case final int version when version < 23) {
|
||||
fail('Requires SDK >= 23, got $version');
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await nativeDriver.close();
|
||||
await flutterDriver.close();
|
||||
});
|
||||
|
||||
test('should screenshot and match a blue -> orange gradient', () async {
|
||||
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'),
|
||||
);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
test('should rotate landscape and screenshot the gradient', () async {
|
||||
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||
await nativeDriver.rotateToLandscape();
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'),
|
||||
);
|
||||
|
||||
await nativeDriver.rotateResetDefault();
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'),
|
||||
);
|
||||
}, timeout: Timeout.none);
|
||||
}
|
@ -7,12 +7,14 @@ import 'package:android_driver_extensions/skia_gold.dart';
|
||||
import 'package:flutter_driver/flutter_driver.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '_luci_skia_gold_prelude.dart';
|
||||
import '../_luci_skia_gold_prelude.dart';
|
||||
|
||||
void main() async {
|
||||
// To test the golden file generation locally, comment out the following line.
|
||||
// autoUpdateGoldenFiles = true;
|
||||
|
||||
const String goldenPrefix = 'virtual_display_platform_view';
|
||||
|
||||
late final FlutterDriver flutterDriver;
|
||||
late final NativeDriver nativeDriver;
|
||||
|
||||
@ -24,6 +26,12 @@ void main() async {
|
||||
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
|
||||
await nativeDriver.configureForScreenshotTesting();
|
||||
await flutterDriver.waitUntilFirstFrameRasterized();
|
||||
|
||||
// Double check that we are really probably testing using Virtual Display.
|
||||
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||
if (await nativeDriver.sdkVersion case final int version when version < 23) {
|
||||
fail('Requires SDK >= 23, got $version');
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
@ -35,7 +43,7 @@ void main() async {
|
||||
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('platform_view_blue_orange_gradient_portrait.android.png'),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'),
|
||||
);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
@ -44,13 +52,13 @@ void main() async {
|
||||
await nativeDriver.rotateToLandscape();
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('platform_view_blue_orange_gradient_landscape.android.png'),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'),
|
||||
);
|
||||
|
||||
await nativeDriver.rotateResetDefault();
|
||||
await expectLater(
|
||||
nativeDriver.screenshot(),
|
||||
matchesGoldenFile('platform_view_blue_orange_gradient_portrait_post_rotation.android.png'),
|
||||
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'),
|
||||
);
|
||||
}, timeout: Timeout.none);
|
||||
}
|
@ -49,10 +49,14 @@ final class NativeDriverCommandExtension implements CommandExtension {
|
||||
if (result == null) {
|
||||
return const _MethodChannelResult(<String, Object?>{});
|
||||
}
|
||||
if (result is! Map<String, Object?>) {
|
||||
throw ArgumentError.value(result, 'result', 'Expected a Map<String, Object?>');
|
||||
if (result is! Map<Object?, Object?>) {
|
||||
throw ArgumentError.value(
|
||||
result,
|
||||
'result',
|
||||
'Expected a Map<String, Object?>, got ${result.runtimeType}',
|
||||
);
|
||||
}
|
||||
return _MethodChannelResult(result);
|
||||
return _MethodChannelResult(result.cast());
|
||||
}
|
||||
|
||||
// While these could have been implemented in native code, they are already
|
||||
|
@ -71,6 +71,12 @@ final class AndroidNativeDriver implements NativeDriver {
|
||||
await _driver.sendCommand(NativeCommand.tap(finder));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> get sdkVersion async {
|
||||
final Map<String, Object?> result = await _driver.sendCommand(NativeCommand.getSdkVersion);
|
||||
return result['version']! as int;
|
||||
}
|
||||
|
||||
/// Waits for 2 seconds before completing.
|
||||
///
|
||||
/// There is no perfect way, outside of polling, to know when the device is
|
||||
|
@ -27,6 +27,9 @@ final class NativeCommand extends Command {
|
||||
/// Pings the device to ensure it is responsive.
|
||||
static const NativeCommand ping = NativeCommand('ping');
|
||||
|
||||
/// Gets the SDK version code.
|
||||
static const NativeCommand getSdkVersion = NativeCommand('sdk_version');
|
||||
|
||||
/// The method to call on the plugin.
|
||||
final String method;
|
||||
|
||||
|
@ -45,6 +45,9 @@ abstract interface class NativeDriver {
|
||||
/// ```
|
||||
Future<Duration> ping();
|
||||
|
||||
/// Returns the SDK version.
|
||||
Future<int> get sdkVersion;
|
||||
|
||||
/// Take a screenshot using a platform-specific mechanism.
|
||||
///
|
||||
/// The image is returned as an opaque handle that can be used to retrieve
|
||||
|
Loading…
Reference in New Issue
Block a user