diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn index 9480c64335..de41ca3927 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn @@ -477,6 +477,7 @@ if (enable_unittests) { "tests/fake_session_unittests.cc", "tests/flatland_connection_unittests.cc", "tests/flatland_external_view_embedder_unittests.cc", + "tests/flatland_platform_view_unittest.cc", "tests/flutter_runner_product_configuration_unittests.cc", "tests/gfx_external_view_embedder_unittests.cc", "tests/gfx_session_connection_unittests.cc", diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/flatland_platform_view.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/flatland_platform_view.cc index dcbc56d359..5e47dbba8b 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/flatland_platform_view.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/flatland_platform_view.cc @@ -73,6 +73,10 @@ void FlatlandPlatformView::OnGetLayout( static_cast(info.logical_size().height)}; // TODO(fxbug.dev/64201): Set device pixel ratio. + if (info.pixel_scale().width != 1 || info.pixel_scale().height != 1) { + FML_LOG(ERROR) + << "Flutter does not currently support pixel_scale's other than 1"; + } SetViewportMetrics({ 1, // device_pixel_ratio @@ -204,6 +208,7 @@ void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, }); })); }; + on_create_view_callback_(view_id_raw, std::move(on_view_created), std::move(on_view_bound), hit_testable, focusable); } @@ -211,9 +216,9 @@ void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, void FlatlandPlatformView::OnDisposeView(int64_t view_id_raw) { auto on_view_unbound = [weak = weak_factory_.GetWeakPtr(), - platform_task_runner = task_runners_.GetPlatformTaskRunner()]( - fuchsia::ui::composition::ContentId content_id) { - platform_task_runner->PostTask([weak, content_id]() { + platform_task_runner = task_runners_.GetPlatformTaskRunner(), + view_id_raw](fuchsia::ui::composition::ContentId content_id) { + platform_task_runner->PostTask([weak, content_id, view_id_raw]() { if (!weak) { FML_LOG(WARNING) << "Flatland View unbound from PlatformView after PlatformView" @@ -223,6 +228,7 @@ void FlatlandPlatformView::OnDisposeView(int64_t view_id_raw) { FML_DCHECK(weak->child_view_info_.count(content_id.value) == 1); weak->child_view_info_.erase(content_id.value); + weak->focus_delegate_->OnDisposeChildView(view_id_raw); }); }; on_destroy_view_callback_(view_id_raw, std::move(on_view_unbound)); diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.cc index b7e85828bd..60af4c3641 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.cc @@ -87,7 +87,8 @@ bool FocusDelegate::HandlePlatformMessage( if (child_view_view_refs_.count(id) != 1) { FML_LOG(ERROR) << "Argument 'viewId' (" << id << ") does not refer to a valid ChildView"; - return false; + Complete(std::move(response), "[1]"); + return true; } return RequestFocusById(id, std::move(response)); @@ -104,6 +105,11 @@ void FocusDelegate::OnChildViewViewRef(uint64_t view_id, child_view_view_refs_[view_id] = std::move(view_ref); } +void FocusDelegate::OnDisposeChildView(uint64_t view_id) { + FML_CHECK(child_view_view_refs_.count(view_id) == 1); + child_view_view_refs_.erase(view_id); +} + void FocusDelegate::Complete( fml::RefPtr response, std::string value) { diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.h b/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.h index 98793939fa..ec6ae91ae7 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/focus_delegate.h @@ -53,6 +53,8 @@ class FocusDelegate { void OnChildViewViewRef(uint64_t view_id, fuchsia::ui::views::ViewRef view_ref); + void OnDisposeChildView(uint64_t view_id); + private: fuchsia::ui::views::ViewRefFocusedPtr view_ref_focused_; fuchsia::ui::views::FocuserPtr focuser_; diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/fakes/platform_message.h b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/fakes/platform_message.h index ef1a095528..e024b9bc85 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/fakes/platform_message.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/fakes/platform_message.h @@ -29,6 +29,8 @@ class FakePlatformMessageResponse : public PlatformMessageResponse { } } + bool IsCompleted() { return is_complete_; } + std::unique_ptr WithMessage(std::string channel, std::string message) { return std::make_unique( diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/flatland_platform_view_unittest.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/flatland_platform_view_unittest.cc new file mode 100644 index 0000000000..c32445bb14 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/flatland_platform_view_unittest.cc @@ -0,0 +1,1495 @@ +// Copyright 2013 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "flutter/flow/embedded_views.h" +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/pointer_data.h" +#include "flutter/lib/ui/window/viewport_metrics.h" +#include "flutter/shell/common/context_options.h" +#include "flutter/shell/platform/fuchsia/flutter/flatland_platform_view.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "fakes/focuser.h" +#include "fakes/platform_message.h" +#include "fakes/touch_source.h" +#include "fakes/view_ref_focused.h" +#include "flutter/shell/platform/fuchsia/flutter/surface.h" +#include "flutter/shell/platform/fuchsia/flutter/task_runner_adapter.h" +#include "platform/assert.h" +#include "pointer_event_utility.h" + +namespace flutter_runner::testing { +namespace { + +class MockExternalViewEmbedder : public flutter::ExternalViewEmbedder { + public: + SkCanvas* GetRootCanvas() override { return nullptr; } + std::vector GetCurrentCanvases() override { + return std::vector(); + } + + void CancelFrame() override {} + void BeginFrame( + SkISize frame_size, + GrDirectContext* context, + double device_pixel_ratio, + fml::RefPtr raster_thread_merger) override {} + void SubmitFrame(GrDirectContext* context, + std::unique_ptr frame) override { + return; + } + + void PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) override {} + SkCanvas* CompositeEmbeddedView(int view_id) override { return nullptr; } +}; + +class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { + public: + void Reset() { + message_ = nullptr; + metrics_ = flutter::ViewportMetrics{}; + semantics_features_ = 0; + semantics_enabled_ = false; + pointer_packets_.clear(); + } + + // |flutter::PlatformView::Delegate| + void OnPlatformViewCreated(std::unique_ptr surface) { + ASSERT_EQ(surface_.get(), nullptr); + + surface_ = std::move(surface); + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewDestroyed() {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewSetViewportMetrics( + const flutter::ViewportMetrics& metrics) { + metrics_ = metrics; + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewDispatchPlatformMessage( + std::unique_ptr message) { + message_ = std::move(message); + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewDispatchPointerDataPacket( + std::unique_ptr packet) { + pointer_packets_.push_back(std::move(packet)); + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewDispatchKeyDataPacket( + std::unique_ptr packet, + std::function callback) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewDispatchSemanticsAction(int32_t id, + flutter::SemanticsAction action, + fml::MallocMapping args) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewSetSemanticsEnabled(bool enabled) { + semantics_enabled_ = enabled; + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewSetAccessibilityFeatures(int32_t flags) { + semantics_features_ = flags; + } + // |flutter::PlatformView::Delegate| + void OnPlatformViewRegisterTexture( + std::shared_ptr texture) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewUnregisterTexture(int64_t texture_id) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) {} + // |flutter::PlatformView::Delegate| + std::unique_ptr> ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) { + return nullptr; + } + // |flutter::PlatformView::Delegate| + void LoadDartDeferredLibrary( + intptr_t loading_unit_id, + std::unique_ptr snapshot_data, + std::unique_ptr snapshot_instructions) {} + // |flutter::PlatformView::Delegate| + void LoadDartDeferredLibraryError(intptr_t loading_unit_id, + const std::string error_message, + bool transient) {} + // |flutter::PlatformView::Delegate| + void UpdateAssetResolverByType( + std::unique_ptr updated_asset_resolver, + flutter::AssetResolver::AssetResolverType type) {} + + flutter::Surface* surface() const { return surface_.get(); } + flutter::PlatformMessage* message() const { return message_.get(); } + const flutter::ViewportMetrics& metrics() const { return metrics_; } + int32_t semantics_features() const { return semantics_features_; } + bool semantics_enabled() const { return semantics_enabled_; } + const std::vector>& + pointer_packets() const { + return pointer_packets_; + } + std::vector> + TakePointerDataPackets() { + auto tmp = std::move(pointer_packets_); + pointer_packets_.clear(); + return tmp; + } + + private: + std::unique_ptr surface_; + std::unique_ptr message_; + flutter::ViewportMetrics metrics_; + std::vector> pointer_packets_; + int32_t semantics_features_ = 0; + bool semantics_enabled_ = false; +}; + +class MockResponse : public flutter::PlatformMessageResponse { + public: + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); +}; + +class TestPlatformMessageResponse : public flutter::PlatformMessageResponse { + public: + TestPlatformMessageResponse() {} + void Complete(std::unique_ptr data) override { + result_string = std::string( + reinterpret_cast(data->GetMapping()), data->GetSize()); + is_complete_ = true; + } + void CompleteEmpty() override { is_complete_ = true; } + std::string result_string; + FML_DISALLOW_COPY_AND_ASSIGN(TestPlatformMessageResponse); +}; + +class MockKeyboard : public fuchsia::ui::input3::testing::Keyboard_TestBase { + public: + explicit MockKeyboard( + fidl::InterfaceRequest keyboard) + : keyboard_(this, std::move(keyboard)) {} + ~MockKeyboard() = default; + + void AddListener(fuchsia::ui::views::ViewRef view_ref, + fuchsia::ui::input3::KeyboardListenerHandle listener, + AddListenerCallback callback) override { + FML_CHECK(!listener_.is_bound()); + + listener_ = listener.Bind(); + view_ref_ = std::move(view_ref); + + callback(); + } + + void NotImplemented_(const std::string& name) override { FAIL(); } + + fidl::Binding keyboard_; + fuchsia::ui::input3::KeyboardListenerPtr listener_{}; + fuchsia::ui::views::ViewRef view_ref_{}; + + FML_DISALLOW_COPY_AND_ASSIGN(MockKeyboard); +}; + +class MockChildViewWatcher + : public fuchsia::ui::composition::testing::ChildViewWatcher_TestBase { + public: + explicit MockChildViewWatcher( + fidl::InterfaceRequest + request) + : binding_(this, std::move(request)) {} + ~MockChildViewWatcher() = default; + + // |fuchsia::ui::composition::ChildViewWatcher| + void GetStatus(GetStatusCallback callback) override { + // GetStatus only returns once as per flatland.fidl comments + if (get_status_returned_) { + return; + } + callback(fuchsia::ui::composition::ChildViewStatus::CONTENT_HAS_PRESENTED); + get_status_returned_ = true; + } + + // |fuchsia::ui::composition::ChildViewWatcher| + void GetViewRef(GetViewRefCallback callback) override { + // GetViewRef only returns once as per flatland.fidl comments + if (control_ref_.reference) { + return; + } + auto pair = scenic::ViewRefPair::New(); + control_ref_ = std::move(pair.control_ref); + callback(std::move(pair.view_ref)); + } + + void NotImplemented_(const std::string& name) override { FAIL(); } + + fidl::Binding binding_; + fuchsia::ui::views::ViewRefControl control_ref_; + bool get_status_returned_ = false; + + FML_DISALLOW_COPY_AND_ASSIGN(MockChildViewWatcher); +}; + +class MockParentViewportWatcher + : public fuchsia::ui::composition::testing::ParentViewportWatcher_TestBase { + public: + explicit MockParentViewportWatcher() : binding_(this, handle_.NewRequest()) {} + ~MockParentViewportWatcher() = default; + + // |fuchsia::ui::composition::ParentViewportWatcher| + void GetStatus(GetStatusCallback callback) override { + // GetStatus only returns once as per flatland.fidl comments + if (get_status_returned_) { + return; + } + callback( + fuchsia::ui::composition::ParentViewportStatus::CONNECTED_TO_DISPLAY); + get_status_returned_ = true; + } + + // |fuchsia::ui::composition::ParentViewportWatcher| + void GetLayout(GetLayoutCallback callback) override { + if (layout_changed_) { + callback(std::move(layout_)); + } else { + FML_CHECK(!pending_callback_valid_); + pending_layout_callback_ = std::move(callback); + pending_callback_valid_ = true; + } + } + + void SetLayout(uint32_t logical_size_x, + uint32_t logical_size_y, + uint32_t pixel_scale_x = 1u, + uint32_t pixel_scale_y = 1u) { + ::fuchsia::math::SizeU logical_size; + logical_size.width = logical_size_x; + logical_size.height = logical_size_y; + layout_.set_logical_size(logical_size); + ::fuchsia::math::SizeU pixel_scale; + pixel_scale.width = pixel_scale_x; + pixel_scale.height = pixel_scale_y; + layout_.set_pixel_scale(pixel_scale); + + if (pending_callback_valid_) { + pending_layout_callback_(std::move(layout_)); + pending_callback_valid_ = false; + } else { + layout_changed_ = true; + } + } + + fuchsia::ui::composition::ParentViewportWatcherHandle GetHandle() { + FML_CHECK(handle_); // You can only get the handle once. + return std::move(handle_); + } + + void NotImplemented_(const std::string& name) override { FAIL(); } + + fuchsia::ui::composition::ParentViewportWatcherHandle handle_; + fidl::Binding binding_; + + fuchsia::ui::composition::LayoutInfo layout_; + bool layout_changed_ = false; + GetLayoutCallback pending_layout_callback_; + bool pending_callback_valid_ = false; + + bool get_status_returned_ = false; + + FML_DISALLOW_COPY_AND_ASSIGN(MockParentViewportWatcher); +}; + +// Used to construct partial instances of PlatformView for testing. The +// PlatformView constructor has many parameters, not all of which need to +// be filled out for each test. The builder allows you to initialize only +// those that matter to your specific test. Not all builder methods are +// provided: if you find some that are missing, feel free to add them. +class PlatformViewBuilder { + public: + PlatformViewBuilder(flutter::PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners) + : delegate_(delegate), + task_runners_(task_runners), + view_ref_pair_(scenic::ViewRefPair::New()) {} + + PlatformViewBuilder& SetExternalViewEmbedder( + std::shared_ptr embedder) { + external_external_view_embedder_ = embedder; + return *this; + } + + PlatformViewBuilder& SetImeService( + fuchsia::ui::input::ImeServiceHandle ime_service) { + ime_service_ = std::move(ime_service); + return *this; + } + + PlatformViewBuilder& SetKeyboard( + fuchsia::ui::input3::KeyboardHandle keyboard) { + keyboard_ = std::move(keyboard); + return *this; + } + + PlatformViewBuilder& SetTouchSource( + fuchsia::ui::pointer::TouchSourceHandle touch_source) { + touch_source_ = std::move(touch_source); + return *this; + } + + PlatformViewBuilder& SetMouseSource( + fuchsia::ui::pointer::MouseSourceHandle mouse_source) { + mouse_source_ = std::move(mouse_source); + return *this; + } + + PlatformViewBuilder& SetFocuser(fuchsia::ui::views::FocuserHandle focuser) { + focuser_ = std::move(focuser); + return *this; + } + + PlatformViewBuilder& SetViewRefFocused( + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused) { + view_ref_focused_ = std::move(view_ref_focused); + return *this; + } + + PlatformViewBuilder& SetEnableWireframeCallback(OnEnableWireframe callback) { + wireframe_enabled_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetParentViewportWatcher( + fuchsia::ui::composition::ParentViewportWatcherHandle + parent_viewport_watcher) { + parent_viewport_watcher_ = std::move(parent_viewport_watcher); + return *this; + } + + PlatformViewBuilder& SetCreateViewCallback(OnCreateFlatlandView callback) { + on_create_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetDestroyViewCallback(OnDestroyFlatlandView callback) { + on_destroy_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetUpdateViewCallback(OnUpdateView callback) { + on_update_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetCreateSurfaceCallback(OnCreateSurface callback) { + on_create_surface_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetShaderWarmupCallback(OnShaderWarmup callback) { + on_shader_warmup_callback_ = std::move(callback); + return *this; + } + + // Once Build is called, the instance is no longer usable. + FlatlandPlatformView Build() { + EXPECT_FALSE(std::exchange(built_, true)) + << "Build() was already called, this buider is good for one use only."; + return FlatlandPlatformView( + delegate_, task_runners_, std::move(view_ref_pair_.view_ref), + external_external_view_embedder_, std::move(ime_service_), + std::move(keyboard_), std::move(touch_source_), + std::move(mouse_source_), std::move(focuser_), + std::move(view_ref_focused_), std::move(parent_viewport_watcher_), + std::move(wireframe_enabled_callback_), + std::move(on_create_view_callback_), + std::move(on_update_view_callback_), + std::move(on_destroy_view_callback_), + std::move(on_create_surface_callback_), + std::move(on_semantics_node_update_callback_), + std::move(on_request_announce_callback_), + std::move(on_shader_warmup_callback_), [](auto...) {}, [](auto...) {}); + } + + private: + PlatformViewBuilder() = delete; + + flutter::PlatformView::Delegate& delegate_; + flutter::TaskRunners task_runners_; + scenic::ViewRefPair view_ref_pair_; + + std::shared_ptr + external_external_view_embedder_; + fuchsia::ui::input::ImeServiceHandle ime_service_; + fuchsia::ui::input3::KeyboardHandle keyboard_; + fuchsia::ui::pointer::TouchSourceHandle touch_source_; + fuchsia::ui::pointer::MouseSourceHandle mouse_source_; + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused_; + fuchsia::ui::views::FocuserHandle focuser_; + fit::closure on_session_listener_error_callback_; + OnEnableWireframe wireframe_enabled_callback_; + fuchsia::ui::composition::ParentViewportWatcherHandle + parent_viewport_watcher_; + OnCreateFlatlandView on_create_view_callback_; + OnDestroyFlatlandView on_destroy_view_callback_; + OnUpdateView on_update_view_callback_; + OnCreateSurface on_create_surface_callback_; + OnSemanticsNodeUpdate on_semantics_node_update_callback_; + OnRequestAnnounce on_request_announce_callback_; + OnShaderWarmup on_shader_warmup_callback_; + + bool built_{false}; +}; + +std::string ToString(const fml::Mapping& mapping) { + return std::string(mapping.GetMapping(), + mapping.GetMapping() + mapping.GetSize()); +} + +// Stolen from pointer_data_packet_converter_unittests.cc. +void UnpackPointerPacket(std::vector& output, // NOLINT + std::unique_ptr packet) { + size_t kBytesPerPointerData = + flutter::kPointerDataFieldCount * flutter::kBytesPerField; + auto buffer = packet->data(); + size_t buffer_length = buffer.size(); + + for (size_t i = 0; i < buffer_length / kBytesPerPointerData; i++) { + flutter::PointerData pointer_data; + memcpy(&pointer_data, &buffer[i * kBytesPerPointerData], + sizeof(flutter::PointerData)); + output.push_back(pointer_data); + } + packet.reset(); +} + +} // namespace + +class FlatlandPlatformViewTests : public ::testing::Test { + protected: + FlatlandPlatformViewTests() : loop_(&kAsyncLoopConfigAttachToCurrentThread) {} + + async_dispatcher_t* dispatcher() { return loop_.dispatcher(); } + + void RunLoopUntilIdle() { + loop_.RunUntilIdle(); + loop_.ResetQuit(); + } + + void RunLoopOnce() { + loop_.Run(zx::time::infinite(), true); + loop_.ResetQuit(); + } + + fuchsia::ui::input3::KeyEvent MakeEvent( + fuchsia::ui::input3::KeyEventType event_type, + std::optional modifiers, + fuchsia::input::Key key) { + fuchsia::ui::input3::KeyEvent event; + event.set_timestamp(++event_timestamp_); + event.set_type(event_type); + if (modifiers.has_value()) { + event.set_modifiers(modifiers.value()); + } + event.set_key(key); + return event; + } + + fuchsia::ui::composition::ChildViewWatcherPtr MakeChildViewWatcher() { + fuchsia::ui::composition::ChildViewWatcherPtr ptr; + auto watcher = std::make_unique( + ptr.NewRequest(loop_.dispatcher())); + child_view_watchers_.push_back(std::move(watcher)); + return ptr; + } + + private: + async::Loop loop_; + + uint64_t event_timestamp_{42}; + + std::vector> child_view_watchers_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlatlandPlatformViewTests); +}; + +// This test makes sure that the PlatformView always completes a platform +// message request, even for error conditions or if the request is malformed. +TEST_F(FlatlandPlatformViewTests, InvalidPlatformMessageRequest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + FakeViewRefFocused vrf; + fidl::BindingSet vrf_bindings; + auto vrf_handle = vrf_bindings.AddBinding(&vrf); + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetViewRefFocused(std::move(vrf_handle)) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + // Invalid platform channel. + auto response1 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response1->WithMessage( + "flutter/invalid", "{\"method\":\"Invalid.invalidMethod\"}")); + + // Invalid json. + auto response2 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage( + response2->WithMessage("flutter/platform_views", "{Invalid JSON")); + + // Invalid method. + auto response3 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response3->WithMessage( + "flutter/platform_views", "{\"method\":\"View.focus.invalidMethod\"}")); + + // Missing arguments. + auto response4 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response4->WithMessage( + "flutter/platform_views", "{\"method\":\"View.update\"}")); + auto response5 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage( + response5->WithMessage("flutter/platform_views", + "{\"method\":\"View.update\",\"args\":{" + "\"irrelevantField\":\"irrelevantValue\"}}")); + + // Wrong argument types. + auto response6 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response6->WithMessage( + "flutter/platform_views", + "{\"method\":\"View.update\",\"args\":{\"viewId\":false,\"hitTestable\":" + "123,\"focusable\":\"yes\"}}")); + + // Run the event loop and check our responses. + RunLoopUntilIdle(); + response1->ExpectCompleted(""); + response2->ExpectCompleted(""); + response3->ExpectCompleted(""); + response4->ExpectCompleted(""); + response5->ExpectCompleted(""); + response6->ExpectCompleted(""); +} + +// This test makes sure that the PlatformView correctly returns a Surface +// instance that can surface the provided gr_context and external_view_embedder. +TEST_F(FlatlandPlatformViewTests, CreateSurfaceTest) { + MockPlatformViewDelegate delegate; + + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + nullptr, // platform + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // raster + nullptr, // ui + nullptr // io + ); + + // Test create surface callback function. + sk_sp gr_context = GrDirectContext::MakeMock( + nullptr, + flutter::MakeDefaultContextOptions(flutter::ContextType::kRender)); + std::shared_ptr external_view_embedder = + std::make_shared(); + auto CreateSurfaceCallback = [&external_view_embedder, gr_context]() { + return std::make_unique( + "PlatformViewTest", external_view_embedder, gr_context.get()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetCreateSurfaceCallback(CreateSurfaceCallback) + .SetExternalViewEmbedder(external_view_embedder) + .Build(); + platform_view.NotifyCreated(); + + RunLoopUntilIdle(); + + EXPECT_EQ(gr_context.get(), delegate.surface()->GetContext()); + EXPECT_EQ(external_view_embedder.get(), + platform_view.CreateExternalViewEmbedder().get()); +} + +// This test makes sure that the PlatformView correctly registers Scenic +// MetricsEvents sent to it via FIDL, correctly parses the metrics it receives, +// and calls the SetViewportMetrics callback with the appropriate parameters. +TEST_F(FlatlandPlatformViewTests, SetViewportMetrics) { + constexpr uint32_t width = 640; + constexpr uint32_t height = 480; + + MockPlatformViewDelegate delegate; + EXPECT_EQ(delegate.metrics(), flutter::ViewportMetrics()); + + MockParentViewportWatcher watcher; + std::vector events; + flutter::TaskRunners task_runners("test_runners", nullptr, nullptr, nullptr, + nullptr); + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetParentViewportWatcher(watcher.GetHandle()) + .Build(); + RunLoopUntilIdle(); + EXPECT_EQ(delegate.metrics(), flutter::ViewportMetrics()); + + watcher.SetLayout(width, height); + RunLoopUntilIdle(); + EXPECT_EQ(delegate.metrics(), + flutter::ViewportMetrics(1.0, width, height, -1.0)); +} + +// This test makes sure that the PlatformView correctly registers semantics +// settings changes applied to it and calls the SetSemanticsEnabled / +// SetAccessibilityFeatures callbacks with the appropriate parameters. +TEST_F(FlatlandPlatformViewTests, ChangesAccessibilitySettings) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + EXPECT_FALSE(delegate.semantics_enabled()); + EXPECT_EQ(delegate.semantics_features(), 0); + + auto platform_view = + PlatformViewBuilder(delegate, std::move(task_runners)).Build(); + + RunLoopUntilIdle(); + + platform_view.SetSemanticsEnabled(true); + + EXPECT_TRUE(delegate.semantics_enabled()); + EXPECT_EQ(delegate.semantics_features(), + static_cast( + flutter::AccessibilityFeatureFlag::kAccessibleNavigation)); + + platform_view.SetSemanticsEnabled(false); + + EXPECT_FALSE(delegate.semantics_enabled()); + EXPECT_EQ(delegate.semantics_features(), 0); +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for EnableWireframe. +TEST_F(FlatlandPlatformViewTests, EnableWireframeTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + // Test wireframe callback function. If the message sent to the platform + // view was properly handled and parsed, this function should be called, + // setting |wireframe_enabled| to true. + bool wireframe_enabled = false; + auto EnableWireframeCallback = [&wireframe_enabled](bool should_enable) { + wireframe_enabled = should_enable; + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetEnableWireframeCallback(EnableWireframeCallback) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + const uint8_t txt[] = + "{" + " \"method\":\"View.enableWireframe\"," + " \"args\": {" + " \"enable\":true" + " }" + "}"; + + std::unique_ptr message = + std::make_unique( + "flutter/platform_views", fml::MallocMapping::Copy(txt, sizeof(txt)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message)); + + RunLoopUntilIdle(); + + EXPECT_TRUE(wireframe_enabled); +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for Createview. +TEST_F(FlatlandPlatformViewTests, CreateViewTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // platform + nullptr, // raster + nullptr, // ui + nullptr // io + ); + + // Test wireframe callback function. If the message sent to the platform + // view was properly handled and parsed, this function should be called, + // setting |wireframe_enabled| to true. + bool create_view_called = false; + auto CreateViewCallback = + [&create_view_called, this]( + int64_t view_id, flutter_runner::ViewCallback on_view_created, + flutter_runner::FlatlandViewCreatedCallback on_view_bound, + bool hit_testable, bool focusable) { + create_view_called = true; + on_view_created(); + fuchsia::ui::composition::ContentId content_id; + on_view_bound(std::move(content_id), MakeChildViewWatcher()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetCreateViewCallback(CreateViewCallback) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + // JSON for the message to be passed into the PlatformView. + const uint8_t txt[] = + "{" + " \"method\":\"View.create\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":true," + " \"focusable\":true" + " }" + "}"; + + std::unique_ptr message = + std::make_unique( + "flutter/platform_views", fml::MallocMapping::Copy(txt, sizeof(txt)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message)); + + RunLoopUntilIdle(); + + EXPECT_TRUE(create_view_called); +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for UpdateView. +TEST_F(FlatlandPlatformViewTests, UpdateViewTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + std::optional occlusion_hint_for_test; + std::optional hit_testable_for_test; + std::optional focusable_for_test; + auto UpdateViewCallback = [&occlusion_hint_for_test, &hit_testable_for_test, + &focusable_for_test]( + int64_t view_id, SkRect occlusion_hint, + bool hit_testable, bool focusable) { + occlusion_hint_for_test = occlusion_hint; + hit_testable_for_test = hit_testable; + focusable_for_test = focusable; + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetUpdateViewCallback(UpdateViewCallback) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + // Send a basic message. + const uint8_t json[] = + "{" + " \"method\":\"View.update\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":true," + " \"focusable\":true" + " }" + "}"; + std::unique_ptr message = + std::make_unique( + "flutter/platform_views", + fml::MallocMapping::Copy(json, sizeof(json)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message)); + + RunLoopUntilIdle(); + ASSERT_TRUE(occlusion_hint_for_test.has_value()); + ASSERT_TRUE(hit_testable_for_test.has_value()); + ASSERT_TRUE(focusable_for_test.has_value()); + EXPECT_EQ(occlusion_hint_for_test.value(), SkRect::MakeEmpty()); + EXPECT_EQ(hit_testable_for_test.value(), true); + EXPECT_EQ(focusable_for_test.value(), true); + + // Reset for the next message. + occlusion_hint_for_test.reset(); + hit_testable_for_test.reset(); + focusable_for_test.reset(); + + // Send another basic message. + const uint8_t json_false[] = + "{" + " \"method\":\"View.update\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":false," + " \"focusable\":false" + " }" + "}"; + std::unique_ptr message_false = + std::make_unique( + "flutter/platform_views", + fml::MallocMapping::Copy(json_false, sizeof(json_false)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message_false)); + RunLoopUntilIdle(); + ASSERT_TRUE(occlusion_hint_for_test.has_value()); + ASSERT_TRUE(hit_testable_for_test.has_value()); + ASSERT_TRUE(focusable_for_test.has_value()); + EXPECT_EQ(occlusion_hint_for_test.value(), SkRect::MakeEmpty()); + EXPECT_EQ(hit_testable_for_test.value(), false); + EXPECT_EQ(focusable_for_test.value(), false); + + // Reset for the next message. + occlusion_hint_for_test.reset(); + hit_testable_for_test.reset(); + focusable_for_test.reset(); + + // Send a message including an occlusion hint. + const uint8_t json_occlusion_hint[] = + "{" + " \"method\":\"View.update\"," + " \"args\": {" + " \"viewId\":42," + " \"hitTestable\":true," + " \"focusable\":true," + " \"viewOcclusionHintLTRB\":[0.1,0.2,0.3,0.4]" + " }" + "}"; + std::unique_ptr message_occlusion_hint = + std::make_unique( + "flutter/platform_views", + fml::MallocMapping::Copy(json_occlusion_hint, + sizeof(json_occlusion_hint)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message_occlusion_hint)); + RunLoopUntilIdle(); + ASSERT_TRUE(occlusion_hint_for_test.has_value()); + ASSERT_TRUE(hit_testable_for_test.has_value()); + ASSERT_TRUE(focusable_for_test.has_value()); + EXPECT_EQ(occlusion_hint_for_test.value(), + SkRect::MakeLTRB(0.1, 0.2, 0.3, 0.4)); + EXPECT_EQ(hit_testable_for_test.value(), true); + EXPECT_EQ(focusable_for_test.value(), true); +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for DestroyView. +TEST_F(FlatlandPlatformViewTests, DestroyViewTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // platform + nullptr, // raster + nullptr, // ui + nullptr // io + ); + + bool destroy_view_called = false; + + auto on_destroy_view = + [&destroy_view_called]( + int64_t view_id, + flutter_runner::FlatlandViewIdCallback on_view_unbound) { + destroy_view_called = true; + fuchsia::ui::composition::ContentId content_id; + on_view_unbound(std::move(content_id)); + }; + + bool create_view_called = false; + auto on_create_view = + [&create_view_called, this]( + int64_t view_id, flutter_runner::ViewCallback on_view_created, + flutter_runner::FlatlandViewCreatedCallback on_view_bound, + bool hit_testable, bool focusable) { + create_view_called = true; + on_view_created(); + fuchsia::ui::composition::ContentId content_id; + on_view_bound(std::move(content_id), MakeChildViewWatcher()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetCreateViewCallback(on_create_view) + .SetDestroyViewCallback(on_destroy_view) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + std::ostringstream create_message; + create_message << "{" + << " \"method\":\"View.create\"," + << " \"args\": {" + << " \"viewId\":42," + << " \"hitTestable\":true," + << " \"focusable\":true" + << " }" + << "}"; + + auto create_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(create_response->WithMessage( + "flutter/platform_views", create_message.str())); + RunLoopUntilIdle(); + + // JSON for the message to be passed into the PlatformView. + const uint8_t txt[] = + "{" + " \"method\":\"View.dispose\"," + " \"args\": {" + " \"viewId\":42" + " }" + "}"; + + std::unique_ptr message = + std::make_unique( + "flutter/platform_views", fml::MallocMapping::Copy(txt, sizeof(txt)), + fml::RefPtr()); + base_view->HandlePlatformMessage(std::move(message)); + + RunLoopUntilIdle(); + + EXPECT_TRUE(destroy_view_called); +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for View.focus.getCurrent and +// View.focus.getNext. +TEST_F(FlatlandPlatformViewTests, GetFocusStatesTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + FakeViewRefFocused vrf; + fidl::BindingSet vrf_bindings; + auto vrf_handle = vrf_bindings.AddBinding(&vrf); + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetViewRefFocused(std::move(vrf_handle)) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + std::vector vrf_states{false, true, true, false, + true, false, true, true}; + + for (std::size_t i = 0; i < vrf_states.size(); ++i) { + // View.focus.getNext should complete with the next focus state. + auto response1 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response1->WithMessage( + "flutter/platform_views", "{\"method\":\"View.focus.getNext\"}")); + // Duplicate View.focus.getNext requests should complete empty. + auto response2 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response2->WithMessage( + "flutter/platform_views", "{\"method\":\"View.focus.getNext\"}")); + + // Post watch events and make sure the hanging get is invoked each time. + RunLoopUntilIdle(); + EXPECT_EQ(vrf.times_watched, i + 1); + + // Dispatch the next vrf event. + vrf.ScheduleCallback(vrf_states[i]); + RunLoopUntilIdle(); + + // Make sure View.focus.getCurrent completes with the current focus state. + auto response3 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response3->WithMessage( + "flutter/platform_views", "{\"method\":\"View.focus.getCurrent\"}")); + // Duplicate View.focus.getCurrent are allowed. + auto response4 = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(response4->WithMessage( + "flutter/platform_views", "{\"method\":\"View.focus.getCurrent\"}")); + + // Run event loop and check our results. + RunLoopUntilIdle(); + response1->ExpectCompleted(vrf_states[i] ? "[true]" : "[false]"); + response2->ExpectCompleted("[null]"); + response3->ExpectCompleted(vrf_states[i] ? "[true]" : "[false]"); + response4->ExpectCompleted(vrf_states[i] ? "[true]" : "[false]"); + } +} + +// This test makes sure that the PlatformView forwards messages on the +// "flutter/platform_views" channel for View.focus.request. +TEST_F(FlatlandPlatformViewTests, RequestFocusTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // platform + nullptr, // raster + nullptr, // ui + nullptr // io + ); + + FakeFocuser focuser; + fidl::BindingSet focuser_bindings; + auto focuser_handle = focuser_bindings.AddBinding(&focuser); + + bool create_view_called = false; + auto on_create_view = + [&create_view_called, this]( + int64_t view_id, flutter_runner::ViewCallback on_view_created, + flutter_runner::FlatlandViewCreatedCallback on_view_bound, + bool hit_testable, bool focusable) { + create_view_called = true; + on_view_created(); + fuchsia::ui::composition::ContentId content_id; + on_view_bound(std::move(content_id), MakeChildViewWatcher()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetFocuser(std::move(focuser_handle)) + .SetCreateViewCallback(on_create_view) + .Build(); + + // Cast platform_view to its base view so we can have access to the public + // "HandlePlatformMessage" function. + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + uint64_t view_id = 42; + + std::ostringstream create_message; + create_message << "{" + << " \"method\":\"View.create\"," + << " \"args\": {" + << " \"viewId\":" << view_id << "," + << " \"hitTestable\":true," + << " \"focusable\":true" + << " }" + << "}"; + + // Dispatch the plaform message request. + auto create_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(create_response->WithMessage( + "flutter/platform_views", create_message.str())); + + RunLoopUntilIdle(); + + // JSON for the message to be passed into the PlatformView. + std::ostringstream focus_message; + focus_message << "{" + << " \"method\":\"View.focus.requestById\"," + << " \"args\": {" + << " \"viewId\":" << view_id << " }" + << "}"; + + // Dispatch the plaform message request. + auto focus_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(focus_response->WithMessage( + "flutter/platform_views", focus_message.str())); + RunLoopUntilIdle(); + + focus_response->ExpectCompleted("[0]"); + EXPECT_TRUE(focuser.request_focus_called()); +} + +// This test tries to set focus on a view without creating it first +TEST_F(FlatlandPlatformViewTests, RequestFocusNeverCreatedTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // platform + nullptr, // raster + nullptr, // ui + nullptr // io + ); + + FakeFocuser focuser; + fidl::BindingSet focuser_bindings; + auto focuser_handle = focuser_bindings.AddBinding(&focuser); + + bool create_view_called = false; + auto on_create_view = + [&create_view_called, this]( + int64_t view_id, flutter_runner::ViewCallback on_view_created, + flutter_runner::FlatlandViewCreatedCallback on_view_bound, + bool hit_testable, bool focusable) { + create_view_called = true; + on_view_created(); + fuchsia::ui::composition::ContentId content_id; + on_view_bound(std::move(content_id), MakeChildViewWatcher()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetFocuser(std::move(focuser_handle)) + .SetCreateViewCallback(on_create_view) + .Build(); + + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + uint64_t view_id = 42; + + std::ostringstream focus_message; + focus_message << "{" + << " \"method\":\"View.focus.requestById\"," + << " \"args\": {" + << " \"viewId\":" << view_id << " }" + << "}"; + + // Dispatch the plaform message request. + auto focus_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(focus_response->WithMessage( + "flutter/platform_views", focus_message.str())); + RunLoopUntilIdle(); + + focus_response->ExpectCompleted("[1]"); + EXPECT_FALSE(focuser.request_focus_called()); +} + +TEST_F(FlatlandPlatformViewTests, RequestFocusDisposedTest) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", // label + flutter_runner::CreateFMLTaskRunner( + async_get_default_dispatcher()), // platform + nullptr, // raster + nullptr, // ui + nullptr // io + ); + + FakeFocuser focuser; + fidl::BindingSet focuser_bindings; + auto focuser_handle = focuser_bindings.AddBinding(&focuser); + + bool create_view_called = false; + auto on_create_view = + [&create_view_called, this]( + int64_t view_id, flutter_runner::ViewCallback on_view_created, + flutter_runner::FlatlandViewCreatedCallback on_view_bound, + bool hit_testable, bool focusable) { + create_view_called = true; + on_view_created(); + fuchsia::ui::composition::ContentId content_id; + on_view_bound(std::move(content_id), MakeChildViewWatcher()); + }; + + bool destroy_view_called = false; + + auto on_destroy_view = + [&destroy_view_called]( + int64_t view_id, + flutter_runner::FlatlandViewIdCallback on_view_unbound) { + destroy_view_called = true; + fuchsia::ui::composition::ContentId content_id; + on_view_unbound(std::move(content_id)); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetFocuser(std::move(focuser_handle)) + .SetCreateViewCallback(on_create_view) + .SetDestroyViewCallback(on_destroy_view) + .Build(); + + auto base_view = static_cast(&platform_view); + EXPECT_TRUE(base_view); + + uint64_t view_id = 42; + + // Create a new view + std::ostringstream create_message; + create_message << "{" + << " \"method\":\"View.create\"," + << " \"args\": {" + << " \"viewId\":" << view_id << "," + << " \"hitTestable\":true," + << " \"focusable\":true" + << " }" + << "}"; + + auto create_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(create_response->WithMessage( + "flutter/platform_views", create_message.str())); + RunLoopUntilIdle(); + + EXPECT_FALSE(destroy_view_called); + // Dispose of the view + std::ostringstream dispose_message; + dispose_message << "{" + << " \"method\":\"View.dispose\"," + << " \"args\": {" + << " \"viewId\":" << view_id << " }" + << "}"; + + auto dispose_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(dispose_response->WithMessage( + "flutter/platform_views", dispose_message.str())); + RunLoopUntilIdle(); + EXPECT_TRUE(destroy_view_called); + + // Request focus on newly disposed view + std::ostringstream focus_message; + focus_message << "{" + << " \"method\":\"View.focus.requestById\"," + << " \"args\": {" + << " \"viewId\":" << view_id << " }" + << "}"; + + auto focus_response = FakePlatformMessageResponse::Create(); + base_view->HandlePlatformMessage(focus_response->WithMessage( + "flutter/platform_views", focus_message.str())); + RunLoopUntilIdle(); + + // Expect it to fail + focus_response->ExpectCompleted("[1]"); + EXPECT_FALSE(focuser.request_focus_called()); +} + +// Makes sure that OnKeyEvent is dispatched as a platform message. +TEST_F(FlatlandPlatformViewTests, OnKeyEvent) { + struct EventFlow { + fuchsia::ui::input3::KeyEvent event; + fuchsia::ui::input3::KeyEventStatus expected_key_event_status; + std::string expected_platform_message; + }; + + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + fuchsia::ui::input3::KeyboardHandle keyboard_service; + MockKeyboard keyboard(keyboard_service.NewRequest()); + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetKeyboard(std::move(keyboard_service)) + .Build(); + RunLoopUntilIdle(); + + std::vector events; + // Press A. Get 'a'. + // The HID usage for the key A is 0x70004, or 458756. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::PRESSED, std::nullopt, + fuchsia::input::Key::A), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":97,"modifiers":0})", + }); + // Release A. Get 'a' release. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::RELEASED, std::nullopt, + fuchsia::input::Key::A), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keyup","keymap":"fuchsia","hidUsage":458756,"codePoint":97,"modifiers":0})", + }); + // Press CAPS_LOCK. Modifier now active. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::PRESSED, + fuchsia::ui::input3::Modifiers::CAPS_LOCK, + fuchsia::input::Key::CAPS_LOCK), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":458809,"codePoint":0,"modifiers":1})", + }); + // Press A. Get 'A'. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::PRESSED, std::nullopt, + fuchsia::input::Key::A), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":65,"modifiers":1})", + }); + // Release CAPS_LOCK. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::RELEASED, + fuchsia::ui::input3::Modifiers::CAPS_LOCK, + fuchsia::input::Key::CAPS_LOCK), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keyup","keymap":"fuchsia","hidUsage":458809,"codePoint":0,"modifiers":1})", + }); + // Press A again. This time get 'A'. + // CAPS_LOCK is latched active even if it was just released. + events.emplace_back(EventFlow{ + MakeEvent(fuchsia::ui::input3::KeyEventType::PRESSED, std::nullopt, + fuchsia::input::Key::A), + fuchsia::ui::input3::KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":65,"modifiers":1})", + }); + + for (const auto& event : events) { + fuchsia::ui::input3::KeyEvent e; + event.event.Clone(&e); + fuchsia::ui::input3::KeyEventStatus key_event_status{0u}; + keyboard.listener_->OnKeyEvent( + std::move(e), + [&key_event_status](fuchsia::ui::input3::KeyEventStatus status) { + key_event_status = status; + }); + RunLoopUntilIdle(); + + ASSERT_NOTNULL(delegate.message()); + EXPECT_EQ(event.expected_platform_message, + ToString(delegate.message()->data())); + EXPECT_EQ(event.expected_key_event_status, key_event_status); + } +} + +TEST_F(FlatlandPlatformViewTests, OnShaderWarmup) { + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + uint64_t width = 200; + uint64_t height = 100; + std::vector shaders = {"foo.skp", "bar.skp", "baz.skp"}; + + OnShaderWarmup on_shader_warmup = + [&](const std::vector& shaders_in, + std::function completion_callback, uint64_t width_in, + uint64_t height_in) { + ASSERT_EQ(shaders.size(), shaders_in.size()); + for (size_t i = 0; i < shaders_in.size(); i++) { + ASSERT_EQ(shaders[i], shaders_in[i]); + } + ASSERT_EQ(width, width_in); + ASSERT_EQ(height, height_in); + + completion_callback(shaders_in.size()); + }; + + auto platform_view = PlatformViewBuilder(delegate, std::move(task_runners)) + .SetShaderWarmupCallback(on_shader_warmup) + .Build(); + + std::ostringstream shaders_array_ostream; + shaders_array_ostream << "[ "; + for (auto it = shaders.begin(); it != shaders.end(); ++it) { + shaders_array_ostream << "\"" << *it << "\""; + if (std::next(it) != shaders.end()) { + shaders_array_ostream << ", "; + } + } + shaders_array_ostream << "]"; + + std::string shaders_array_string = shaders_array_ostream.str(); + + // Create initial view for testing. + std::ostringstream warmup_shaders_ostream; + warmup_shaders_ostream << "{" + << " \"method\":\"WarmupSkps\"," + << " \"args\":{" + << " \"shaders\":" << shaders_array_string << "," + << " \"width\":" << width << "," + << " \"height\":" << height << " }" + << "}\n"; + std::string warmup_shaders_string = warmup_shaders_ostream.str(); + + fml::RefPtr response( + new TestPlatformMessageResponse); + static_cast(&platform_view) + ->HandlePlatformMessage(std::make_unique( + "fuchsia/shader_warmup", + fml::MallocMapping::Copy(warmup_shaders_string.c_str(), + warmup_shaders_string.size()), + response)); + RunLoopUntilIdle(); + ASSERT_TRUE(response->is_complete()); + + std::ostringstream expected_result_ostream; + expected_result_ostream << "[" << shaders.size() << "]"; + std::string expected_result_string = expected_result_ostream.str(); + EXPECT_EQ(expected_result_string, response->result_string); +} + +TEST_F(FlatlandPlatformViewTests, TouchSourceLogicalToPhysicalConversion) { + constexpr uint32_t width = 640; + constexpr uint32_t height = 480; + constexpr std::array, 2> kRect = { + {{0, 0}, {width, height}}}; + constexpr std::array kIdentity = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + constexpr fuchsia::ui::pointer::TouchInteractionId kIxnOne = { + .device_id = 0u, .pointer_id = 1u, .interaction_id = 2u}; + + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners("test_runners", nullptr, nullptr, nullptr, + nullptr); + + MockParentViewportWatcher viewport_watcher; + FakeTouchSource touch_server; + fidl::BindingSet touch_bindings; + auto touch_handle = touch_bindings.AddBinding(&touch_server); + auto platform_view = + PlatformViewBuilder(delegate, std::move(task_runners)) + .SetParentViewportWatcher(viewport_watcher.GetHandle()) + .SetTouchSource(std::move(touch_handle)) + .Build(); + RunLoopUntilIdle(); + EXPECT_EQ(delegate.pointer_packets().size(), 0u); + + viewport_watcher.SetLayout(width, height); + RunLoopUntilIdle(); + EXPECT_EQ(delegate.metrics(), flutter::ViewportMetrics(1, width, height, -1)); + + // Inject + std::vector events = + TouchEventBuilder::New() + .AddTime(/* in nanoseconds */ 1111789u) + .AddViewParameters(kRect, kRect, kIdentity) + .AddSample(kIxnOne, fuchsia::ui::pointer::EventPhase::ADD, + {width / 2, height / 2}) + .AddResult( + {.interaction = kIxnOne, + .status = fuchsia::ui::pointer::TouchInteractionStatus::GRANTED}) + .BuildAsVector(); + touch_server.ScheduleCallback(std::move(events)); + RunLoopUntilIdle(); + + // Unpack + std::vector> packets = + delegate.TakePointerDataPackets(); + ASSERT_EQ(packets.size(), 1u); + std::vector flutter_events; + UnpackPointerPacket(flutter_events, std::move(packets[0])); + + // Examine phases + ASSERT_EQ(flutter_events.size(), 2u); + EXPECT_EQ(flutter_events[0].change, flutter::PointerData::Change::kAdd); + EXPECT_EQ(flutter_events[1].change, flutter::PointerData::Change::kDown); + + // Examine coordinates + EXPECT_EQ(flutter_events[0].physical_x, width / 2); + EXPECT_EQ(flutter_events[0].physical_y, height / 2); + EXPECT_EQ(flutter_events[1].physical_x, width / 2); + EXPECT_EQ(flutter_events[1].physical_y, height / 2); +} + +} // namespace flutter_runner::testing