chromium/ui/ozone/platform/flatland/flatland_window_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/ozone/platform/flatland/flatland_window.h"

#include <fidl/fuchsia.ui.pointer/cpp/fidl.h>
#include <fidl/fuchsia.ui.pointer/cpp/hlcpp_conversion.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/ui/scenic/cpp/testing/fake_flatland.h>
#include <lib/ui/scenic/cpp/testing/fake_touch_source.h>
#include <lib/ui/scenic/cpp/testing/fake_view_ref_focused.h>
#include <lib/zx/channel.h>

#include <memory>
#include <string>

#include "base/fuchsia/koid.h"
#include "base/fuchsia/scoped_service_publisher.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/logging.h"
#include "base/test/fidl_matchers.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/fuchsia/util/pointer_event_utility.h"
#include "ui/ozone/platform/flatland/flatland_window_manager.h"
#include "ui/ozone/test/mock_platform_window_delegate.h"
#include "ui/platform_window/fuchsia/view_ref_pair.h"

using ::scenic::FakeGraph;
using ::scenic::FakeTransform;
using ::scenic::FakeTransformPtr;
using ::scenic::FakeView;
using ::scenic::FakeViewport;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::Property;
using ::testing::SaveArg;
using ::testing::VariantWith;

namespace ui {

namespace {

Matcher<FakeGraph> IsWindowGraph(
    const fuchsia::ui::composition::ParentViewportWatcherPtr&
        parent_viewport_watcher,
    const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token,
    Matcher<std::vector<FakeTransformPtr>> children_transform_matcher) {
  std::optional<zx_koid_t> view_token_koid =
      base::GetRelatedKoid(viewport_creation_token.value);
  EXPECT_TRUE(view_token_koid.has_value());
  std::optional<zx_koid_t> watcher_koid =
      base::GetRelatedKoid(parent_viewport_watcher.channel());
  EXPECT_TRUE(watcher_koid.has_value());

  return AllOf(
      Field("root_transform", &FakeGraph::root_transform,
            Pointee(AllOf(
                Field("translation", &FakeTransform::translation,
                      ::base::test::FidlEq(FakeTransform::kDefaultTranslation)),
                Field("scale", &FakeTransform::scale,
                      ::base::test::FidlEq(FakeTransform::kDefaultScale)),
                Field("opacity", &FakeTransform::opacity,
                      FakeTransform::kDefaultOpacity),
                Field("children", &FakeTransform::children,
                      children_transform_matcher)))),
      Field("view", &FakeGraph::view,
            Optional(AllOf(
                Field("view_token", &FakeView::view_token, view_token_koid),
                Field("parent_viewport_watcher",
                      &FakeView::parent_viewport_watcher, watcher_koid)))));
}

Matcher<fuchsia::ui::composition::ViewportProperties> IsViewportProperties(
    const fuchsia::math::SizeU& logical_size) {
  return AllOf(
      Property("has_logical_size",
               &fuchsia::ui::composition::ViewportProperties::has_logical_size,
               true),
      Property("logical_size",
               &fuchsia::ui::composition::ViewportProperties::logical_size,
               ::base::test::FidlEq(logical_size)));
}

Matcher<FakeTransformPtr> IsViewport(
    const fuchsia::ui::views::ViewCreationToken& view_token,
    const fuchsia::math::SizeU& viewport_logical_size) {
  auto viewport_koid = base::GetRelatedKoid(view_token.value);

  return Pointee(AllOf(
      Field("translation", &FakeTransform::translation,
            ::base::test::FidlEq(FakeTransform::kDefaultTranslation)),
      Field("scale", &FakeTransform::scale,
            ::base::test::FidlEq(FakeTransform::kDefaultScale)),
      Field("opacity", &FakeTransform::opacity, FakeTransform::kDefaultOpacity),
      Field("children", &FakeTransform::children, IsEmpty()),
      Field("content", &FakeTransform::content,
            Pointee(VariantWith<FakeViewport>(AllOf(
                Field("viewport_properties", &FakeViewport::viewport_properties,
                      IsViewportProperties(viewport_logical_size)),
                Field("viewport_token", &FakeViewport::viewport_token,
                      viewport_koid)))))));
}

Matcher<FakeTransformPtr> IsHitShield() {
  return Pointee(AllOf(
      // Must not clip the hit region.
      Field("clip_bounds", &FakeTransform::clip_bounds,
            testing::Eq(std::nullopt)),
      // Hit region must be "infinite".
      Field("hit_regions", &FakeTransform::hit_regions,
            testing::Contains(
                ::base::test::FidlEq(scenic::kInfiniteHitRegion)))));
}

}  // namespace

class FlatlandWindowTest : public ::testing::Test {
 protected:
  FlatlandWindowTest()
      : fake_flatland_publisher_(test_context_.additional_services(),
                                 fake_flatland_.GetFlatlandRequestHandler()) {}
  ~FlatlandWindowTest() override = default;

