Port touch-based tests from embedder integration test (flutter/engine#38234)

* Port touch-based tests from embedder integration test

* Remove RegisterTouchScreen and related variables

* Update embedded child view size
This commit is contained in:
Erik
2022-12-16 15:54:07 -05:00
committed by GitHub
parent b23b1bf484
commit 5262e76f26
7 changed files with 229 additions and 171 deletions

View File

@@ -86,12 +86,8 @@ constexpr auto kTestUiStackRef = ChildRef{kTestUiStack};
constexpr fuchsia_test_utils::Color kParentBackgroundColor = {0x00, 0x00, 0xFF,
0xFF}; // Blue
constexpr fuchsia_test_utils::Color kParentTappedColor = {0x00, 0x00, 0x00,
0xFF}; // Black
constexpr fuchsia_test_utils::Color kChildBackgroundColor = {0xFF, 0x00, 0xFF,
0xFF}; // Pink
constexpr fuchsia_test_utils::Color kChildTappedColor = {0xFF, 0xFF, 0x00,
0xFF}; // Yellow
// TODO(fxb/64201): Remove forced opacity colors when Flatland is enabled.
constexpr fuchsia_test_utils::Color kOverlayBackgroundColor1 = {
@@ -160,60 +156,18 @@ class FlutterEmbedderTest : public ::loop_fixture::RealLoop,
callback = nullptr,
zx::duration timeout = kTestTimeout);
// Simulates a tap at location (x, y).
void InjectTap(int32_t x, int32_t y);
// Injects an input event, and posts a task to retry after
// `kTapRetryInterval`.
//
// We post the retry task because the first input event we send to Flutter may
// be lost. The reason the first event may be lost is that there is a race
// condition as the scene owner starts up.
//
// More specifically: in order for our app
// to receive the injected input, two things must be true before we inject
// touch input:
// * The Scenic root view must have been installed, and
// * The Input Pipeline must have received a viewport to inject touch into.
//
// The problem we have is that the `is_rendering` signal that we monitor only
// guarantees us the view is ready. If the viewport is not ready in Input
// Pipeline at that time, it will drop the touch event.
//
// TODO(fxbug.dev/96986): Improve synchronization and remove retry logic.
void TryInject(int32_t x, int32_t y);
private:
fuchsia::ui::scenic::Scenic* scenic() { return scenic_.get(); }
void SetUpRealmBase();
// Registers a fake touch screen device with an injection coordinate space
// spanning [-1000, 1000] on both axes.
void RegisterTouchScreen();
fuchsia::ui::scenic::ScenicPtr scenic_;
fuchsia::ui::test::input::RegistryPtr input_registry_;
fuchsia::ui::test::input::TouchScreenPtr fake_touchscreen_;
fuchsia::ui::test::scene::ControllerPtr scene_provider_;
fuchsia::ui::observation::geometry::ViewTreeWatcherPtr view_tree_watcher_;
// Wrapped in optional since the view is not created until the middle of SetUp
component_testing::RealmBuilder realm_builder_;
std::unique_ptr<component_testing::RealmRoot> realm_;
// The typical latency on devices we've tested is ~60 msec. The retry interval
// is chosen to be a) Long enough that it's unlikely that we send a new tap
// while a previous tap is still being
// processed. That is, it should be far more likely that a new tap is sent
// because the first tap was lost, than because the system is just running
// slowly.
// b) Short enough that we don't slow down tryjobs.
//
// The first property is important to avoid skewing the latency metrics that
// we collect. For an explanation of why a tap might be lost, see the
// documentation for TryInject().
static constexpr auto kTapRetryInterval = zx::sec(1);
};
void FlutterEmbedderTest::SetUpRealmBase() {
@@ -374,9 +328,6 @@ void FlutterEmbedderTest::LaunchParentViewInRealm(
}
realm_ = std::make_unique<RealmRoot>(realm_builder_.Build());
// Register fake touch screen device.
RegisterTouchScreen();
// Instruct Test UI Stack to present parent-view's View.
std::optional<zx_koid_t> view_ref_koid;
scene_provider_ = realm_->Connect<fuchsia::ui::test::scene::Controller>();
@@ -443,36 +394,6 @@ bool FlutterEmbedderTest::TakeScreenshotUntil(
timeout);
}
void FlutterEmbedderTest::RegisterTouchScreen() {
FML_LOG(INFO) << "Registering fake touch screen";
input_registry_ = realm_->Connect<fuchsia::ui::test::input::Registry>();
input_registry_.set_error_handler(
[](auto) { FML_LOG(ERROR) << "Error from input helper"; });
bool touchscreen_registered = false;
fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request;
request.set_device(fake_touchscreen_.NewRequest());
input_registry_->RegisterTouchScreen(
std::move(request),
[&touchscreen_registered]() { touchscreen_registered = true; });
RunLoopUntil([&touchscreen_registered] { return touchscreen_registered; });
FML_LOG(INFO) << "Touchscreen registered";
}
void FlutterEmbedderTest::InjectTap(int32_t x, int32_t y) {
fuchsia::ui::test::input::TouchScreenSimulateTapRequest tap_request;
tap_request.mutable_tap_location()->x = x;
tap_request.mutable_tap_location()->y = y;
fake_touchscreen_->SimulateTap(std::move(tap_request), [x, y]() {
FML_LOG(INFO) << "Tap injected at (" << x << ", " << y << ")";
});
}
void FlutterEmbedderTest::TryInject(int32_t x, int32_t y) {
InjectTap(x, y);
async::PostDelayedTask(
dispatcher(), [this, x, y] { TryInject(x, y); }, kTapRetryInterval);
}
TEST_F(FlutterEmbedderTest, Embedding) {
LaunchParentViewInRealm();
@@ -489,53 +410,6 @@ TEST_F(FlutterEmbedderTest, Embedding) {
}));
}
TEST_F(FlutterEmbedderTest, HittestEmbedding) {
LaunchParentViewInRealm();
// Take screenshot until we see the child-view's embedded color.
ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor));
// Simulate a tap at the center of the child view.
TryInject(/* x = */ 0, /* y = */ 0);
// Take screenshot until we see the child-view's tapped color.
ASSERT_TRUE(TakeScreenshotUntil(
kChildTappedColor,
[](std::map<fuchsia_test_utils::Color, size_t> histogram) {
// Expect parent and child background colors, with parent color > child
// color.
EXPECT_GT(histogram[kParentBackgroundColor], 0u);
EXPECT_EQ(histogram[kChildBackgroundColor], 0u);
EXPECT_GT(histogram[kChildTappedColor], 0u);
EXPECT_GT(histogram[kParentBackgroundColor],
histogram[kChildTappedColor]);
}));
}
TEST_F(FlutterEmbedderTest, HittestDisabledEmbedding) {
LaunchParentViewInRealm({"--no-hitTestable"});
// Take screenshots until we see the child-view's embedded color.
ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor));
// Simulate a tap at the center of the child view.
TryInject(/* x = */ 0, /* y = */ 0);
// The parent-view should change color.
ASSERT_TRUE(TakeScreenshotUntil(
kParentTappedColor,
[](std::map<fuchsia_test_utils::Color, size_t> histogram) {
// Expect parent and child background colors, with parent color > child
// color.
EXPECT_EQ(histogram[kParentBackgroundColor], 0u);
EXPECT_GT(histogram[kParentTappedColor], 0u);
EXPECT_GT(histogram[kChildBackgroundColor], 0u);
EXPECT_EQ(histogram[kChildTappedColor], 0u);
EXPECT_GT(histogram[kParentTappedColor],
histogram[kChildBackgroundColor]);
}));
}
TEST_F(FlutterEmbedderTest, EmbeddingWithOverlay) {
LaunchParentViewInRealm({"--showOverlay"});
@@ -555,33 +429,4 @@ TEST_F(FlutterEmbedderTest, EmbeddingWithOverlay) {
}));
}
TEST_F(FlutterEmbedderTest, HittestEmbeddingWithOverlay) {
LaunchParentViewInRealm({"--showOverlay"});
// Take screenshot until we see the child-view's embedded color.
ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor));
// The bottom-left corner of the overlay is at the center of the screen,
// which is at (0, 0) in the injection coordinate space. Inject a pointer
// event just outside the overlay's bounds, and ensure that it goes to the
// embedded view.
TryInject(/* x = */ -1, /* y = */ 1);
// Take screenshot until we see the child-view's tapped color.
ASSERT_TRUE(TakeScreenshotUntil(
kChildTappedColor,
[](std::map<fuchsia_test_utils::Color, size_t> histogram) {
// Expect parent, overlay and child background colors.
// With parent color > child color and overlay color > child color.
const size_t overlay_pixel_count = OverlayPixelCount(histogram);
EXPECT_GT(histogram[kParentBackgroundColor], 0u);
EXPECT_GT(overlay_pixel_count, 0u);
EXPECT_EQ(histogram[kChildBackgroundColor], 0u);
EXPECT_GT(histogram[kChildTappedColor], 0u);
EXPECT_GT(histogram[kParentBackgroundColor],
histogram[kChildTappedColor]);
EXPECT_GT(overlay_pixel_count, histogram[kChildTappedColor]);
}));
}
} // namespace flutter_embedder_test

