From c45b835577b01777e70681c7b751af82784ff3a4 Mon Sep 17 00:00:00 2001 From: Gray Mackall <34871572+gmackall@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:18:19 -0800 Subject: [PATCH] [hcpp] Add tests for transform mutator (#164664) Adds tests covering all transform cases (rotation, flipping, scaling, translation). Fixes https://github.com/flutter/flutter/issues/164213. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Gray Mackall --- .../hcpp/platform_view_transform_main.dart | 193 ++++++++++++++++++ .../platform_view_transform_main_test.dart | 128 ++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 dev/integration_tests/android_engine_test/lib/hcpp/platform_view_transform_main.dart create mode 100644 dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_transform_main_test.dart diff --git a/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_transform_main.dart b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_transform_main.dart new file mode 100644 index 0000000000..8c36964c2b --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_transform_main.dart @@ -0,0 +1,193 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:android_driver_extensions/extension.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +import '../src/allow_list_devices.dart'; + +void main() async { + ensureAndroidDevice(); + enableFlutterDriverExtension( + handler: (String? command) async { + return json.encode({ + 'supported': await HybridAndroidViewController.checkIfSupported(), + }); + }, + commands: [nativeDriverCommands], + ); + + // Run on full screen. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + runApp(const MainApp()); +} + +final class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State with SingleTickerProviderStateMixin { + double angle = 0; + double scale = 1.0; + Offset translation = Offset.zero; + bool flippedX = false; + + void _incrementAngle() { + setState(() { + angle += 0.5; + }); + } + + void _incrementScale() { + setState(() { + scale += 0.1; + }); + } + + void _decrementScale() { + setState(() { + scale -= 0.1; + }); + } + + void _incrementTranslation() { + setState(() { + translation = translation.translate(10, 0); + }); + } + + void _decrementTranslation() { + setState(() { + translation = translation.translate(-10, 0); + }); + } + + void _toggleFlip() { + setState(() { + flippedX = !flippedX; + }); + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final Matrix4 transformMatrix = + Matrix4.identity() + ..translate(translation.dx, translation.dy) + ..scale(scale) + ..rotateZ(angle * math.pi); + + final Widget transformedView = Transform.flip( + flipX: flippedX, + child: Transform( + transform: transformMatrix, + alignment: Alignment.center, + child: const Stack( + alignment: Alignment.center, + children: [ + SizedBox(width: 300, height: 500, child: ColoredBox(color: Colors.green)), + SizedBox( + width: 200, + height: 400, + child: _HybridCompositionAndroidPlatformView( + viewType: 'blue_orange_gradient_platform_view', + ), + ), + ], + ), + ), + ); + + final Widget widget = Column( + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + TextButton( + onPressed: _incrementAngle, + key: const ValueKey('Rotate'), + child: const Text('Rotate'), + ), + TextButton( + onPressed: _incrementScale, + key: const ValueKey('Scale Up'), + child: const Text('Scale Up'), + ), + TextButton( + onPressed: _decrementScale, + key: const ValueKey('Scale Down'), + child: const Text('Scale Down'), + ), + TextButton( + onPressed: _incrementTranslation, + key: const ValueKey('Translate Right'), + child: const Text('Translate Right'), + ), + TextButton( + onPressed: _decrementTranslation, + key: const ValueKey('Translate Left'), + child: const Text('Translate Left'), + ), + TextButton( + onPressed: _toggleFlip, + key: const ValueKey('Flip X'), + child: const Text('Flip X'), + ), + ], + ), + transformedView, + ], + ); + + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: widget)), + ); + } +} + +final class _HybridCompositionAndroidPlatformView extends StatelessWidget { + const _HybridCompositionAndroidPlatformView({required this.viewType}); + + final String viewType; + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.transparent, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initHybridAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } +} diff --git a/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_transform_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_transform_main_test.dart new file mode 100644 index 0000000000..7da8053a92 --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_transform_main_test.dart @@ -0,0 +1,128 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +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'; + +/// For local debugging, a (local) golden-file is required as a baseline: +/// +/// ```sh +/// # Checkout HEAD, i.e. *before* changes you want to test. +/// UPDATE_GOLDENS=1 flutter drive lib/hcpp/platform_view_transform_main.dart +/// +/// # Make your changes. +/// +/// # Run the test against baseline. +/// flutter drive lib/hcpp/platform_view_transform_main.dart +/// ``` +/// +/// For a convenient way to deflake a test, see `tool/deflake.dart`. +void main() async { + const String goldenPrefix = 'hybrid_composition_pp_platform_view'; + + late final FlutterDriver flutterDriver; + late final NativeDriver nativeDriver; + + setUpAll(() async { + if (isLuci) { + await enableSkiaGoldComparator(namePrefix: 'android_engine_test$goldenVariant'); + } + flutterDriver = await FlutterDriver.connect(); + nativeDriver = await AndroidNativeDriver.connect(flutterDriver); + await nativeDriver.configureForScreenshotTesting(); + await flutterDriver.waitUntilFirstFrameRasterized(); + }); + + tearDownAll(() async { + await nativeDriver.close(); + await flutterDriver.close(); + }); + + test('verify that HCPP is supported and enabled', () async { + final Map response = + json.decode(await flutterDriver.requestData('')) as Map; + + expect(response['supported'], true); + }, timeout: Timeout.none); + + test('should rotate in a circle', () async { + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.no_transform.png'), + ); + await flutterDriver.tap(find.byValueKey('Rotate')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.half_pi_radians.png'), + ); + await flutterDriver.tap(find.byValueKey('Rotate')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.one_pi_radians.png'), + ); + await flutterDriver.tap(find.byValueKey('Rotate')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.one_and_a_half_pi_radians.png'), + ); + await flutterDriver.tap(find.byValueKey('Rotate')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.no_transform.png'), + ); + }, timeout: Timeout.none); + + test('should scale down and then back up', () async { + await flutterDriver.tap(find.byValueKey('Scale Down')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.scaled_down.png'), + ); + await flutterDriver.tap(find.byValueKey('Scale Up')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.no_transform.png'), + ); + }, timeout: Timeout.none); + + test('should flip and then flip back', () async { + await flutterDriver.tap(find.byValueKey('Flip X')); + await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.flipped_x.png')); + await flutterDriver.tap(find.byValueKey('Flip X')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.no_transform.png'), + ); + }, timeout: Timeout.none); + + test('should translate and then translate back', () async { + await flutterDriver.tap(find.byValueKey('Translate Left')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.translated_left.png'), + ); + await flutterDriver.tap(find.byValueKey('Translate Right')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.no_transform.png'), + ); + }, timeout: Timeout.none); + + test('should match all applied', () async { + await flutterDriver.tap(find.byValueKey('Flip X')); + await flutterDriver.tap(find.byValueKey('Translate Right')); + await flutterDriver.tap(find.byValueKey('Scale Down')); + await flutterDriver.tap(find.byValueKey('Rotate')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.all_applied.png'), + ); + }, timeout: Timeout.none); +}