  void CreateWindow() {
    EXPECT_FALSE(flatland_window_);

    fuchsia::ui::views::ViewCreationToken view_token;
    fuchsia::ui::views::ViewportCreationToken viewport_token;
    auto status =
        zx::channel::create(0, &viewport_token.value, &view_token.value);
    CHECK_EQ(ZX_OK, status);
    viewport_token_ = std::move(viewport_token);

    EXPECT_CALL(window_delegate_, OnAcceleratedWidgetAvailable(_))
        .WillOnce(SaveArg<0>(&window_widget_));

    PlatformWindowInitProperties properties;
    properties.view_ref_pair = ViewRefPair::New();
    properties.view_creation_token = std::move(view_token);
    flatland_window_ = std::make_unique<FlatlandWindow>(
        &window_manager_, &window_delegate_, std::move(properties));
  }

  void SetLayoutInfo(uint32_t width,
                     uint32_t height,
                     float dpr,
                     fuchsia::math::Inset inset = {0, 0, 0, 0}) {
    fuchsia::ui::composition::LayoutInfo layout_info;
    layout_info.set_logical_size({width, height});
    layout_info.set_device_pixel_ratio({dpr, dpr});
    layout_info.set_inset(inset);
    flatland_window_->OnGetLayout(std::move(layout_info));
  }

  void SetWindowStatus(fuchsia::ui::composition::ParentViewportStatus status) {
    flatland_window_->OnGetStatus(status);
  }

  void SetViewRefFocusedHandle(
      fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused_handle) {
    flatland_window_->view_ref_focused_.Bind(
        std::move(view_ref_focused_handle));
    flatland_window_->view_ref_focused_->Watch(fit::bind_member(
        flatland_window_.get(), &FlatlandWindow::OnViewRefFocusedWatchResult));
  }

  void SetTouchSource(
      fidl::ClientEnd<fuchsia_ui_pointer::TouchSource> touch_source) {
    auto mouse_endpoints =
        fidl::CreateEndpoints<fuchsia_ui_pointer::MouseSource>();
    EXPECT_TRUE(mouse_endpoints.is_ok()) << mouse_endpoints.status_string();
    flatland_window_->pointer_handler_ = std::make_unique<PointerEventsHandler>(
        std::move(touch_source), std::move(mouse_endpoints->client));
    flatland_window_->pointer_handler_->StartWatching(base::BindRepeating(
        &FlatlandWindow::DispatchEvent,
        // This is safe since |flatland_window_| is a class member.
        base::Unretained(flatland_window_.get())));
  }

  bool HasPendingAttachSurfaceContentClosure() {
    return !!flatland_window_->pending_attach_surface_content_closure_;
  }

  const fuchsia::ui::composition::ParentViewportWatcherPtr&
  parent_viewport_watcher() {
    return flatland_window_->parent_viewport_watcher_;
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};
  scenic::FakeFlatland fake_flatland_;

  base::TestComponentContextForProcess test_context_;
  // Injects binding for responding to Flatland protocol connection requests.
  const base::ScopedServicePublisher<fuchsia::ui::composition::Flatland>
      fake_flatland_publisher_;

  FlatlandWindowManager window_manager_;

  MockPlatformWindowDelegate window_delegate_;
  std::unique_ptr<FlatlandWindow> flatland_window_;

  gfx::AcceleratedWidget window_widget_ = gfx::kNullAcceleratedWidget;

