Flutter Engine
The Flutter Engine
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"
45#include "flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.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 =
136 "fuchsia-pkg://fuchsia.com/touch-input-view#meta/touch-input-view.cm";
137constexpr auto kEmbeddingFlutterView = "embedding-flutter-view";
138constexpr auto kEmbeddingFlutterViewRef = ChildRef{kEmbeddingFlutterView};
139constexpr auto kEmbeddingFlutterViewUrl =
140 "fuchsia-pkg://fuchsia.com/embedding-flutter-view#meta/"
141 "embedding-flutter-view.cm";
142
143bool CompareDouble(double f0, double f1, double epsilon) {
144 return std::abs(f0 - f1) <= epsilon;
145}
146
147// This component implements the TouchInput protocol
148// and the interface for a RealmBuilder LocalComponentImpl. A LocalComponentImpl
149// is a component that is implemented here in the test, as opposed to
150// elsewhere in the system. When it's inserted to the realm, it will act
151// like a proper component. This is accomplished, in part, because the
152// realm_builder library creates the necessary plumbing. It creates a manifest
153// for the component and routes all capabilities to and from it.
154// LocalComponentImpl:
155// https://fuchsia.dev/fuchsia-src/development/testing/components/realm_builder#mock-components
156class TouchInputListenerServer
157 : public fuchsia::ui::test::input::TouchInputListener,
158 public LocalComponentImpl {
159 public:
160 explicit TouchInputListenerServer(async_dispatcher_t* dispatcher)
161 : dispatcher_(dispatcher) {}
162
163 // |fuchsia::ui::test::input::TouchInputListener|
164 void ReportTouchInput(
165 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest
166 request) override {
167 FML_LOG(INFO) << "Received ReportTouchInput event";
168 events_received_.push_back(std::move(request));
169 }
170
171 // |LocalComponentImpl::OnStart|
172 // When the component framework requests for this component to start, this
173 // method will be invoked by the realm_builder library.
174 void OnStart() override {
175 FML_LOG(INFO) << "Starting TouchInputListenerServer";
176 // When this component starts, add a binding to the
177 // protocol to this component's outgoing directory.
178 ASSERT_EQ(ZX_OK, outgoing()->AddPublicService(
179 fidl::InterfaceRequestHandler<
180 fuchsia::ui::test::input::TouchInputListener>(
181 [this](auto request) {
182 bindings_.AddBinding(this, std::move(request),
183 dispatcher_);
184 })));
185 }
186
187 const std::vector<
188 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest>&
189 events_received() {
190 return events_received_;
191 }
192
193 private:
194 async_dispatcher_t* dispatcher_ = nullptr;
195 fidl::BindingSet<fuchsia::ui::test::input::TouchInputListener> bindings_;
196 std::vector<
197 fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest>
198 events_received_;
199};
200
201class FlutterTapTestBase : public PortableUITest, public ::testing::Test {
202 protected:
203 ~FlutterTapTestBase() override {
204 FML_CHECK(touch_injection_request_count() > 0)
205 << "Injection expected but didn't happen.";
206 }
207
208 void SetUp() override {
209 PortableUITest::SetUp();
210
211 // Post a "just in case" quit task, if the test hangs.
212 async::PostDelayedTask(
213 dispatcher(),
214 [] {
216 << "\n\n>> Test did not complete in time, terminating. <<\n\n";
217 },
218 kTimeout);
219
220 // Get the display information using the
221 // |fuchsia.ui.display.singleton.Info|.
222 FML_LOG(INFO)
223 << "Waiting for display info from fuchsia.ui.display.singleton.Info";
224 std::optional<bool> display_metrics_obtained;
225 fuchsia::ui::display::singleton::InfoPtr display_info =
226 realm_root()
227 ->component()
228 .Connect<fuchsia::ui::display::singleton::Info>();
229 display_info->GetMetrics([this, &display_metrics_obtained](auto info) {
230 display_width_ = info.extent_in_px().width;
231 display_height_ = info.extent_in_px().height;
232 display_metrics_obtained = true;
233 });
234 RunLoopUntil([&display_metrics_obtained] {
235 return display_metrics_obtained.has_value();
236 });
237
238 // Register input injection device.
239 FML_LOG(INFO) << "Registering input injection device";
240 RegisterTouchScreen();
241 }
242
243 bool LastEventReceivedMatches(float expected_x,
244 float expected_y,
245 std::string component_name) {
246 const auto& events_received =
247 touch_input_listener_server_->events_received();
248
249 if (events_received.empty()) {
250 return false;
251 }
252
253 const auto& last_event = events_received.back();
254
255 auto pixel_scale = last_event.has_device_pixel_ratio()
256 ? last_event.device_pixel_ratio()
257 : 1;
258
259 auto actual_x = pixel_scale * last_event.local_x();
260 auto actual_y = pixel_scale * last_event.local_y();
261 auto actual_component = last_event.component_name();
262
263 bool last_event_matches =
264 CompareDouble(actual_x, expected_x, pixel_scale) &&
265 CompareDouble(actual_y, expected_y, pixel_scale) &&
266 last_event.component_name() == component_name;
267
268 if (last_event_matches) {
269 FML_LOG(INFO) << "Received event for component " << component_name
270 << " at (" << expected_x << ", " << expected_y << ")";
271 } else {
272 FML_LOG(WARNING) << "Expecting event for component " << component_name
273 << " at (" << expected_x << ", " << expected_y << "). "
274 << "Instead received event for component "
275 << actual_component << " at (" << actual_x << ", "
276 << actual_y << "), accounting for pixel scale of "
277 << pixel_scale;
278 }
279
280 return last_event_matches;
281 }
282
283 // Guaranteed to be initialized after SetUp().
284 uint32_t display_width() const { return display_width_; }
285 uint32_t display_height() const { return display_height_; }
286
287 std::string GetTestUIStackUrl() override { return kTestUIStackUrl; };
288
289 TouchInputListenerServer* touch_input_listener_server_;
290};
291
292class FlutterTapTest : public FlutterTapTestBase {
293 private:
294 void ExtendRealm() override {
295 FML_LOG(INFO) << "Extending realm";
296 // Key part of service setup: have this test component vend the
297 // |TouchInputListener| service in the constructed realm.
298 auto touch_input_listener_server =
299 std::make_unique<TouchInputListenerServer>(dispatcher());
300 touch_input_listener_server_ = touch_input_listener_server.get();
301 realm_builder()->AddLocalChild(
302 kMockTouchInputListener, [touch_input_listener_server = std::move(
303 touch_input_listener_server)]() mutable {
304 return std::move(touch_input_listener_server);
305 });
306
307 // Add touch-input-view to the Realm
308 realm_builder()->AddChild(kTouchInputView, kTouchInputViewUrl,
309 component_testing::ChildOptions{
310 .environment = kFlutterRunnerEnvironment,
311 });
312
313 // Route the TouchInput protocol capability to the Dart component
314 realm_builder()->AddRoute(
315 Route{.capabilities = {Protocol{
316 fuchsia::ui::test::input::TouchInputListener::Name_}},
317 .source = kMockTouchInputListenerRef,
318 .targets = {kFlutterJitRunnerRef, kTouchInputViewRef}});
319
320 realm_builder()->AddRoute(
321 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
322 .source = kTouchInputViewRef,
323 .targets = {ParentRef()}});
324 }
325};
326
327class FlutterEmbedTapTest : public FlutterTapTestBase {
328 protected:
329 void SetUp() override {
330 PortableUITest::SetUp(false);
331
332 // Post a "just in case" quit task, if the test hangs.
333 async::PostDelayedTask(
334 dispatcher(),
335 [] {
337 << "\n\n>> Test did not complete in time, terminating. <<\n\n";
338 },
339 kTimeout);
340 }
341
342 void LaunchClientWithEmbeddedView() {
343 BuildRealm();
344
345 // Get the display information using the
346 // |fuchsia.ui.display.singleton.Info|.
347 FML_LOG(INFO)
348 << "Waiting for display info from fuchsia.ui.display.singleton.Info";
349 std::optional<bool> display_metrics_obtained;
350 fuchsia::ui::display::singleton::InfoPtr display_info =
351 realm_root()
352 ->component()
353 .Connect<fuchsia::ui::display::singleton::Info>();
354 display_info->GetMetrics([this, &display_metrics_obtained](auto info) {
355 display_width_ = info.extent_in_px().width;
356 display_height_ = info.extent_in_px().height;
357 display_metrics_obtained = true;
358 });
359 RunLoopUntil([&display_metrics_obtained] {
360 return display_metrics_obtained.has_value();
361 });
362
363 // Register input injection device.
364 FML_LOG(INFO) << "Registering input injection device";
365 RegisterTouchScreen();
366
367 PortableUITest::LaunchClientWithEmbeddedView();
368 }
369
370 // Helper method to add a component argument
371 // This will be written into an args.csv file that can be parsed and read
372 // by embedding-flutter-view.dart
373 //
374 // Note: You must call this method before LaunchClientWithEmbeddedView()
375 // Realm Builder will not allow you to create a new directory / file in a
376 // realm that's already been built
377 void AddComponentArgument(std::string component_arg) {
378 auto config_directory_contents = DirectoryContents();
379 config_directory_contents.AddFile("args.csv", component_arg);
380 realm_builder()->RouteReadOnlyDirectory(
381 "config-data", {kEmbeddingFlutterViewRef},
382 std::move(config_directory_contents));
383 }
384
385 private:
386 void ExtendRealm() override {
387 FML_LOG(INFO) << "Extending realm";
388 // Key part of service setup: have this test component vend the
389 // |TouchInputListener| service in the constructed realm.
390 auto touch_input_listener_server =
391 std::make_unique<TouchInputListenerServer>(dispatcher());
392 touch_input_listener_server_ = touch_input_listener_server.get();
393 realm_builder()->AddLocalChild(
394 kMockTouchInputListener, [touch_input_listener_server = std::move(
395 touch_input_listener_server)]() mutable {
396 return std::move(touch_input_listener_server);
397 });
398
399 // Add touch-input-view to the Realm
400 realm_builder()->AddChild(kTouchInputView, kTouchInputViewUrl,
401 component_testing::ChildOptions{
402 .environment = kFlutterRunnerEnvironment,
403 });
404 // Add embedding-flutter-view to the Realm
405 // This component will embed touch-input-view as a child view
406 realm_builder()->AddChild(kEmbeddingFlutterView, kEmbeddingFlutterViewUrl,
407 component_testing::ChildOptions{
408 .environment = kFlutterRunnerEnvironment,
409 });
410
411 // Route the TouchInput protocol capability to the Dart component
412 realm_builder()->AddRoute(
413 Route{.capabilities = {Protocol{
414 fuchsia::ui::test::input::TouchInputListener::Name_}},
415 .source = kMockTouchInputListenerRef,
416 .targets = {kFlutterJitRunnerRef, kTouchInputViewRef,
417 kEmbeddingFlutterViewRef}});
418
419 realm_builder()->AddRoute(
420 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
421 .source = kEmbeddingFlutterViewRef,
422 .targets = {ParentRef()}});
423 realm_builder()->AddRoute(
424 Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
425 .source = kTouchInputViewRef,
426 .targets = {kEmbeddingFlutterViewRef}});
427 }
428};
429
430TEST_F(FlutterTapTest, FlutterTap) {
431 // Launch client view, and wait until it's rendering to proceed with the test.
432 FML_LOG(INFO) << "Initializing scene";
433 LaunchClient();
434 FML_LOG(INFO) << "Client launched";
435
436 // touch-input-view logical coordinate space doesn't match the fake touch
437 // screen injector's coordinate space, which spans [-1000, 1000] on both axes.
438 // Scenic handles figuring out where in the coordinate space
439 // to inject a touch event (this is fixed to a display's bounds).
440 InjectTap(-500, -500);
441 // For a (-500 [x], -500 [y]) tap, we expect a touch event in the middle of
442 // the upper-left quadrant of the screen.
443 RunLoopUntil([this] {
444 return LastEventReceivedMatches(
445 /*expected_x=*/static_cast<float>(display_width() / 4.0f),
446 /*expected_y=*/static_cast<float>(display_height() / 4.0f),
447 /*component_name=*/"touch-input-view");
448 });
449
450 // There should be 1 injected tap
451 ASSERT_EQ(touch_injection_request_count(), 1);
452}
453
454TEST_F(FlutterEmbedTapTest, FlutterEmbedTap) {
455 // Launch view
456 FML_LOG(INFO) << "Initializing scene";
457 LaunchClientWithEmbeddedView();
458 FML_LOG(INFO) << "Client launched";
459
460 {
461 // Embedded child view takes up the center of the screen
462 // Expect a response from the child view if we inject a tap there
463 InjectTap(0, 0);
464 RunLoopUntil([this] {
465 return LastEventReceivedMatches(
466 /*expected_x=*/static_cast<float>(display_width() / 8.0f),
467 /*expected_y=*/static_cast<float>(display_height() / 8.0f),
468 /*component_name=*/"touch-input-view");
469 });
470 }
471
472 {
473 // Parent view takes up the rest of the screen
474 // Validate that parent can still receive taps
475 InjectTap(500, 500);
476 RunLoopUntil([this] {
477 return LastEventReceivedMatches(
478 /*expected_x=*/static_cast<float>(display_width() / (4.0f / 3.0f)),
479 /*expected_y=*/static_cast<float>(display_height() / (4.0f / 3.0f)),
480 /*component_name=*/"embedding-flutter-view");
481 });
482 }
483
484 // There should be 2 injected taps
485 ASSERT_EQ(touch_injection_request_count(), 2);
486}
487
488TEST_F(FlutterEmbedTapTest, FlutterEmbedOverlayEnabled) {
489 FML_LOG(INFO) << "Initializing scene";
490 AddComponentArgument("--showOverlay");
491 LaunchClientWithEmbeddedView();
492 FML_LOG(INFO) << "Client launched";
493
494 {
495 // The bottom-left corner of the overlay is at the center of the screen
496 // Expect the overlay / parent view to respond if we inject a tap there
497 // and not the embedded child view
498 InjectTap(0, 0);
499 RunLoopUntil([this] {
500 return LastEventReceivedMatches(
501 /*expected_x=*/static_cast<float>(display_width() / 2.0f),
502 /*expected_y=*/static_cast<float>(display_height() / 2.0f),
503 /*component_name=*/"embedding-flutter-view");
504 });
505 }
506
507 {
508 // The embedded child view is just outside of the bottom-left corner of the
509 // overlay
510 // Expect the embedded child view to still receive taps
511 InjectTap(-1, -1);
512 RunLoopUntil([this] {
513 return LastEventReceivedMatches(
514 /*expected_x=*/static_cast<float>(display_width() / 8.0f),
515 /*expected_y=*/static_cast<float>(display_height() / 8.0f),
516 /*component_name=*/"touch-input-view");
517 });
518 }
519
520 // There should be 2 injected taps
521 ASSERT_EQ(touch_injection_request_count(), 2);
522}
523
524} // namespace
525} // namespace touch_input_test::testing
static void info(const char *fmt,...) SK_PRINTF_LIKE(1
Definition DM.cpp:213
#define FATAL(error)
TEST_F(FlGnomeSettingsTest, ClockFormat)
#define FML_LOG(severity)
Definition logging.h:82
#define FML_CHECK(condition)
Definition logging.h:85
TouchInputListenerServer * touch_input_listener_server_