chromium/ash/picker/views/picker_list_item_view_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/picker/views/picker_list_item_view.h"

#include <string>
#include <utility>

#include "ash/picker/model/picker_action_type.h"
#include "ash/picker/views/picker_badge_view.h"
#include "ash/picker/views/picker_item_view.h"
#include "ash/picker/views/picker_preview_bubble.h"
#include "ash/picker/views/picker_preview_bubble_controller.h"
#include "ash/picker/views/picker_shortcut_hint_view.h"
#include "ash/picker/views/picker_submenu_controller.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/style/ash_color_provider.h"
#include "ash/test/view_drawn_waiter.h"
#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_utils.h"

namespace ash {
namespace {

using ::testing::Property;
using ::testing::SizeIs;

base::OnceCallback<std::optional<base::File::Info>()> GetNulloptFileInfo() {
  return base::ReturnValueOnce<std::optional<base::File::Info>>(std::nullopt);
}

base::OnceCallback<std::optional<base::File::Info>()> GetFileInfoCallback(
    base::File::Info file_info) {
  return base::ReturnValueOnce<std::optional<base::File::Info>>(file_info);
}

class PickerListItemViewTest : public views::ViewsTestBase {
 private:
  AshColorProvider provider_;
};

TEST_F(PickerListItemViewTest, SetsPrimaryText) {
  PickerListItemView item_view(base::DoNothing());

  const std::u16string kPrimaryText = u"Item";
  item_view.SetPrimaryText(kPrimaryText);

  ASSERT_THAT(item_view.primary_container_for_testing()->children(), SizeIs(1));
  const views::View* primary_label =
      item_view.primary_container_for_testing()->children()[0];
  ASSERT_TRUE(views::IsViewClass<views::Label>(primary_label));
  EXPECT_THAT(views::AsViewClass<views::Label>(primary_label),
              Property(&views::Label::GetText, kPrimaryText));
}

TEST_F(PickerListItemViewTest, SetsPrimaryImage) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetPrimaryImage(ui::ImageModel(), /*available_width=*/100);

  ASSERT_THAT(item_view.primary_container_for_testing()->children(), SizeIs(1));
  EXPECT_TRUE(views::IsViewClass<views::ImageView>(
      item_view.primary_container_for_testing()->children()[0]));
}

TEST_F(PickerListItemViewTest, SetPrimaryImageScalesImage) {
  PickerListItemView item_view(base::DoNothing());
  item_view.SetPrimaryImage(
      ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(1)),
      /*available_width=*/320);

  ASSERT_THAT(item_view.primary_container_for_testing()->children(), SizeIs(1));
  ASSERT_TRUE(views::IsViewClass<views::ImageView>(
      item_view.primary_container_for_testing()->children()[0]));
  EXPECT_EQ(views::AsViewClass<views::ImageView>(
                item_view.primary_container_for_testing()->children()[0])
                ->GetImageBounds()
                .size(),
            gfx::Size(252, 64));
}

TEST_F(PickerListItemViewTest, SetsLeadingIcon) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetLeadingIcon(ui::ImageModel::FromVectorIcon(
      kImeMenuEmoticonIcon, cros_tokens::kCrosSysOnSurface));

  EXPECT_TRUE(
      item_view.leading_icon_view_for_testing().GetImageModel().IsVectorIcon());
}

TEST_F(PickerListItemViewTest, SetsShortcutHintView) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetShortcutHintView(std::make_unique<PickerShortcutHintView>(
      PickerCapsLockResult::Shortcut::kAltSearch));

  EXPECT_NE(item_view.shortcut_hint_view_for_testing(), nullptr);
}

TEST_F(PickerListItemViewTest, SetsBadgeVisible) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetBadgeVisible(true);

  EXPECT_TRUE(item_view.trailing_badge_for_testing().GetVisible());
}