  fuchsia::ui::views::ViewportCreationToken viewport_token_;
};

TEST_F(FlatlandWindowTest, Initialization) {
  CreateWindow();
  ASSERT_NE(window_widget_, gfx::kNullAcceleratedWidget);

  // Check that there are no crashes after flushing tasks.
  task_environment_.RunUntilIdle();
}

TEST_F(FlatlandWindowTest, PresentsOnShow) {
  size_t presents_called = 0u;
  fake_flatland_.SetPresentHandler(
      [&presents_called](auto) { ++presents_called; });

  CreateWindow();
  ASSERT_NE(window_widget_, gfx::kNullAcceleratedWidget);

  task_environment_.RunUntilIdle();
  EXPECT_EQ(0u, presents_called);

  flatland_window_->Show(/*inactive=*/false);
  EXPECT_EQ(0u, presents_called);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, presents_called);
  EXPECT_THAT(
      fake_flatland_.graph(),
      IsWindowGraph(parent_viewport_watcher(), viewport_token_, IsEmpty()));
}

// Tests that FlatlandWindow processes and delegates focus signal.
TEST_F(FlatlandWindowTest, ProcessesFocusedSignal) {
  CreateWindow();
  flatland_window_->Show(/*inactive=*/false);
  task_environment_.RunUntilIdle();

  scenic::FakeViewRefFocused fake_view_ref_focused;
  fidl::Binding<fuchsia::ui::views::ViewRefFocused>
      fake_view_ref_focused_binding(&fake_view_ref_focused);
  SetViewRefFocusedHandle(fake_view_ref_focused_binding.NewBinding());

  // FlatlandWindow should start watching.
  task_environment_.RunUntilIdle();
  EXPECT_EQ(fake_view_ref_focused.times_watched(), 1u);

  // Send focused=true signal.
  bool focus_delegated = false;
  EXPECT_CALL(window_delegate_, OnActivationChanged(_))
      .WillRepeatedly(SaveArg<0>(&focus_delegated));
  fake_view_ref_focused.ScheduleCallback(true);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(fake_view_ref_focused.times_watched(), 2u);
  EXPECT_TRUE(focus_delegated);

  // Send focused=false signal.
  fake_view_ref_focused.ScheduleCallback(false);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(fake_view_ref_focused.times_watched(), 3u);
  EXPECT_FALSE(focus_delegated);
}

TEST_F(FlatlandWindowTest, AppliesDevicePixelRatio) {
  CreateWindow();
  EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1);
  SetLayoutInfo(100, 100, 1.f);

  scenic::FakeTouchSource fake_touch_source;
  fidl::Binding<fuchsia::ui::pointer::TouchSource> fake_touch_source_binding(
      &fake_touch_source);
  SetTouchSource(fidl::HLCPPToNatural(fake_touch_source_binding.NewBinding()));
  task_environment_.RunUntilIdle();

  // Send a touch event and expect coordinates to be the same as TouchEvent.
  const float kLocationX = 9.f;
  const float kLocationY = 10.f;
  bool event_received = false;
  EXPECT_CALL(window_delegate_, DispatchEvent(_))
      .WillOnce([&event_received, kLocationX, kLocationY](ui::Event* event) {
        EXPECT_EQ(event->AsTouchEvent()->location_f().x(), kLocationX);
        EXPECT_EQ(event->AsTouchEvent()->location_f().y(), kLocationY);
        event_received = true;
      });
  std::vector<fuchsia_ui_pointer::TouchEvent> events;
  events.push_back(TouchEventBuilder()
                       .SetPosition({kLocationX, kLocationY})
                       .SetTouchInteractionStatus(
                           fuchsia_ui_pointer::TouchInteractionStatus::kGranted)
                       .Build());
  fake_touch_source.ScheduleCallback(fidl::NaturalToHLCPP(std::move(events)));
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(event_received);

  // Update device pixel ratio.
  const float kDPR = 2.f;
  EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1);
  SetLayoutInfo(100, 100, kDPR);

  // Send the same touch event and expect coordinates to be scaled from
  // TouchEvent.
  event_received = false;
  EXPECT_CALL(window_delegate_, DispatchEvent(_))
      .WillOnce([&event_received, kLocationX, kLocationY,
                 kDPR](ui::Event* event) {
        EXPECT_EQ(event->AsTouchEvent()->location_f().x(), kLocationX * kDPR);
        EXPECT_EQ(event->AsTouchEvent()->location_f().y(), kLocationY * kDPR);
        event_received = true;
      });
  events.clear();
  events.push_back(TouchEventBuilder()
                       .SetPosition({kLocationX, kLocationY})
                       .SetTouchInteractionStatus(
                           fuchsia_ui_pointer::TouchInteractionStatus::kGranted)
                       .Build());
  fake_touch_source.ScheduleCallback(fidl::NaturalToHLCPP(std::move(events)));
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(event_received);
}

