chromium/ash/annotator/annotator_controller_unittest.cc

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

#include "ash/annotator/annotator_controller.h"

#include "ash/annotator/annotation_tray.h"
#include "ash/annotator/annotator_metrics.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/projector/projector_metrics.h"
#include "ash/public/cpp/annotator/annotator_tool.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/tray/tray_container.h"
#include "ash/test/ash_test_base.h"
#include "ash/webui/annotator/test/mock_annotator_client.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/controls/image_view.h"

namespace ash {

namespace {

constexpr char kAnnotatorMarkerColorHistogramName[] =
    "Ash.Projector.MarkerColor.ClamshellMode";
constexpr char kProjectorToolbarHistogramName[] =
    "Ash.Projector.Toolbar.ClamshellMode";

}  // namespace

class AnnotatorControllerTest : public AshTestBase {
 public:
  AnnotatorControllerTest() = default;

  AnnotatorControllerTest(const AnnotatorControllerTest&) = delete;
  AnnotatorControllerTest& operator=(const AnnotatorControllerTest&) = delete;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    auto* annotator_controller = Shell::Get()->annotator_controller();
    annotator_controller->SetToolClient(&client_);
  }

  AnnotatorController* annotator_controller() {
    return Shell::Get()->annotator_controller();
  }

 protected:
  MockAnnotatorClient client_;
};

TEST_F(AnnotatorControllerTest, SetAnnotatorTool) {
  base::HistogramTester histogram_tester;
  AnnotatorTool tool;
  tool.color = kAnnotatorDefaultPenColor;
  EXPECT_CALL(client_, SetTool(tool));

  annotator_controller()->SetAnnotatorTool(tool);
  histogram_tester.ExpectUniqueSample(kAnnotatorMarkerColorHistogramName,
                                      AnnotatorMarkerColor::kMagenta,
                                      /*count=*/1);
}

TEST_F(AnnotatorControllerTest, RegisterAndUnregisterView) {
  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  EXPECT_TRUE(annotation_tray->visible_preferred());
  annotator_controller()->UnregisterView(Shell::GetPrimaryRootWindow());
  EXPECT_FALSE(annotation_tray->visible_preferred());
}

TEST_F(AnnotatorControllerTest, RegisterAndUnregisterViewMultipleDisplays) {
  UpdateDisplay("800x700,801+0-800x700");
  aura::Window::Windows roots = Shell::GetAllRootWindows();
  ASSERT_EQ(2u, roots.size());
  auto* primary_display_tray = Shell::GetPrimaryRootWindowController()
                                   ->GetStatusAreaWidget()
                                   ->annotation_tray();
  auto* external_display_tray = RootWindowController::ForWindow(roots[1])
                                    ->GetStatusAreaWidget()
                                    ->annotation_tray();

  // Show tray on primary root window.
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  EXPECT_TRUE(primary_display_tray->visible_preferred());
  EXPECT_FALSE(external_display_tray->visible_preferred());

  // Change the root window.
  annotator_controller()->UnregisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->RegisterView(roots[1]);
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_TRUE(external_display_tray->visible_preferred());

  annotator_controller()->UnregisterView(roots[1]);
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_FALSE(external_display_tray->visible_preferred());
}

TEST_F(AnnotatorControllerTest, UnregisterViewWhenAnnotatorIsEnabled) {
  base::HistogramTester histogram_tester;

  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  EXPECT_TRUE(annotation_tray->visible_preferred());

  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->EnableAnnotatorTool();
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());

  annotator_controller()->UnregisterView(Shell::GetPrimaryRootWindow());
  EXPECT_FALSE(annotation_tray->visible_preferred());
  histogram_tester.ExpectUniqueSample(kProjectorToolbarHistogramName,
                                      ProjectorToolbar::kMarkerTool,
                                      /*count=*/1);
}

