Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
touch-input-test.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <fuchsia/accessibility/semantics/cpp/fidl.h>
6#include <fuchsia/buildinfo/cpp/fidl.h>
7#include <fuchsia/component/cpp/fidl.h>
8#include <fuchsia/fonts/cpp/fidl.h>
9#include <fuchsia/intl/cpp/fidl.h>
10#include <fuchsia/kernel/cpp/fidl.h>
11#include <fuchsia/memorypressure/cpp/fidl.h>
12#include <fuchsia/metrics/cpp/fidl.h>
13#include <fuchsia/net/interfaces/cpp/fidl.h>
14#include <fuchsia/sysmem/cpp/fidl.h>
15#include <fuchsia/tracing/provider/cpp/fidl.h>
16#include <fuchsia/ui/app/cpp/fidl.h>
17#include <fuchsia/ui/display/singleton/cpp/fidl.h>
18#include <fuchsia/ui/input/cpp/fidl.h>
19#include <fuchsia/ui/test/input/cpp/fidl.h>
20#include <fuchsia/ui/test/scene/cpp/fidl.h>
21#include <fuchsia/web/cpp/fidl.h>
22#include <lib/async-loop/testing/cpp/real_loop.h>
23#include <lib/async/cpp/task.h>
24#include <lib/fidl/cpp/binding_set.h>
25#include <lib/sys/component/cpp/testing/realm_builder.h>
26#include <lib/sys/component/cpp/testing/realm_builder_types.h>
27#include <lib/sys/cpp/component_context.h>
28#include <lib/zx/clock.h>
29#include <lib/zx/time.h>
30#include <zircon/status.h>
31#include <zircon/types.h>
32#include <zircon/utc.h>
33
34#include <cstddef>
35#include <cstdint>
36#include <iostream>
37#include <memory>
38#include <type_traits>
39#include <utility>
40#include <vector>
41
42#include <gtest/gtest.h>
43
44#include "flutter/fml/logging.h"
46
47// This test exercises the touch input dispatch path from Input Pipeline to a
48// Scenic client. It is a multi-component test, and carefully avoids sleeping or
49// polling for component coordination.
50// - It runs real Scene Manager and Scenic components.
51// - It uses a fake display controller; the physical device is unused.
52//
53// Components involved
54// - This test program
55// - Scene Manager
56// - Scenic
57// - Child view, a Scenic client
58//
59// Touch dispatch path
60// - Test program's injection -> Input Pipeline -> Scenic -> Child view
61//
62// Setup sequence
63// - The test sets up this view hierarchy:
64// - Top level scene, owned by Scene Manager.
65// - Child view, owned by the ui client.
66// - The test waits for a Scenic event that verifies the child has UI content in
67// the scene graph.
68// - The test injects input into Input Pipeline, emulating a display's touch
69// report.
70// - Input Pipeline dispatches the touch event to Scenic, which in turn
71// dispatches it to the child.
72// - The child receives the touch event and reports back to the test over a
73// custom test-only FIDL.
74// - Test waits for the child to report a touch; when the test receives the
75// report, the test quits
76// successfully.
77//
78// This test uses the realm_builder library to construct the topology of
79// components and routes services between them. For v2 components, every test
80// driver component sits as a child of test_manager in the topology. Thus, the
81// topology of a test driver component such as this one looks like this:
82//
83// test_manager
84// |
85// touch-input-test.cml (this component)
86//
87// With the usage of the realm_builder library, we construct a realm during
88// runtime and then extend the topology to look like:
89//
90// test_manager
91// |
92// touch-input-test.cml (this component)
93// |
94// <created realm root>
95// / \
96// scenic input-pipeline
97//
98// For more information about testing v2 components and realm_builder,
99// visit the following links:
100//
101// Testing: https://fuchsia.dev/fuchsia-src/concepts/testing/v2
102// Realm Builder:
103// https://fuchsia.dev/fuchsia-src/development/components/v2/realm_builder
104
106namespace {
107// Types imported for the realm_builder library.
108using component_testing::ChildRef;
109using component_testing::ConfigValue;
110using component_testing::DirectoryContents;
111using component_testing::LocalComponentImpl;
112using component_testing::ParentRef;
113using component_testing::Protocol;
114using component_testing::Realm;
115using component_testing::RealmRoot;
116using component_testing::Route;
117
119
120using RealmBuilder = component_testing::RealmBuilder;
121
122// Max timeout in failure cases.
123// Set this as low as you can that still works across all test platforms.
124constexpr zx::duration kTimeout = zx::min(1);
125
126constexpr auto kTestUIStackUrl =
127 "fuchsia-pkg://fuchsia.com/flatland-scene-manager-test-ui-stack#meta/"
128 "test-ui-stack.cm";
129
130constexpr auto kMockTouchInputListener = "touch_input_listener";
131constexpr auto kMockTouchInputListenerRef = ChildRef{kMockTouchInputListener};
132
133constexpr auto kTouchInputView = "touch-input-view";
134constexpr auto kTouchInputViewRef = ChildRef{kTouchInputView};
135constexpr auto kTouchInputViewUrl = "touch-input-view#meta/touch-input-view.cm";
136constexpr auto kEmbeddingFlutterView = "embedding-flutter-view";
137constexpr auto kEmbeddingFlutterViewRef = ChildRef{kEmbeddingFlutterView};
138constexpr auto kEmbeddingFlutterViewUrl =
139 "embedding-flutter-view#meta/embedding-flutter-view.cm";
140
141bool CompareDouble(double f0, double f1, double epsilon) {
142 return std::abs(f0 - f1) <= epsilon;
143}
144
145// This component implements the TouchInput protocol
146// and the interface for a RealmBuilder LocalComponentImpl. A LocalComponentImpl
147// is a component that is implemented here in the test, as opposed to
148// elsewhere in the system. When it's inserted to the realm, it will act
149// like a proper component. This is accomplished, in part, because the
150// realm_builder library creates the necessary plumbing. It creates a manifest
151// for the component and routes all capabilities to and from it.
152// LocalComponentImpl:
153// https://fuchsia.dev/fuchsia-src/development/testing/components/realm_builder#mock-components
154class TouchInputListenerServer
155 : public fuchsia::ui::test::input::TouchInputListener,
156 public LocalComponentImpl {
157 public:
158 explicit TouchInputListenerServer(async_dispatcher_t* dispatcher)
159 : dispatcher_(dispatcher) {}
160
161 // |fuchsia::ui::test::input::TouchInputListener|
162 void ReportTouchInput(
163 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest
164 request) override {
165 FML_LOG(INFO) << "Received ReportTouchInput event";
166 events_received_.push_back(std::move(request));
167 }
168
169 // |LocalComponentImpl::OnStart|
170 // When the component framework requests for this component to start, this
171 // method will be invoked by the realm_builder library.
172 void OnStart() override {
173 FML_LOG(INFO) << "Starting TouchInputListenerServer";
174 // When this component starts, add a binding to the
175 // protocol to this component's outgoing directory.
176 ASSERT_EQ(ZX_OK, outgoing()->AddPublicService(
177 fidl::InterfaceRequestHandler<
178 fuchsia::ui::test::input::TouchInputListener>(
179 [this](auto request) {
180 bindings_.AddBinding(this, std::move(request),
181 dispatcher_);
182 })));
183 }
184
185 const std::vector<
186 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest>&
187 events_received() {
188 return events_received_;
189 }
190
191 private:
192 async_dispatcher_t* dispatcher_ = nullptr;
193 fidl::BindingSet<fuchsia::ui::test::input::TouchInputListener> bindings_;
194 std::vector<
195 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest>
196 events_received_;
197};
198
199class FlutterTapTestBase : public PortableUITest, public ::testing::Test {
200 protected:
201 ~FlutterTapTestBase() override {
202 FML_CHECK(touch_injection_request_count() > 0)
203 << "Injection expected but didn't happen.";
204 }
205
206 void SetUp() override {
207 PortableUITest::SetUp();
208
209 // Post a "just in case" quit task, if the test hangs.
210 async::PostDelayedTask(
211 dispatcher(),
212 [] {
213 FML_LOG(FATAL)
214 << "\n\n>> Test did not complete in time, terminating. <<\n\n";
215 },
216 kTimeout);
217
218 // Get the display information using the
219 // |fuchsia.ui.display.singleton.Info|.
220 FML_LOG(INFO)
221 << "Waiting for display info from fuchsia.ui.display.singleton.Info";
222 std::optional<bool> display_metrics_obtained;
223 fuchsia::ui::display::singleton::InfoPtr display_info =
224 realm_root()
225 ->component()
226 .Connect<fuchsia::ui::display::singleton::Info>();
227 display_info->GetMetrics([this, &display_metrics_obtained](auto info) {
228 display_width_ = info.extent_in_px().width;
229 display_height_ = info.extent_in_px().height;
230 display_metrics_obtained = true;
231 });
232 RunLoopUntil([&display_metrics_obtained] {
233 return display_metrics_obtained.has_value();
234 });
235
236 // Register input injection device.
237 FML_LOG(INFO) << "Registering input injection device";
238 RegisterTouchScreen();
239 }
240
241 bool LastEventReceivedMatches(float expected_x,
242 float expected_y,
243 std::string component_name) {
244 const auto& events_received =
245 touch_input_listener_server_->events_received();
246
247 if (events_received.empty()) {
248 return false;
249 }
250
251 const auto& last_event = events_received.back();
252
253 auto pixel_scale = last_event.has_device_pixel_ratio()
254 ? last_event.device_pixel_ratio()
255 : 1;
256
257 auto actual_x = pixel_scale * last_event.local_x();
258 auto actual_y = pixel_scale * last_event.local_y();
259 auto actual_component = last_event.component_name();
260
261 bool last_event_matches =
262 CompareDouble(actual_x, expected_x, pixel_scale) &&
263 CompareDouble(actual_y, expected_y, pixel_scale) &&
264 last_event.component_name() == component_name;
265
266 if (last_event_matches) {
267 FML_LOG(INFO) << "Received event for component " << component_name
268 << " at (" << expected_x << ", " << expected_y << ")";
269 } else {
270 FML_LOG(WARNING) << "Expecting event for component " << component_name
271 << " at (" << expected_x << ", " << expected_y << "). "
272 << "Instead received event for component "
273 << actual_component << " at (" << actual_x << ", "
274 << actual_y << "), accounting for pixel scale of "
275 << pixel_scale;
276 }
277
278 return last_event_matches;
279 }
280
281 // Guaranteed to be initialized after SetUp().
282 uint32_t display_width() const { return display_width_; }
283 uint32_t display_height() const { return display_height_; }
284
285 std::string GetTestUIStackUrl() override { return kTestUIStackUrl; };
286
287 TouchInputListenerServer* touch_input_listener_server_;
288};
289
290class FlutterTapTest : public FlutterTapTestBase {
291 private:
292 void ExtendRealm() override {
293 FML_LOG(INFO) << "Extending realm";
294 // Key part of service setup: have this test component vend the
295 // |TouchInputListener| service in the constructed realm.
296 auto touch_input_listener_server =
297 std::make_unique<TouchInputListenerServer>(dispatcher());
298 touch_input_listener_server_ = touch_input_listener_server.get();
299 realm_builder()->AddLocalChild(
300 kMockTouchInputListener, [touch_input_listener_server = std::move(
301 touch_input_listener_server)]() mutable {
302 return std::move(touch_input_listener_server);
303 });
304
305 // Add touch-input-view to the Realm
306 realm_builder()->AddChild(kTouchInputView, kTouchInputViewUrl,
307 component_testing::ChildOptions{
308 .environment = kFlutterRunnerEnvironment,
309 });
310
311 // Route the TouchInput protocol capability to the Dart component
312 realm_builder()->AddRoute(
313 Route{.capabilities = {Protocol{
314 fuchsia::ui::test::input::TouchInputListener::Name_}},
315 .source = kMockTouchInputListenerRef,
316 .targets = {kFlutterJitRunnerRef, kTouchInputViewRef}});
317
318 realm_builder()->AddRoute(
319 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
320 .source = kTouchInputViewRef,
321 .targets = {ParentRef()}});
322 }
323};
324
325class FlutterEmbedTapTest : public FlutterTapTestBase {
326 protected:
327 void SetUp() override {
328 PortableUITest::SetUp(false);
329
330 // Post a "just in case" quit task, if the test hangs.
331 async::PostDelayedTask(
332 dispatcher(),
333 [] {
334 FML_LOG(FATAL)
335 << "\n\n>> Test did not complete in time, terminating. <<\n\n";
336 },
337 kTimeout);
338 }
339
340 void LaunchClientWithEmbeddedView() {
341 BuildRealm();
342
343 // Get the display information using the
344 // |fuchsia.ui.display.singleton.Info|.
345 FML_LOG(INFO)
346 << "Waiting for display info from fuchsia.ui.display.singleton.Info";
347 std::optional<bool> display_metrics_obtained;
348 fuchsia::ui::display::singleton::InfoPtr display_info =
349 realm_root()
350 ->component()
351 .Connect<fuchsia::ui::display::singleton::Info>();
352 display_info->GetMetrics([this, &display_metrics_obtained](auto info) {
353 display_width_ = info.extent_in_px().width;
354 display_height_ = info.extent_in_px().height;
355 display_metrics_obtained = true;
356 });
357 RunLoopUntil([&display_metrics_obtained] {
358 return display_metrics_obtained.has_value();
359 });
360
361 // Register input injection device.
362 FML_LOG(INFO) << "Registering input injection device";
363 RegisterTouchScreen();
364
365 PortableUITest::LaunchClientWithEmbeddedView();
366 }
367
368 // Helper method to add a component argument
369 // This will be written into an args.csv file that can be parsed and read
370 // by embedding-flutter-view.dart
371 //
372 // Note: You must call this method before LaunchClientWithEmbeddedView()
373 // Realm Builder will not allow you to create a new directory / file in a
374 // realm that's already been built
375 void AddComponentArgument(std::string component_arg) {
376 auto config_directory_contents = DirectoryContents();
377 config_directory_contents.AddFile("args.csv", component_arg);
378 realm_builder()->RouteReadOnlyDirectory(
379 "config-data", {kEmbeddingFlutterViewRef},
380 std::move(config_directory_contents));
381 }
382
383 private:
384 void ExtendRealm() override {
385 FML_LOG(INFO) << "Extending realm";
386 // Key part of service setup: have this test component vend the
387 // |TouchInputListener| service in the constructed realm.
388 auto touch_input_listener_server =
389 std::make_unique<TouchInputListenerServer>(dispatcher());
390 touch_input_listener_server_ = touch_input_listener_server.get();
391 realm_builder()->AddLocalChild(
392 kMockTouchInputListener, [touch_input_listener_server = std::move(
393 touch_input_listener_server)]() mutable {
394 return std::move(touch_input_listener_server);
395 });
396
397 // Add touch-input-view to the Realm
398 realm_builder()->AddChild(kTouchInputView, kTouchInputViewUrl,
399 component_testing::ChildOptions{
400 .environment = kFlutterRunnerEnvironment,
401 });
402 // Add embedding-flutter-view to the Realm
403 // This component will embed touch-input-view as a child view
404 realm_builder()->AddChild(kEmbeddingFlutterView, kEmbeddingFlutterViewUrl,
405 component_testing::ChildOptions{
406 .environment = kFlutterRunnerEnvironment,
407 });
408
409 // Route the TouchInput protocol capability to the Dart component
410 realm_builder()->AddRoute(
411 Route{.capabilities = {Protocol{
412 fuchsia::ui::test::input::TouchInputListener::Name_}},
413 .source = kMockTouchInputListenerRef,
414 .targets = {kFlutterJitRunnerRef, kTouchInputViewRef,
415 kEmbeddingFlutterViewRef}});
416
417 realm_builder()->AddRoute(
418 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
419 .source = kEmbeddingFlutterViewRef,
420 .targets = {ParentRef()}});
421 realm_builder()->AddRoute(
422 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
423 .source = kTouchInputViewRef,
424 .targets = {kEmbeddingFlutterViewRef}});
425 }
426};
427
428TEST_F(FlutterTapTest, FlutterTap) {
429 // Launch client view, and wait until it's rendering to proceed with the test.
430 FML_LOG(INFO) << "Initializing scene";
431 LaunchClient();
432 FML_LOG(INFO) << "Client launched";
433
434 // touch-input-view logical coordinate space doesn't match the fake touch
435 // screen injector's coordinate space, which spans [-1000, 1000] on both axes.
436 // Scenic handles figuring out where in the coordinate space
437 // to inject a touch event (this is fixed to a display's bounds).
438 InjectTap(-500, -500);
439 // For a (-500 [x], -500 [y]) tap, we expect a touch event in the middle of
440 // the upper-left quadrant of the screen.
441 RunLoopUntil([this] {
442 return LastEventReceivedMatches(
443 /*expected_x=*/static_cast<float>(display_width() / 4.0f),
444 /*expected_y=*/static_cast<float>(display_height() / 4.0f),
445 /*component_name=*/"touch-input-view");
446 });
447
448 // There should be 1 injected tap
449 ASSERT_EQ(touch_injection_request_count(), 1);
450}
451
452TEST_F(FlutterEmbedTapTest, FlutterEmbedTap) {
453 // Launch view
454 FML_LOG(INFO) << "Initializing scene";
455 LaunchClientWithEmbeddedView();
456 FML_LOG(INFO) << "Client launched";
457
458 {
459 // Embedded child view takes up the center of the screen
460 // Expect a response from the child view if we inject a tap there
461 InjectTap(0, 0);
462 RunLoopUntil([this] {
463 return LastEventReceivedMatches(
464 /*expected_x=*/static_cast<float>(display_width() / 8.0f),
465 /*expected_y=*/static_cast<float>(display_height() / 8.0f),
466 /*component_name=*/"touch-input-view");
467 });
468 }
469
470 {
471 // Parent view takes up the rest of the screen
472 // Validate that parent can still receive taps
473 InjectTap(500, 500);
474 RunLoopUntil([this] {
475 return LastEventReceivedMatches(
476 /*expected_x=*/static_cast<float>(display_width() / (4.0f / 3.0f)),
477 /*expected_y=*/static_cast<float>(display_height() / (4.0f / 3.0f)),
478 /*component_name=*/"embedding-flutter-view");
479 });
480 }
481
482 // There should be 2 injected taps
483 ASSERT_EQ(touch_injection_request_count(), 2);
484}
485
486TEST_F(FlutterEmbedTapTest, FlutterEmbedOverlayEnabled) {
487 FML_LOG(INFO) << "Initializing scene";
488 AddComponentArgument("--showOverlay");
489 LaunchClientWithEmbeddedView();
490 FML_LOG(INFO) << "Client launched";
491
492 {
493 // The bottom-left corner of the overlay is at the center of the screen
494 // Expect the overlay / parent view to respond if we inject a tap there
495 // and not the embedded child view
496 InjectTap(0, 0);
497 RunLoopUntil([this] {
498 return LastEventReceivedMatches(
499 /*expected_x=*/static_cast<float>(display_width() / 2.0f),
500 /*expected_y=*/static_cast<float>(display_height() / 2.0f),
501 /*component_name=*/"embedding-flutter-view");
502 });
503 }
504
505 {
506 // The embedded child view is just outside of the bottom-left corner of the
507 // overlay
508 // Expect the embedded child view to still receive taps
509 InjectTap(-1, -1);
510 RunLoopUntil([this] {
511 return LastEventReceivedMatches(
512 /*expected_x=*/static_cast<float>(display_width() / 8.0f),
513 /*expected_y=*/static_cast<float>(display_height() / 8.0f),
514 /*component_name=*/"touch-input-view");
515 });
516 }
517
518 // There should be 2 injected taps
519 ASSERT_EQ(touch_injection_request_count(), 2);
520}
521
522} // namespace
523} // namespace touch_input_test::testing
#define FML_LOG(severity)
Definition logging.h:101
#define FML_CHECK(condition)
Definition logging.h:104
TouchInputListenerServer * touch_input_listener_server_