TEST_F(PickerListItemViewTest, SetsBadgeNotVisible) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetBadgeVisible(false);

  EXPECT_FALSE(item_view.trailing_badge_for_testing().GetVisible());
}

TEST_F(PickerListItemViewTest, SetsBadgeVisibleWithPrimaryText) {
  PickerListItemView item_view(base::DoNothing());
  item_view.SetPrimaryText(u"a");

  item_view.SetBadgeVisible(true);

  EXPECT_TRUE(item_view.trailing_badge_for_testing().GetVisible());
}

TEST_F(PickerListItemViewTest, DoesNotSetBadgeVisibleWithPrimaryImage) {
  PickerListItemView item_view(base::DoNothing());
  item_view.SetPrimaryImage(ui::ImageModel(), /*available_width=*/100);

  item_view.SetBadgeVisible(true);

  EXPECT_FALSE(item_view.trailing_badge_for_testing().GetVisible());
}

TEST_F(PickerListItemViewTest, SetBadgeActionDoHasNoLabelText) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetBadgeAction(PickerActionType::kDo);

  EXPECT_EQ(item_view.trailing_badge_for_testing().GetText(), u"");
}

TEST_F(PickerListItemViewTest, SetBadgeActionHasLabelText) {
  PickerListItemView item_view(base::DoNothing());

  item_view.SetBadgeAction(PickerActionType::kInsert);
  EXPECT_NE(item_view.trailing_badge_for_testing().GetText(), u"");

  item_view.SetBadgeAction(PickerActionType::kOpen);
  EXPECT_NE(item_view.trailing_badge_for_testing().GetText(), u"");

  item_view.SetBadgeAction(PickerActionType::kCreate);
  EXPECT_NE(item_view.trailing_badge_for_testing().GetText(), u"");
}

TEST_F(PickerListItemViewTest, SetPreviewUpdatesIconWithPlaceholder) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  widget->Show();

  item_view->SetPreview(&preview_controller, base::NullCallback(),
                        base::FilePath(), base::DoNothing(),
                        /*update_icon=*/true);

  EXPECT_EQ(item_view->leading_icon_view_for_testing()
                .GetImageModel()
                .GetImage()
                .AsBitmap()
                .getColor(1, 1),
            SK_ColorTRANSPARENT);
}

TEST_F(PickerListItemViewTest, SetPreviewUpdatesIconOncePreviewIconResolves) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  widget->Show();
  base::RunLoop run_loop;
  SkBitmap bitmap = gfx::test::CreateBitmap(100, SK_ColorBLUE);

  item_view->SetPrimaryText(u"abc");
  item_view->SetPreview(
      &preview_controller, base::NullCallback(), base::FilePath(),
      base::BindLambdaForTesting(
          [&](const base::FilePath& file_path, const gfx::Size& size,
              HoldingSpaceImage::BitmapCallback callback) {
            std::move(callback).Run(&bitmap, base::File::Error::FILE_OK);
            run_loop.Quit();
          }),
      /*update_icon=*/true);

  run_loop.Run();
  EXPECT_EQ(item_view->leading_icon_view_for_testing()
                .GetImageModel()
                .GetImage()
                .AsBitmap()
                .getColor(1, 1),
            SK_ColorBLUE);
}

TEST_F(PickerListItemViewTest, SetPreviewResolvesFileInfo) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  widget->Show();
  base::test::TestFuture<void> file_info_future;

  item_view->SetPreview(
      &preview_controller,
      file_info_future.GetSequenceBoundCallback().Then(GetNulloptFileInfo()),
      base::FilePath(), base::DoNothing(), /*update_icon=*/true);

  EXPECT_TRUE(file_info_future.Wait());
}