TEST_F(AnnotatorControllerTest, EnableAnnotator) {
  // Does nothing as there is no view for annotating.
  annotator_controller()->EnableAnnotatorTool();
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());

  // Enables the annotator after creating the view for annotating.
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->EnableAnnotatorTool();
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());
}

TEST_F(AnnotatorControllerTest, DisableAnnotator) {
  base::HistogramTester histogram_tester;

  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  EXPECT_TRUE(annotation_tray->visible_preferred());

  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->EnableAnnotatorTool();
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());

  annotator_controller()->DisableAnnotator();
  EXPECT_FALSE(annotation_tray->visible_preferred());
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());
}

// Verifies that toggling on the marker enables the annotator tool.
TEST_F(AnnotatorControllerTest, EnablingDisablingMarker) {
  base::HistogramTester histogram_tester;

  // Enable marker.
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->EnableAnnotatorTool();
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());

  EXPECT_CALL(client_, Clear());
  annotator_controller()->ResetTools();
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());

  histogram_tester.ExpectUniqueSample(kProjectorToolbarHistogramName,
                                      ProjectorToolbar::kMarkerTool,
                                      /*count=*/1);
}

TEST_F(AnnotatorControllerTest, ToggleMarkerWithKeyboardShortcut) {
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(true);

  auto* event_generator = GetEventGenerator();
  event_generator->PressAndReleaseKey(ui::VKEY_OEM_3, ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());

  event_generator->PressAndReleaseKey(ui::VKEY_OEM_3, ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());
}

TEST_F(AnnotatorControllerTest, SetAnnotatorToolAndStorePref) {
  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();
  // Assert that the initial pref is unset.
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  EXPECT_EQ(
      pref_service->GetUint64(prefs::kProjectorAnnotatorLastUsedMarkerColor),
      0u);

  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(true);
  // Assert that the icon image for the annotator is set.
  ASSERT_EQ(annotation_tray->tray_container()->children().size(), 1u);
  const views::ImageView* image_view = static_cast<views::ImageView*>(
      annotation_tray->tray_container()->children()[0]);
  EXPECT_FALSE(image_view->GetImageModel().IsEmpty());

  LeftClickOn(annotation_tray);
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());

  annotator_controller()->DisableAnnotator();
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());
  // Check that the last used color is stored in the pref.
  EXPECT_EQ(
      pref_service->GetUint64(prefs::kProjectorAnnotatorLastUsedMarkerColor),
      kAnnotatorDefaultPenColor);
}

// Tests that long pressing the AnnotationTray shows a bubble.
TEST_F(AnnotatorControllerTest, LongPressShowsBubble) {
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(true);

  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();

  // Long press the tray item, it should show a bubble.
  gfx::Point location = annotation_tray->GetBoundsInScreen().CenterPoint();

  // Temporarily reconfigure gestures so that the long press takes 2
  // milliseconds.
  ui::GestureConfiguration* gesture_config =
      ui::GestureConfiguration::GetInstance();
  const int old_long_press_time_in_ms = gesture_config->long_press_time_in_ms();
  const base::TimeDelta old_short_press_time =
      gesture_config->short_press_time();
  const int old_show_press_delay_in_ms =
      gesture_config->show_press_delay_in_ms();
  gesture_config->set_long_press_time_in_ms(1);
  gesture_config->set_short_press_time(base::Milliseconds(1));
  gesture_config->set_show_press_delay_in_ms(1);

  ui::test::EventGenerator* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(location);
  event_generator->PressTouch();

  // Hold the press down for 2 ms, to trigger a long press.
  base::RunLoop run_loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(2));
  run_loop.Run();

  gesture_config->set_long_press_time_in_ms(old_long_press_time_in_ms);
  gesture_config->set_short_press_time(old_short_press_time);
  gesture_config->set_show_press_delay_in_ms(old_show_press_delay_in_ms);

  event_generator->ReleaseTouch();

  EXPECT_TRUE(annotation_tray->GetBubbleWidget());
}

