Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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();

Expand All @@ -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"});

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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);
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Loading