TEST_F(FlatlandWindowTest, WaitsForNonZeroSizeToAttachSurfaceContent) {
  size_t presents_called = 0u;
  fake_flatland_.SetPresentHandler(
      [&presents_called](auto) { ++presents_called; });

  CreateWindow();

  // FlatlandWindow should start watching callbacks in ctor.
  task_environment_.RunUntilIdle();

  // Try attaching the content. It should only be a closure.
  fuchsia::ui::views::ViewCreationToken view_token;
  fuchsia::ui::views::ViewportCreationToken viewport_token;
  auto status =
      zx::channel::create(0, &viewport_token.value, &view_token.value);
  CHECK_EQ(ZX_OK, status);
  flatland_window_->AttachSurfaceContent(std::move(viewport_token));
  EXPECT_TRUE(HasPendingAttachSurfaceContentClosure());

  // Setting layout info should trigger the closure and delegate calls.
  EXPECT_CALL(window_delegate_, OnWindowStateChanged(_, _)).Times(1);
  EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1);

  const uint32_t kWidth = 200;
  const uint32_t kHeight = 100;
  const fuchsia::math::SizeU expected_size = {kWidth, kHeight};

  SetWindowStatus(
      fuchsia::ui::composition::ParentViewportStatus::CONNECTED_TO_DISPLAY);
  SetLayoutInfo(kWidth, kHeight, 1.f);
  EXPECT_FALSE(HasPendingAttachSurfaceContentClosure());

  // There should be a present call in FakeFlatland after flushing the tasks.
  EXPECT_EQ(0u, presents_called);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, presents_called);

  // Show to attach the scene graph.
  flatland_window_->Show(/*inactive=*/false);
  fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values;
  on_next_frame_begin_values.set_additional_present_credits(1);
  fake_flatland_.FireOnNextFrameBeginEvent(
      std::move(on_next_frame_begin_values));

  // Spin the loop to process Present().
  task_environment_.RunUntilIdle();
  EXPECT_EQ(2u, presents_called);
  EXPECT_THAT(fake_flatland_.graph(),
              IsWindowGraph(parent_viewport_watcher(), viewport_token_,
                            Contains(IsViewport(view_token, expected_size))));
}

// Verify that surface is cleared when the window is disconnected from the
// display.
TEST_F(FlatlandWindowTest, ResetSurfaceOnDisconnect) {
  CreateWindow();

  EXPECT_CALL(window_delegate_, OnBoundsChanged(_));
  SetLayoutInfo(100, 100, 1.f);
  task_environment_.RunUntilIdle();

  // Try attaching the content. It should only be a closure.
  fuchsia::ui::views::ViewCreationToken view_token;
  fuchsia::ui::views::ViewportCreationToken viewport_token;
  auto status =
      zx::channel::create(0, &viewport_token.value, &view_token.value);
  CHECK_EQ(ZX_OK, status);
  flatland_window_->AttachSurfaceContent(std::move(viewport_token));

  SetWindowStatus(
      fuchsia::ui::composition::ParentViewportStatus::CONNECTED_TO_DISPLAY);

  // Show to attach the scene graph.
  flatland_window_->Show(/*inactive=*/false);
  fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values;
  on_next_frame_begin_values.set_additional_present_credits(1);
  fake_flatland_.FireOnNextFrameBeginEvent(
      std::move(on_next_frame_begin_values));

  // Spin the loop to process Present().
  task_environment_.RunUntilIdle();

  // surface view should be attached once the window is shown.
  EXPECT_THAT(fake_flatland_.graph(),
              IsWindowGraph(parent_viewport_watcher(), viewport_token_, _));

  // Remove the window from the screen and verify that it simulates destruction
  // of AcceleratedWidget, which is necessary to ensure that WindowSurface is
  // re-initialized.
  testing::Mock::VerifyAndClearExpectations(&window_delegate_);
  EXPECT_CALL(window_delegate_, OnAcceleratedWidgetDestroyed());
  EXPECT_CALL(window_delegate_, OnAcceleratedWidgetAvailable(window_widget_));

  SetWindowStatus(fuchsia::ui::composition::ParentViewportStatus::
                      DISCONNECTED_FROM_DISPLAY);

  on_next_frame_begin_values.set_additional_present_credits(1);
  fake_flatland_.FireOnNextFrameBeginEvent(
      std::move(on_next_frame_begin_values));

  // Spin the loop to process Present().
  task_environment_.RunUntilIdle();

  // Verify that the surface view is cleared.
  EXPECT_THAT(
      fake_flatland_.graph(),
      IsWindowGraph(parent_viewport_watcher(), viewport_token_, IsEmpty()));
}