TEST_F(PickerListItemViewTest, PseudofocusHidesLabelsBeforeFileInfoResolves) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  widget->Show();
  base::test::TestFuture<void> file_info_future;

  item_view->SetPreview(
      &preview_controller,
      file_info_future.GetSequenceBoundCallback().Then(GetNulloptFileInfo()),
      base::FilePath(), base::DoNothing(), /*update_icon=*/true);
  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);

  PickerPreviewBubbleView* bubble_view =
      preview_controller.bubble_view_for_testing();
  ASSERT_FALSE(file_info_future.IsReady());
  ViewDrawnWaiter().Wait(bubble_view);
  EXPECT_FALSE(bubble_view->GetLabelVisibleForTesting());
}

TEST_F(PickerListItemViewTest,
       PseudofocusHidesPreviewLabelsAfterFileInfoResolvesWithNullFileInfo) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  widget->Show();
  base::test::TestFuture<void> file_info_future;

  item_view->SetPreview(
      &preview_controller,
      file_info_future.GetSequenceBoundCallback().Then(
          base::ReturnValueOnce<std::optional<base::File::Info>>(
              base::File::Info())),
      base::FilePath(), base::DoNothing(), /*update_icon=*/true);
  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);

  PickerPreviewBubbleView* bubble_view =
      preview_controller.bubble_view_for_testing();
  ASSERT_TRUE(file_info_future.Wait()) << "File info was never resolved";
  // `GetSequenceBoundCallback` allows this sequence to know when the callback
  // is called by the task runner... but it doesn't allow this sequence to know
  // when the *reply* is run (in this sequence). This `RunUntilIdle` is required
  // to ensure that the reply is run.
  base::RunLoop().RunUntilIdle();
  ViewDrawnWaiter().Wait(bubble_view);
  EXPECT_FALSE(bubble_view->GetLabelVisibleForTesting());
}

TEST_F(PickerListItemViewTest, PseudofocusShowsPreviewLabelsWithValidFileInfo) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  widget->Show();
  base::File::Info only_modified;
  EXPECT_TRUE(base::Time::FromString("23 Dec 2021 09:01:00",
                                     &only_modified.last_modified));
  base::test::TestFuture<void> file_info_future;

  item_view->SetPreview(&preview_controller,
                        file_info_future.GetSequenceBoundCallback().Then(
                            GetFileInfoCallback(only_modified)),
                        base::FilePath(), base::DoNothing(),
                        /*update_icon=*/true);
  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);

  PickerPreviewBubbleView* bubble_view =
      preview_controller.bubble_view_for_testing();
  ASSERT_TRUE(file_info_future.Wait()) << "File info was never resolved";
  base::RunLoop().RunUntilIdle();
  ViewDrawnWaiter().Wait(bubble_view);
  EXPECT_TRUE(bubble_view->GetLabelVisibleForTesting());
  EXPECT_EQ(bubble_view->GetMainTextForTesting(), u"Edited · Dec 23");
}

TEST_F(PickerListItemViewTest, PseudofocusShowsPreviewUsingCachedFileInfo) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  widget->Show();
  base::File::Info only_modified;
  EXPECT_TRUE(base::Time::FromString("23 Dec 2021 09:01:00",
                                     &only_modified.last_modified));
  base::test::TestFuture<void> file_info_future;
  item_view->SetPreview(&preview_controller,
                        file_info_future.GetSequenceBoundCallback().Then(
                            GetFileInfoCallback(only_modified)),
                        base::FilePath(), base::DoNothing(),
                        /*update_icon=*/true);
  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);
  PickerPreviewBubbleView* bubble_view =
      preview_controller.bubble_view_for_testing();
  ASSERT_TRUE(file_info_future.Wait()) << "File info was never resolved";
  base::RunLoop().RunUntilIdle();
  ViewDrawnWaiter().Wait(bubble_view);
  item_view->SetItemState(PickerItemView::ItemState::kNormal);

  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);

  EXPECT_TRUE(bubble_view->GetLabelVisibleForTesting());
  EXPECT_EQ(bubble_view->GetMainTextForTesting(), u"Edited · Dec 23");
}