// Tests that tapping the AnnotationTray enables annotation.
TEST_F(AnnotatorControllerTest, TapEnabledAnnotation) {
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(true);

  GestureTapOn(Shell::GetPrimaryRootWindowController()
                   ->GetStatusAreaWidget()
                   ->annotation_tray());

  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());
}

TEST_F(AnnotatorControllerTest, OnCanvasInitialized) {
  auto* annotation_tray = Shell::GetPrimaryRootWindowController()
                              ->GetStatusAreaWidget()
                              ->annotation_tray();
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());

  EXPECT_FALSE(annotation_tray->GetEnabled());

  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(/*success=*/true);
  EXPECT_TRUE(annotator_controller()->GetAnnotatorAvailability());
  EXPECT_TRUE(annotation_tray->GetEnabled());

  annotator_controller()->OnCanvasInitialized(/*success=*/false);
  EXPECT_FALSE(annotator_controller()->GetAnnotatorAvailability());
  EXPECT_FALSE(annotation_tray->GetEnabled());
}

TEST_F(AnnotatorControllerTest, ToggleAnnotationTray) {
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->CreateAnnotationOverlayForWindow(
      Shell::GetPrimaryRootWindow(), std::nullopt);
  annotator_controller()->OnCanvasInitialized(true);
  EXPECT_FALSE(annotator_controller()->is_annotator_enabled());

  annotator_controller()->ToggleAnnotationTray();
  EXPECT_TRUE(annotator_controller()->is_annotator_enabled());
}

TEST_F(AnnotatorControllerTest, CreateAnnotationsOverlayView) {
  auto overlay = annotator_controller()->CreateAnnotationsOverlayView();
  EXPECT_TRUE(overlay.get());
}

TEST_F(AnnotatorControllerTest, UpdateRootViewMultipleDisplays) {
  UpdateDisplay("800x700,801+0-800x700");
  aura::Window::Windows roots = Shell::GetAllRootWindows();
  ASSERT_EQ(2u, roots.size());
  auto* primary_display_tray = Shell::GetPrimaryRootWindowController()
                                   ->GetStatusAreaWidget()
                                   ->annotation_tray();
  auto* external_display_tray = RootWindowController::ForWindow(roots[1])
                                    ->GetStatusAreaWidget()
                                    ->annotation_tray();

  // Show tray on primary root window.
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  EXPECT_TRUE(primary_display_tray->visible_preferred());
  EXPECT_FALSE(external_display_tray->visible_preferred());

  // Change the root window of the recorded window.
  annotator_controller()->UpdateRootView(roots[1]);
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_TRUE(external_display_tray->visible_preferred());

  annotator_controller()->DisableAnnotator();
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_FALSE(external_display_tray->visible_preferred());
}

TEST_F(AnnotatorControllerTest, RegisterViewTwice) {
  UpdateDisplay("800x700,801+0-800x700");
  aura::Window::Windows roots = Shell::GetAllRootWindows();
  ASSERT_EQ(2u, roots.size());
  auto* primary_display_tray = Shell::GetPrimaryRootWindowController()
                                   ->GetStatusAreaWidget()
                                   ->annotation_tray();
  auto* external_display_tray = RootWindowController::ForWindow(roots[1])
                                    ->GetStatusAreaWidget()
                                    ->annotation_tray();

  // Register view twice on 2 different roots.
  annotator_controller()->RegisterView(Shell::GetPrimaryRootWindow());
  annotator_controller()->RegisterView(roots[1]);
  // Expect the tray to be visible only on the second root window, as the first
  // root window was unregistered. Annotator allows only one root window at a
  // time.
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_TRUE(external_display_tray->visible_preferred());

  annotator_controller()->DisableAnnotator();
  EXPECT_FALSE(primary_display_tray->visible_preferred());
  EXPECT_FALSE(external_display_tray->visible_preferred());
}

}  // namespace ash