// Verify that when surface is attached, a hit region accompanies the surface.
TEST_F(FlatlandWindowTest, SurfaceHasHitTestHitShield) {
  CreateWindow();

  EXPECT_CALL(window_delegate_, OnBoundsChanged(_));
  const uint32_t kWidth = 200;
  const uint32_t kHeight = 100;
  const fuchsia::math::SizeU expected_size = {kWidth, kHeight};
  SetLayoutInfo(kWidth, kHeight, 1.f);

  // Spin the loop to propagate layout.
  task_environment_.RunUntilIdle();

  fuchsia::ui::views::ViewCreationToken view_token;
  fuchsia::ui::views::ViewportCreationToken viewport_token;
  auto status =
      zx::channel::create(0, &viewport_token.value, &view_token.value);
  CHECK_EQ(ZX_OK, status);
  flatland_window_->AttachSurfaceContent(std::move(viewport_token));

  // Show() the window, to trigger creation of the scene graph, including
  // surface and hit shield.
  flatland_window_->Show(/*inactive=*/false);
  fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values;
  on_next_frame_begin_values.set_additional_present_credits(1);
  fake_flatland_.FireOnNextFrameBeginEvent(
      std::move(on_next_frame_begin_values));

  // Spin the loop to process Present().
  task_environment_.RunUntilIdle();

  // Surface should be accompanied by input shield, in that order.
  EXPECT_THAT(
      fake_flatland_.graph(),
      Field("root_transform", &FakeGraph::root_transform,
            Pointee(Field("children", &FakeTransform::children,
                          ElementsAre(IsViewport(view_token, expected_size),
                                      IsHitShield())))));
}

class ParameterizedViewInsetTest : public FlatlandWindowTest,
                                   public testing::WithParamInterface<float> {};

INSTANTIATE_TEST_SUITE_P(ViewInsetTest,
                         ParameterizedViewInsetTest,
                         testing::Values(1.f, 2.f, 3.f));

// Tests whether view insets are properly set in |FlatlandWindow|.
TEST_P(ParameterizedViewInsetTest, ViewInsetsTest) {
  CreateWindow();
  EXPECT_CALL(window_delegate_, OnBoundsChanged(_)).Times(1);
  SetLayoutInfo(100, 100, 1.f);

  const fuchsia::math::Inset inset = {1, 1, 1, 1};
  const float dpr = GetParam();

  // Setting LayoutInfo should trigger a change in the bounds.
  PlatformWindowDelegate::BoundsChange bounds(false);
  EXPECT_CALL(window_delegate_, OnBoundsChanged(_))
      .WillOnce(SaveArg<0>(&bounds));
  SetLayoutInfo(100, 100, dpr, inset);

  EXPECT_EQ(bounds.system_ui_overlap.top(), dpr * inset.top);
  EXPECT_EQ(bounds.system_ui_overlap.left(), dpr * inset.left);
  EXPECT_EQ(bounds.system_ui_overlap.bottom(), dpr * inset.bottom);
  EXPECT_EQ(bounds.system_ui_overlap.right(), dpr * inset.right);
}

}  // namespace ui