TEST_F(PickerListItemViewTest, ClosesPreviewBubbleAfterLosingPseudoFocus) {
  PickerPreviewBubbleController preview_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  widget->Show();
  base::test::TestFuture<void> file_info_future;
  item_view->SetPreview(&preview_controller, base::NullCallback(),
                        base::FilePath(), base::DoNothing(),
                        /*update_icon=*/true);
  item_view->SetItemState(PickerItemView::ItemState::kPseudoFocused);

  item_view->SetItemState(PickerItemView::ItemState::kNormal);

  EXPECT_EQ(preview_controller.bubble_view_for_testing(), nullptr);
}

TEST_F(PickerListItemViewTest, ClosesSubmenuOnEnter) {
  auto anchor_widget =
      CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  anchor_widget->SetContentsView(std::make_unique<views::View>());
  anchor_widget->Show();
  PickerSubmenuController submenu_controller;
  auto widget = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  auto* item_view = widget->SetContentsView(
      std::make_unique<PickerListItemView>(base::DoNothing()));
  item_view->SetPrimaryText(u"abc");
  item_view->SetSubmenuController(&submenu_controller);
  widget->Show();
  submenu_controller.Show(anchor_widget->GetContentsView(), {});

  item_view->OnMouseEntered(ui::MouseEvent(
      ui::EventType::kMouseMoved, gfx::PointF(), gfx::PointF(),
      /*time_stamp=*/{}, ui::EF_IS_SYNTHESIZED, ui::EF_LEFT_MOUSE_BUTTON));

  views::test::WidgetDestroyedWaiter(submenu_controller.widget_for_testing())
      .Wait();
}

TEST_F(PickerListItemViewTest, AccessibleNameUsesPrimaryText) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");

  EXPECT_EQ(view.GetAccessibleName(), u"primary");
}

TEST_F(PickerListItemViewTest, AccessibleNameUsesPrimaryAndSecondaryText) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetSecondaryText(u"secondary");

  EXPECT_EQ(view.GetAccessibleName(), u"primary, secondary");
}

TEST_F(PickerListItemViewTest, AccessibleNameUsesPrimaryTextAndBadgeActionDo) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetBadgeAction(PickerActionType::kDo);

  EXPECT_EQ(view.GetAccessibleName(), u"primary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryTextAndBadgeActionInsert) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetBadgeAction(PickerActionType::kInsert);

  EXPECT_EQ(view.GetAccessibleName(), u"Insert primary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryTextAndBadgeActionOpen) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetBadgeAction(PickerActionType::kOpen);

  EXPECT_EQ(view.GetAccessibleName(), u"Open primary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryTextAndBadgeActionCreate) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetBadgeAction(PickerActionType::kCreate);

  EXPECT_EQ(view.GetAccessibleName(), u"primary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryAndSecondaryTextAndBadgeActionDo) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetSecondaryText(u"secondary");
  view.SetBadgeAction(PickerActionType::kDo);

  EXPECT_EQ(view.GetAccessibleName(), u"primary, secondary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryAndSecondaryTextAndBadgeActionInsert) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetSecondaryText(u"secondary");
  view.SetBadgeAction(PickerActionType::kInsert);

  EXPECT_EQ(view.GetAccessibleName(), u"Insert primary, secondary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryAndSecondaryTextAndBadgeActionOpen) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetSecondaryText(u"secondary");
  view.SetBadgeAction(PickerActionType::kOpen);

  EXPECT_EQ(view.GetAccessibleName(), u"Open primary, secondary");
}

TEST_F(PickerListItemViewTest,
       AccessibleNameUsesPrimaryAndSecondaryTextAndBadgeActionCreate) {
  PickerListItemView view(base::DoNothing());
  view.SetPrimaryText(u"primary");
  view.SetSecondaryText(u"secondary");
  view.SetBadgeAction(PickerActionType::kCreate);

  EXPECT_EQ(view.GetAccessibleName(), u"primary, secondary");
}

}  // namespace
}  // namespace ash