View File

@@ -13,6 +13,8 @@ dart_library("lib") {
sources = [ "embedding-flutter-view.dart" ]
deps = [
"//flutter/shell/platform/fuchsia/dart:args",
"//flutter/shell/platform/fuchsia/dart:vector_math",
"//flutter/tools/fuchsia/dart:fuchsia_services",
"//flutter/tools/fuchsia/dart:zircon",
"//flutter/tools/fuchsia/fidl:fuchsia.ui.app",

View File

@@ -7,15 +7,37 @@ import 'dart:typed_data';
import 'dart:io';
import 'dart:ui';
import 'package:args/args.dart';
import 'package:fidl_fuchsia_ui_app/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:fidl_fuchsia_ui_test_input/fidl_async.dart' as test_touch;
import 'package:fuchsia_services/services.dart';
import 'package:vector_math/vector_math_64.dart' as vector_math_64;
import 'package:zircon/zircon.dart';
final _argsCsvFilePath = '/config/data/args.csv';
void main(List<String> args) {
print('Launching embedding-flutter-view');
TestApp app = TestApp(ChildView.gfx(_launchGfxChildView()));
args = args + _GetArgsFromConfigFile();
final parser = ArgParser()
..addFlag('showOverlay', defaultsTo: false)
..addFlag('hitTestable', defaultsTo: true)
..addFlag('focusable', defaultsTo: true);
final arguments = parser.parse(args);
for (final option in arguments.options) {
print('embedding-flutter-view args: $option: ${arguments[option]}');
}
TestApp app = TestApp(
ChildView.gfx(_launchGfxChildView()),
showOverlay: arguments['showOverlay'],
hitTestable: arguments['hitTestable'],
focusable: arguments['focusable'],
);
app.run();
}
@@ -24,14 +46,22 @@ class TestApp {
static const _blue = Color.fromARGB(255, 0, 0, 255);
final ChildView childView;
final bool showOverlay;
final bool hitTestable;
final bool focusable;
final _responseListener = test_touch.TouchInputListenerProxy();
Color _backgroundColor = _blue;
TestApp(this.childView) {}
TestApp(
this.childView,
{this.showOverlay = false,
this.hitTestable = true,
this.focusable = true}) {
}
void run() {
childView.create((ByteData reply) {
childView.create(hitTestable, focusable, (ByteData reply) {
// Set up window callbacks.
window.onPointerDataPacket = (PointerDataPacket packet) {
this.pointerDataPacket(packet);
@@ -67,13 +97,52 @@ class TestApp {
final sceneBuilder = SceneBuilder()
..pushClipRect(physicalBounds)
..addPicture(Offset.zero, picture);
// Child view should take up half the screen
final childPhysicalSize = window.physicalSize * 0.5;
final childPhysicalSize = window.physicalSize * 0.25;
// Alignment.center
final windowCenter = size.center(Offset.zero);
final windowPhysicalCenter = window.physicalSize.center(Offset.zero);
final childPhysicalOffset = windowPhysicalCenter - childPhysicalSize.center(Offset.zero);
sceneBuilder
..pushTransform(
vector_math_64.Matrix4.translationValues(childPhysicalOffset.dx,
childPhysicalOffset.dy,
0.0).storage)
..addPlatformView(childView.viewId,
width: childPhysicalSize.width,
height: size.height)
height: childPhysicalSize.height)
..pop();
if (showOverlay) {
final containerSize = size * 0.5;
// Alignment.center
final containerOffset = windowCenter - containerSize.center(Offset.zero);
final overlaySize = containerSize * 0.5;
// Alignment.topRight
final overlayOffset = Offset(
containerOffset.dx + containerSize.width - overlaySize.width,
containerOffset.dy);
final overlayPhysicalSize = overlaySize * pixelRatio;
final overlayPhysicalOffset = overlayOffset * pixelRatio;
final overlayPhysicalBounds = overlayPhysicalOffset & overlayPhysicalSize;
final recorder = PictureRecorder();
final overlayCullRect = Offset.zero & overlayPhysicalSize; // in canvas physical coordinates
final canvas = Canvas(recorder, overlayCullRect);
canvas.scale(pixelRatio);
final paint = Paint()..color = Color.fromARGB(255, 0, 255, 0);
canvas.drawRect(Offset.zero & overlaySize, paint);
final overlayPicture = recorder.endRecording();
sceneBuilder
..pushClipRect(overlayPhysicalBounds) // in window physical coordinates
..addPicture(overlayPhysicalOffset, overlayPicture)
..pop();
}
sceneBuilder.pop();
window.render(sceneBuilder.build());
}
@@ -124,15 +193,18 @@ class ChildView {
assert(viewId != null);
}
void create(PlatformMessageResponseCallback callback) {
void create(
bool hitTestable,
bool focusable,
PlatformMessageResponseCallback callback) {
// Construct the dart:ui platform message to create the view, and when the
// return callback is invoked, build the scene. At that point, it is safe
// to embed the child view in the scene.
final viewOcclusionHint = Rect.zero;
final Map<String, dynamic> args = <String, dynamic>{
'viewId': viewId,
'hitTestable': true,
'focusable': true,
'hitTestable': hitTestable,
'focusable': focusable,
'viewOcclusionHintLTRB': <double>[
viewOcclusionHint.left,
viewOcclusionHint.top,
@@ -173,3 +245,14 @@ ViewHolderToken _launchGfxChildView() {
return viewHolderToken;
}
List<String> _GetArgsFromConfigFile() {
List<String> args;
final f = File(_argsCsvFilePath);
if (!f.existsSync()) {
return List.empty();
}
final fileContentCsv = f.readAsStringSync();
args = fileContentCsv.split('\n');
return args;
}

View File

@@ -112,6 +112,7 @@ namespace {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::ConfigValue;
using component_testing::DirectoryContents;
using component_testing::LocalComponent;
using component_testing::LocalComponentHandles;
using component_testing::ParentRef;
@@ -324,6 +325,57 @@ class FlutterTapTest : public FlutterTapTestBase {
};
class FlutterEmbedTapTest : public FlutterTapTestBase {
protected:
void SetUp() override {
PortableUITest::SetUp(false);
// Post a "just in case" quit task, if the test hangs.
async::PostDelayedTask(
dispatcher(),
[] {
FML_LOG(FATAL)
<< "\n\n>> Test did not complete in time, terminating. <<\n\n";
},
kTimeout);
}
void LaunchClientWithEmbeddedView() {
BuildRealm();
// Get the display dimensions.
FML_LOG(INFO) << "Waiting for scenic display info";
scenic_ = realm_root()->template Connect<fuchsia::ui::scenic::Scenic>();
scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) {
display_width_ = display_info.width_in_px;
display_height_ = display_info.height_in_px;
FML_LOG(INFO) << "Got display_width = " << display_width_
<< " and display_height = " << display_height_;
});
RunLoopUntil(
[this] { return display_width_ != 0 && display_height_ != 0; });
// Register input injection device.
FML_LOG(INFO) << "Registering input injection device";
RegisterTouchScreen();
PortableUITest::LaunchClientWithEmbeddedView();
}
// Helper method to add a component argument
// This will be written into an args.csv file that can be parsed and read
// by embedding-flutter-view.dart
//
// Note: You must call this method before LaunchClientWithEmbeddedView()
// Realm Builder will not allow you to create a new directory / file in a
// realm that's already been built
void AddComponentArgument(std::string component_arg) {
auto config_directory_contents = DirectoryContents();
config_directory_contents.AddFile("args.csv", component_arg);
realm_builder()->RouteReadOnlyDirectory(
"config-data", {kEmbeddingFlutterViewRef},
std::move(config_directory_contents));
}
private:
void ExtendRealm() override {
FML_LOG(INFO) << "Extending realm";
@@ -392,6 +444,9 @@ TEST_P(FlutterTapTest, FlutterTap) {
/*expected_y=*/static_cast<float>(display_height() / 4.0f),
/*component_name=*/"touch-input-view");
});
// There should be 1 injected tap
ASSERT_EQ(touch_injection_request_count(), 1);
}
INSTANTIATE_TEST_SUITE_P(FlutterEmbedTapTestParameterized,
@@ -405,19 +460,19 @@ TEST_P(FlutterEmbedTapTest, FlutterEmbedTap) {
FML_LOG(INFO) << "Client launched";
{
// Embedded child view takes up the left side of the screen
// Embedded child view takes up the center of the screen
// Expect a response from the child view if we inject a tap there
InjectTap(-500, -500);
InjectTap(0, 0);
RunLoopUntil([this] {
return LastEventReceivedMatches(
/*expected_x=*/static_cast<float>(display_width() / 4.0f),
/*expected_y=*/static_cast<float>(display_height() / 4.0f),
/*expected_x=*/static_cast<float>(display_width() / 8.0f),
/*expected_y=*/static_cast<float>(display_height() / 8.0f),
/*component_name=*/"touch-input-view");
});
}
{
// Parent view takes up the right side of the screen
// Parent view takes up the rest of the screen
// Validate that parent can still receive taps
InjectTap(500, 500);
RunLoopUntil([this] {
@@ -432,5 +487,63 @@ TEST_P(FlutterEmbedTapTest, FlutterEmbedTap) {
ASSERT_EQ(touch_injection_request_count(), 2);
}
TEST_P(FlutterEmbedTapTest, FlutterEmbedHittestDisabled) {
FML_LOG(INFO) << "Initializing scene";
AddComponentArgument("--no-hitTestable");
LaunchClientWithEmbeddedView();
FML_LOG(INFO) << "Client launched";
// Embedded child view takes up the center of the screen
// hitTestable is turned off for the embedded child view
// Expect the parent (embedding-flutter-view) to respond if we inject a tap
// there
InjectTap(0, 0);
RunLoopUntil([this] {
return LastEventReceivedMatches(
/*expected_x=*/static_cast<float>(display_width() / 2.0f),
/*expected_y=*/static_cast<float>(display_height() / 2.0f),
/*component_name=*/"embedding-flutter-view");
});
// There should be 1 injected tap
ASSERT_EQ(touch_injection_request_count(), 1);
}
TEST_P(FlutterEmbedTapTest, FlutterEmbedOverlayEnabled) {
FML_LOG(INFO) << "Initializing scene";
AddComponentArgument("--showOverlay");
LaunchClientWithEmbeddedView();
FML_LOG(INFO) << "Client launched";
{
// The bottom-left corner of the overlay is at the center of the screen
// Expect the overlay / parent view to respond if we inject a tap there
// and not the embedded child view
InjectTap(0, 0);
RunLoopUntil([this] {
return LastEventReceivedMatches(
/*expected_x=*/static_cast<float>(display_width() / 2.0f),
/*expected_y=*/static_cast<float>(display_height() / 2.0f),
/*component_name=*/"embedding-flutter-view");
});
}
{
// The embedded child view is just outside of the bottom-left corner of the
// overlay
// Expect the embedded child view to still receive taps
InjectTap(-1, -1);
RunLoopUntil([this] {
return LastEventReceivedMatches(
/*expected_x=*/static_cast<float>(display_width() / 8.0f),
/*expected_y=*/static_cast<float>(display_height() / 8.0f),
/*component_name=*/"touch-input-view");
});
}
// There should be 2 injected taps
ASSERT_EQ(touch_injection_request_count(), 2);
}
} // namespace
} // namespace touch_input_test::testing

View File

@@ -69,6 +69,10 @@ class TestApp {
for (PointerData data in packet.data) {
print('touch-input-view received tap: ${data.toStringFull()}');
if (data.change == PointerChange.down) {
this._backgroundColor = _yellow;
}
if (data.change == PointerChange.down || data.change == PointerChange.move) {
Incoming.fromSvcPath()
..connectToService(_responseListener)

View File

@@ -29,9 +29,16 @@ using fuchsia_test_utils::CheckViewExistsInSnapshot;
} // namespace
void PortableUITest::SetUp() {
void PortableUITest::SetUp(bool build_realm) {
SetUpRealmBase();
ExtendRealm();
if (build_realm) {
BuildRealm();
}
}
void PortableUITest::BuildRealm() {
realm_ = std::make_unique<RealmRoot>(realm_builder_.Build());
}

View File

@@ -44,7 +44,11 @@ class PortableUITest : public ::loop_fixture::RealLoop {
"flutter_jit_runner.cm";
static constexpr auto kFlutterRunnerEnvironment = "flutter_runner_env";
void SetUp();
void SetUp(bool build_realm = true);
// Calls the Build method for Realm Builder to build the realm
// Can only be called once, panics otherwise
void BuildRealm();
// Attaches a client view to the scene, and waits for it to render.
void LaunchClient();