chromium/ash/app_list/views/assistant/assistant_page_view_unittest.cc

// Copyright 2019 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/app_list/app_list_controller_impl.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/assistant/model/assistant_ui_model.h"
#include "ash/assistant/test/assistant_ash_test_base.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/colors/assistant_colors.h"
#include "ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.h"
#include "ash/assistant/ui/main_stage/suggestion_chip_view.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_service.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkTypes.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/events/event.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

using assistant::AssistantInteractionMetadata;
using assistant::AssistantInteractionType;

#define EXPECT_INTERACTION_OF_TYPE(type_)                     \
  ({                                                          \
    std::optional<AssistantInteractionMetadata> interaction = \
        current_interaction();                                \
    ASSERT_TRUE(interaction.has_value());                     \
    EXPECT_EQ(interaction->type, type_);                      \
  })

#define EXPECT_NO_INTERACTION()                               \
  ({                                                          \
    std::optional<AssistantInteractionMetadata> interaction = \
        current_interaction();                                \
    ASSERT_FALSE(interaction.has_value());                    \
  })

// Ensures that the given view has the focus. If it doesn't, this will print a
// nice error message indicating which view has the focus instead.
#define EXPECT_HAS_FOCUS(expected_)                                           \
  ({                                                                          \
    const views::View* actual = GetFocusedView();                             \
    EXPECT_TRUE(expected_->HasFocus())                                        \
        << "Expected focus on '" << expected_->GetClassName()                 \
        << "' but it is on '" << (actual ? actual->GetClassName() : "<null>") \
        << "'.";                                                              \
  })

// Ensures that the given view does not have the focus. If it does, this will
// print a nice error message.
#define EXPECT_NOT_HAS_FOCUS(expected_)                  \
  ({                                                     \
    EXPECT_FALSE(expected_->HasFocus())                  \
        << "'" << expected_->GetClassName()              \
        << "' should not have the focus (but it does)."; \
  })

views::View* AddTextfield(views::Widget* widget) {
  auto* result = widget->GetContentsView()->AddChildView(
      std::make_unique<views::Textfield>());
  // Give the text field a non-zero size, otherwise things like tapping on it
  // will fail.
  result->SetSize(gfx::Size(20, 10));
  // Focusable views need an accessible name to pass the accessibility paint
  // checks.
  result->GetViewAccessibility().SetName(u"Name");

  return result;
}

// Stubbed |FocusChangeListener| that simply remembers all the views that
// received focus.
class FocusChangeListenerStub : public views::FocusChangeListener {
 public:
  explicit FocusChangeListenerStub(views::View* view)
      : focus_manager_(view->GetFocusManager()) {
    focus_manager_->AddFocusChangeListener(this);
  }

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

  ~FocusChangeListenerStub() override {
    focus_manager_->RemoveFocusChangeListener(this);
  }

  void OnWillChangeFocus(views::View* focused_before,
                         views::View* focused_now) override {}

  void OnDidChangeFocus(views::View* focused_before,
                        views::View* focused_now) override {
    focused_views_.push_back(focused_now);
  }

  // Returns all views that received the focus at some point.
  const std::vector<raw_ptr<views::View, VectorExperimental>>& focused_views()
      const {
    return focused_views_;
  }

 private:
  std::vector<raw_ptr<views::View, VectorExperimental>> focused_views_;
  raw_ptr<views::FocusManager> focus_manager_;
};

// |ViewObserver| that simply remembers whether the given view was drawn
// on the screen at least once during the lifetime of this observer.
// Note this checks |IsDrawn| and not |GetVisible| because visibility is a
// local property which does not take any of the ancestors into account, and
// we do not care if the |observed_view| is marked as visible but its parent
// is not visible.
class VisibilityObserver : public views::ViewObserver {
 public:
  explicit VisibilityObserver(views::View* observed_view)
      : observed_view_(observed_view) {
    observed_view_->AddObserver(this);

    UpdateWasDrawn();
  }
  ~VisibilityObserver() override { observed_view_->RemoveObserver(this); }

  void OnViewVisibilityChanged(views::View* view_or_ancestor,
                               views::View* starting_view) override {
    UpdateWasDrawn();
  }

  bool was_drawn() const { return was_drawn_; }

 private:
  void UpdateWasDrawn() {
    if (observed_view_->IsDrawn())
      was_drawn_ = true;
  }

  const raw_ptr<views::View> observed_view_;
  bool was_drawn_ = false;
};

// Base class for tests of the embedded assistant page in:
// - Legacy clamshell mode ("peeking launcher")
// - Clamshell mode ("bubble launcher")
// - Tablet mode
class AssistantPageViewTest : public AssistantAshTestBase {
 public:
  AssistantPageViewTest() = default;

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

  void ShowAssistantUiInTextMode() {
    ShowAssistantUi(AssistantEntryPoint::kUnspecified);
    EXPECT_TRUE(IsVisible());
  }

  void ShowAssistantUiInVoiceMode() {
    ShowAssistantUi(AssistantEntryPoint::kHotword);
    EXPECT_TRUE(IsVisible());
  }

  void PressKeyAndWait(ui::KeyboardCode key_code) {
    PressAndReleaseKey(key_code);
    base::RunLoop().RunUntilIdle();
  }

  ash::SuggestionChipView* CreateAndGetSuggestionChip(
      const std::string& chip_query) {
    MockTextInteraction().WithSuggestionChip(chip_query);
    auto suggestion_chips = GetSuggestionChips();
    DCHECK_EQ(suggestion_chips.size(), 1u);
    return suggestion_chips[0];
  }

  const views::View* GetFocusedView() {
    return page_view()->GetWidget()->GetFocusManager()->GetFocusedView();
  }
};

// Counts the number of Assistant interactions that are started.
class AssistantInteractionCounter
    : private assistant::AssistantInteractionSubscriber {
 public:
  explicit AssistantInteractionCounter(assistant::Assistant* service) {
    interaction_observer_.Observe(service);
  }
  AssistantInteractionCounter(AssistantInteractionCounter&) = delete;
  AssistantInteractionCounter& operator=(AssistantInteractionCounter&) = delete;
  ~AssistantInteractionCounter() override = default;

  int interaction_count() const { return interaction_count_; }

 private:
  // AssistantInteractionSubscriber implementation:
  void OnInteractionStarted(const AssistantInteractionMetadata&) override {
    interaction_count_++;
  }

  int interaction_count_ = 0;
  assistant::ScopedAssistantInteractionSubscriber interaction_observer_{this};
};

TEST_F(AssistantPageViewTest, PressingAssistantKeyShowsAssistantPage) {
  ShowAssistantUi(AssistantEntryPoint::kHotkey);

  EXPECT_TRUE(Shell::Get()->app_list_controller()->IsVisible());
  EXPECT_TRUE(page_view()->GetVisible());
}

TEST_F(AssistantPageViewTest,
       OpeningLauncherThenPressingAssistantKeyShowsAssistantPage) {
  OpenLauncher();
  ShowAssistantUi(AssistantEntryPoint::kHotkey);

  EXPECT_TRUE(page_view()->GetVisible());
}

TEST_F(AssistantPageViewTest, PressingAssistantKeyTwiceClosesLauncher) {
  ShowAssistantUi(AssistantEntryPoint::kHotkey);
  CloseAssistantUi(AssistantExitPoint::kHotkey);

  EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible());
}

TEST_F(AssistantPageViewTest, ShouldFocusTextFieldWhenOpeningWithHotkey) {
  ShowAssistantUi(AssistantEntryPoint::kHotkey);

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTest, ShouldNotLoseTextfieldFocusWhenSendingTextQuery) {
  ShowAssistantUi();

  SendQueryThroughTextField("The query");

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTest,
       ShouldNotLoseTextfieldFocusWhenDisplayingResponse) {
  ShowAssistantUi();

  MockTextInteraction().WithTextResponse("The response");

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTest, ShouldNotLoseTextfieldFocusWhenResizing) {
  ShowAssistantUi();

  MockTextInteraction().WithTextResponse(
      "This\ntext\nis\nbig\nenough\nto\ncause\nthe\nassistant\nscreen\nto\n"
      "resize.");

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTest, FocusShouldRemainInAssistantViewWhenPressingTab) {
  constexpr int kMaxIterations = 100;
  ShowAssistantUi();

  const views::View* initial_focused_view = GetFocusedView();
  const views::View* focused_view;
  int num_views = 0;

  do {
    PressKeyAndWait(ui::VKEY_TAB);
    focused_view = GetFocusedView();
    EXPECT_TRUE(page_view()->Contains(focused_view))
        << "Focus advanced to view '" << focused_view->GetClassName()
        << "' which is not a part of the Assistant UI";

    // Validity check to ensure we do not loop forever
    num_views++;
    ASSERT_LT(num_views, kMaxIterations);
  } while (focused_view != initial_focused_view);
}

TEST_F(AssistantPageViewTest, ShouldFocusMicWhenOpeningWithHotword) {
  ShowAssistantUi(AssistantEntryPoint::kHotword);

  EXPECT_HAS_FOCUS(mic_view());
}

TEST_F(AssistantPageViewTest, ShouldShowGreetingLabelWhenOpening) {
  ShowAssistantUi();

  EXPECT_TRUE(greeting_label()->IsDrawn());
  EXPECT_FALSE(onboarding_view()->IsDrawn());
}

TEST_F(AssistantPageViewTest, ShouldDismissGreetingLabelAfterQuery) {
  ShowAssistantUi();

  MockTextInteraction().WithTextResponse("The response");

  EXPECT_FALSE(greeting_label()->IsDrawn());
  EXPECT_FALSE(onboarding_view()->IsDrawn());
}

TEST_F(AssistantPageViewTest, ShouldShowGreetingLabelAgainAfterReopening) {
  ShowAssistantUi();

  // Cause the label to be hidden.
  MockTextInteraction().WithTextResponse("The response");
  ASSERT_FALSE(greeting_label()->IsDrawn());

  // Close and reopen the Assistant UI.
  CloseAssistantUi();
  ShowAssistantUi();

  EXPECT_TRUE(greeting_label()->IsDrawn());
  EXPECT_FALSE(onboarding_view()->IsDrawn());
}

TEST_F(AssistantPageViewTest,
       ShouldNotShowGreetingLabelWhenOpeningFromSearchResult) {
  ShowAssistantUi(AssistantEntryPoint::kLauncherSearchResult);

  EXPECT_FALSE(greeting_label()->IsDrawn());
  EXPECT_FALSE(onboarding_view()->IsDrawn());
}

TEST_F(AssistantPageViewTest,
       ShouldNotShowOnboardingWhenOpeningFromSearchResult) {
  ShowAssistantUi(AssistantEntryPoint::kLauncherSearchResult);

  EXPECT_FALSE(onboarding_view()->IsDrawn());
  EXPECT_FALSE(greeting_label()->IsDrawn());
}

TEST_F(AssistantPageViewTest,
       ShouldNotShowOnboardingToExistingUsersIfShownPreviouslyInMaxSessions) {
  SetTimeOfLastInteraction(base::Time::Now());
  SetNumberOfSessionsWhereOnboardingShown(
      assistant::ui::kOnboardingMaxSessionsShown);

  ShowAssistantUi();

  // This user has *not* interacted with Assistant more recently than 28 days
  // ago so they *are* considered new. Onboarding would normally be shown but,
  // since it was shown already in the max number of previous user sessions, we
  // do *not* show it.
  EXPECT_FALSE(onboarding_view()->IsDrawn());
}

TEST_F(AssistantPageViewTest, ShouldFocusMicViewWhenPressingVoiceInputToggle) {
  ShowAssistantUiInTextMode();

  ClickOnAndWait(voice_input_toggle());

  EXPECT_HAS_FOCUS(mic_view());
}

TEST_F(AssistantPageViewTest,
       ShouldStartVoiceInteractionWhenPressingVoiceInputToggle) {
  ShowAssistantUiInTextMode();

  ClickOnAndWait(voice_input_toggle());

  EXPECT_INTERACTION_OF_TYPE(AssistantInteractionType::kVoice);
}

TEST_F(AssistantPageViewTest,
       ShouldStopVoiceInteractionWhenPressingKeyboardInputToggle) {
  ShowAssistantUiInVoiceMode();
  EXPECT_INTERACTION_OF_TYPE(AssistantInteractionType::kVoice);

  ClickOnAndWait(keyboard_input_toggle());

  EXPECT_FALSE(current_interaction().has_value());
}

TEST_F(AssistantPageViewTest, ShouldNotShowOptInViewWithZeroStateView) {
  ShowAssistantUi();

  // When Launcher Search IPH is enabled, there is no suggestion or opt-in chips
  // on the zero state page.
  const views::View* suggestion_chips = suggestion_chip_container();
  const views::View* opt_in = opt_in_view();

  SetConsentStatus(ConsentStatus::kUnauthorized);
  EXPECT_FALSE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kNotFound);
  EXPECT_FALSE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kUnknown);
  EXPECT_FALSE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kActivityControlAccepted);
  EXPECT_FALSE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());
}

TEST_F(AssistantPageViewTest, ShouldShowOptInViewUnlessUserHasGivenConsent) {
  ShowAssistantUi();

  // When Launcher Search IPH is enabled and it is not in zero state view, we
  // show the suggestion or opt-in chips as needed.
  MockTextInteraction().WithTextResponse("The response");

  const views::View* suggestion_chips = suggestion_chip_container();
  const views::View* opt_in = opt_in_view();

  SetConsentStatus(ConsentStatus::kUnauthorized);
  EXPECT_TRUE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kNotFound);
  EXPECT_TRUE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kUnknown);
  EXPECT_TRUE(opt_in->IsDrawn());
  EXPECT_FALSE(suggestion_chips->IsDrawn());

  SetConsentStatus(ConsentStatus::kActivityControlAccepted);
  EXPECT_FALSE(opt_in->IsDrawn());
  EXPECT_TRUE(suggestion_chips->IsDrawn());
}

TEST_F(AssistantPageViewTest, ShouldSubmitQueryWhenClickingOnSuggestionChip) {
  ShowAssistantUi();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");

  ClickOnAndWait(suggestion_chip);

  EXPECT_INTERACTION_OF_TYPE(AssistantInteractionType::kText);
  EXPECT_EQ("<suggestion chip query>", current_interaction()->query);
}

TEST_F(AssistantPageViewTest,
       ShouldSubmitQueryWhenPressingEnterOnSuggestionChip) {
  ShowAssistantUi();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");

  suggestion_chip->RequestFocus();
  PressKeyAndWait(ui::VKEY_RETURN);

  EXPECT_INTERACTION_OF_TYPE(AssistantInteractionType::kText);
  EXPECT_EQ("<suggestion chip query>", current_interaction()->query);
}

TEST_F(AssistantPageViewTest,
       ShouldNotSubmitQueryWhenPressingSpaceOnSuggestionChip) {
  ShowAssistantUi();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");

  suggestion_chip->RequestFocus();
  PressKeyAndWait(ui::VKEY_SPACE);

  EXPECT_NO_INTERACTION();
}

TEST_F(AssistantPageViewTest,
       ShouldOnlySubmitOneQueryWhenClickingSuggestionChipMultipleTimes) {
  ShowAssistantUi();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");

  AssistantInteractionCounter counter{assistant_service()};
  ClickOnAndWait(suggestion_chip, /*check_if_view_can_process_events=*/false);
  ClickOnAndWait(suggestion_chip, /*check_if_view_can_process_events=*/false);
  ClickOnAndWait(suggestion_chip, /*check_if_view_can_process_events=*/false);
  ClickOnAndWait(suggestion_chip, /*check_if_view_can_process_events=*/false);

  EXPECT_EQ(1, counter.interaction_count());
}

TEST_F(AssistantPageViewTest,
       ShouldOnlySubmitQueryFromFirstSuggestionChipClickedOn) {
  ShowAssistantUi();
  MockTextInteraction()
      .WithSuggestionChip("<first query>")
      .WithSuggestionChip("<second query>")
      .WithSuggestionChip("<third query>");
  auto suggestion_chips = GetSuggestionChips();

  AssistantInteractionCounter counter{assistant_service()};
  ClickOnAndWait(suggestion_chips[0]);
  // All next clicks should be no-ops.
  ClickOnAndWait(suggestion_chips[0],
                 /*check_if_view_can_process_events=*/false);
  ClickOnAndWait(suggestion_chips[1],
                 /*check_if_view_can_process_events=*/false);
  ClickOnAndWait(suggestion_chips[2],
                 /*check_if_view_can_process_events=*/false);

  EXPECT_EQ(1, counter.interaction_count());
  EXPECT_EQ("<first query>", current_interaction()->query);
}

TEST_F(AssistantPageViewTest,
       SuggestionChipsShouldNotBeFocusableAfterSubmittingQuery) {
  ShowAssistantUi();
  MockTextInteraction()
      .WithSuggestionChip("<first query>")
      .WithSuggestionChip("<second query>")
      .WithSuggestionChip("<third query>");
  auto suggestion_chips = GetSuggestionChips();

  suggestion_chips[0]->RequestFocus();
  PressKeyAndWait(ui::VKEY_RETURN);

  for (auto* suggestion_chip : suggestion_chips) {
    EXPECT_FALSE(suggestion_chip->IsFocusable())
        << "Suggestion chip '" << suggestion_chip->GetText()
        << "' is still focusable";
  }
}

TEST_F(AssistantPageViewTest,
       ShouldFocusTextFieldWhenSubmittingSuggestionChipInTextMode) {
  ShowAssistantUiInTextMode();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");

  suggestion_chip->RequestFocus();
  PressKeyAndWait(ui::VKEY_RETURN);

  EXPECT_HAS_FOCUS(input_text_field());
}

// TODO(b/234164113): Test is flaky.
TEST_F(AssistantPageViewTest,
       DISABLED_ShouldFocusMicWhenSubmittingSuggestionChipInVoiceMode) {
  ShowAssistantUi();
  ash::SuggestionChipView* suggestion_chip =
      CreateAndGetSuggestionChip("<suggestion chip query>");
  ClickOnAndWait(voice_input_toggle());

  suggestion_chip->RequestFocus();
  PressKeyAndWait(ui::VKEY_RETURN);

  EXPECT_HAS_FOCUS(mic_view());
}

TEST_F(AssistantPageViewTest,
       ShouldFocusTextFieldWhenPressingKeyboardInputToggle) {
  ShowAssistantUiInVoiceMode();

  ClickOnAndWait(keyboard_input_toggle());

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTest,
       ShouldNotScrollSuggestionChipsWhenSubmittingQuery) {
  ShowAssistantUiInTextMode();
  MockTextInteraction()
      .WithSuggestionChip("there are                                        x")
      .WithSuggestionChip("enough queries                                   x")
      .WithSuggestionChip("to ensure                                        x")
      .WithSuggestionChip("the                                              x")
      .WithSuggestionChip("suggestion chips container                       x")
      .WithSuggestionChip("can scroll.                                      x");

  views::View* chip = GetSuggestionChips()[3];
  chip->RequestFocus();
  chip->ScrollViewToVisible();

  gfx::Rect initial_bounds = chip->GetBoundsInScreen();
  PressKeyAndWait(ui::VKEY_RETURN);
  gfx::Rect final_bounds = chip->GetBoundsInScreen();

  EXPECT_EQ(initial_bounds, final_bounds);
}

TEST_F(AssistantPageViewTest, RememberAndShowHistory) {
  ShowAssistantUiInTextMode();
  EXPECT_HAS_FOCUS(input_text_field());

  MockTextInteraction().WithQuery("query 1").WithTextResponse("response 1");
  MockTextInteraction().WithQuery("query 2").WithTextResponse("response 2");

  EXPECT_HAS_FOCUS(input_text_field());

  EXPECT_TRUE(input_text_field()->GetText().empty());

  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(input_text_field()->GetText(), u"query 2");

  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(input_text_field()->GetText(), u"query 1");

  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(input_text_field()->GetText(), u"query 1");

  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_EQ(input_text_field()->GetText(), u"query 2");

  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_TRUE(input_text_field()->GetText().empty());
}

TEST_F(AssistantPageViewTest, ShouldNotHaveConversationStarters) {
  ShowAssistantUi();

  EXPECT_FALSE(onboarding_view()->IsDrawn());

  // When Launcher Search IPH is enabled, there is no suggestion chips.
  EXPECT_TRUE(GetSuggestionChips().empty());
}

TEST_F(AssistantPageViewTest, ShouldHavePopulatedSuggestionChips) {
  constexpr char kAnyQuery[] = "<query>";
  constexpr char kAnyText[] = "<text>";
  constexpr char kAnyChip[] = "<chip>";

  ShowAssistantUi();
  MockTextInteraction()
      .WithQuery(kAnyQuery)
      .WithTextResponse(kAnyText)
      .WithSuggestionChip(kAnyChip);

  auto chips = GetSuggestionChips();
  ASSERT_EQ(chips.size(), 1u);
  auto* chip = chips.at(0);

  EXPECT_EQ(kAnyChip, base::UTF16ToUTF8(chip->GetText()));
}

TEST_F(AssistantPageViewTest, PageViewHasBackgroundBlurInTabletMode) {
  // AssistantPageView is only used in tablet mode. Clamshell mode uses
  // AppListBubbleAssistantPage.
  SetTabletMode(true);
  ShowAssistantUi();

  EXPECT_FALSE(page_view()->background());
  ui::Layer* page_view_layer = page_view()->layer();
  ASSERT_TRUE(page_view_layer);
  EXPECT_FALSE(page_view_layer->fills_bounds_opaquely());
  EXPECT_EQ(page_view_layer->background_blur(),
            ColorProvider::kBackgroundBlurSigma);
  EXPECT_EQ(
      page_view_layer->GetTargetColor(),
      page_view()->GetColorProvider()->GetColor(kColorAshShieldAndBase80));
}

TEST_F(AssistantPageViewTest, BackgroundColorInDarkLightMode) {
  auto* dark_light_mode_controller = DarkLightModeControllerImpl::Get();
  dark_light_mode_controller->OnActiveUserPrefServiceChanged(
      Shell::Get()->session_controller()->GetActivePrefService());

  // AssistantPageView is only used in tablet mode. Clamshell mode uses
  // AppListBubbleAssistantPage.
  SetTabletMode(true);
  ShowAssistantUi();

  const bool initial_dark_mode_status =
      dark_light_mode_controller->IsDarkModeEnabled();
  EXPECT_EQ(
      page_view()->layer()->GetTargetColor(),
      page_view()->GetColorProvider()->GetColor(kColorAshShieldAndBase80));

  // Switch the color mode.
  dark_light_mode_controller->ToggleColorMode();
  ASSERT_NE(initial_dark_mode_status,
            dark_light_mode_controller->IsDarkModeEnabled());

  EXPECT_EQ(
      page_view()->layer()->GetTargetColor(),
      page_view()->GetColorProvider()->GetColor(kColorAshShieldAndBase80));
}

TEST_F(AssistantPageViewTest, AccessibleProperties) {
  SetTabletMode(true);
  ShowAssistantUi();
  ui::AXNodeData data;

  page_view()->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.role, ax::mojom::Role::kPane);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            l10n_util::GetStringUTF16(IDS_ASH_ASSISTANT_WINDOW));
}

//------------------------------------------------------------------------------
// Tests the |AssistantPageView| with tablet mode enabled.
class AssistantPageViewTabletModeTest : public AssistantPageViewTest {
 public:
  AssistantPageViewTabletModeTest() = default;

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

  void SetUp() override {
    AssistantPageViewTest::SetUp();
    SetTabletMode(true);
  }

  // Ensures all views are created by showing and hiding the UI.
  void CreateAllViews() {
    ShowAssistantUi();
    CloseAssistantUi();
  }

  void ShowAssistantUiInTextMode() {
    // Note we launch in voice mode and switch to text because that was the
    // easiest way I could get this working (we can't simply use |kUnspecified|
    // as that puts us in voice mode.
    ShowAssistantUiInVoiceMode();
    TapOnAndWait(keyboard_input_toggle());
  }

  // Returns a point in the AppList, but outside the Assistant UI.
  gfx::Point GetPointInAppListOutsideAssistantUi() {
    gfx::Point result = GetPointOutside(page_view());

    // Validity check
    EXPECT_TRUE(app_list_view()->bounds().Contains(result));
    EXPECT_FALSE(page_view()->bounds().Contains(result));

    return result;
  }

  gfx::Point GetPointOutside(const views::View* view) {
    return gfx::Point(view->origin().x() - 10, view->origin().y() - 10);
  }

  gfx::Point GetPointInside(const views::View* view) {
    return view->GetBoundsInScreen().CenterPoint();
  }
};

TEST_F(AssistantPageViewTabletModeTest,
       ShouldFocusMicWhenOpeningWithLongPressLauncher) {
  ShowAssistantUi(AssistantEntryPoint::kLongPressLauncher);

  EXPECT_HAS_FOCUS(mic_view());
}

TEST_F(AssistantPageViewTabletModeTest, ShouldFocusMicWhenOpeningWithHotword) {
  ShowAssistantUi(AssistantEntryPoint::kHotword);

  EXPECT_HAS_FOCUS(mic_view());
}

TEST_F(AssistantPageViewTabletModeTest, ShouldFocusTextFieldAfterSendingQuery) {
  ShowAssistantUiInTextMode();

  SendQueryThroughTextField("The query");

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissKeyboardAfterSendingQuery) {
  ShowAssistantUiInTextMode();
  EXPECT_TRUE(IsKeyboardShowing());

  SendQueryThroughTextField("The query");

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotShowKeyboardWhenOpeningLauncherButNotAssistantUi) {
  OpenLauncher();
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldShowKeyboardAfterTouchingInputTextbox) {
  ShowAssistantUiInTextMode();
  DismissKeyboard();
  EXPECT_FALSE(IsKeyboardShowing());

  TapOnAndWait(input_text_field());

  EXPECT_TRUE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest, ShouldNotShowKeyboardWhenItsDisabled) {
  // This tests the scenario where the keyboard is disabled even in tablet mode,
  // e.g. when an external keyboard is connected to a tablet.
  DisableKeyboard();
  ShowAssistantUiInTextMode();

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldFocusTextFieldAfterPressingKeyboardInputToggle) {
  ShowAssistantUiInVoiceMode();
  EXPECT_NOT_HAS_FOCUS(input_text_field());

  TapOnAndWait(keyboard_input_toggle());

  EXPECT_HAS_FOCUS(input_text_field());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldShowKeyboardAfterPressingKeyboardInputToggle) {
  ShowAssistantUiInVoiceMode();
  EXPECT_FALSE(IsKeyboardShowing());

  TapOnAndWait(keyboard_input_toggle());

  EXPECT_TRUE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissKeyboardAfterPressingVoiceInputToggle) {
  ShowAssistantUiInTextMode();
  EXPECT_TRUE(IsKeyboardShowing());

  TapOnAndWait(voice_input_toggle());

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissKeyboardWhenOpeningUiInVoiceMode) {
  // Start by focussing a text field so the system has a reason to show the
  // keyboard.
  views::Widget* widget = SwitchToNewWidget();
  auto* textfield = AddTextfield(widget);
  TapOnAndWait(textfield);
  ASSERT_TRUE(IsKeyboardShowing());

  ShowAssistantUiInVoiceMode();

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissAssistantUiIfLostFocusWhenOtherAppWindowOpens) {
  ShowAssistantUi();

  // Create a new window to steal the focus which should dismiss the Assistant
  // UI.
  SwitchToNewAppWindow();

  EXPECT_FALSE(IsVisible());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotShowKeyboardWhenClosingAssistantUI) {
  // Note: This checks for a bug where the closing sequence of the UI switches
  // the input mode to text which then shows the keyboard.
  ShowAssistantUiInVoiceMode();

  CloseAssistantUi();

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissKeyboardWhenClosingAssistantUI) {
  ShowAssistantUiInTextMode();
  EXPECT_TRUE(IsKeyboardShowing());

  // Enable the animations because the fact that the switch-modality animation
  // runs changes the timing enough that it triggers a potential bug where the
  // keyboard remains visible after the UI is closed.
  ui::ScopedAnimationDurationScaleMode enable_animations{
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION};

  CloseAssistantUi();

  EXPECT_FALSE(IsKeyboardShowing());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissAssistantUiWhenTappingAppListView) {
  ShowAssistantUiInVoiceMode();

  TapAndWait(GetPointInAppListOutsideAssistantUi());

  EXPECT_FALSE(IsVisible());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldDismissKeyboardButNotAssistantUiWhenTappingAppListView) {
  // Note: This is consistent with how the app list search box works;
  // the first tap will dismiss the keyboard but not change the state of the
  // search box.
  ShowAssistantUiInTextMode();
  EXPECT_TRUE(IsKeyboardShowing());

  TapAndWait(GetPointInAppListOutsideAssistantUi());

  EXPECT_FALSE(IsKeyboardShowing());
  EXPECT_TRUE(IsVisible());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotDismissKeyboardWhenTappingInsideAssistantUi) {
  ShowAssistantUiInTextMode();
  EXPECT_TRUE(IsKeyboardShowing());

  TapAndWait(GetPointInside(page_view()));

  EXPECT_TRUE(IsKeyboardShowing());
  EXPECT_TRUE(IsVisible());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotDismissAssistantUiWhenTappingInsideAssistantUi) {
  ShowAssistantUi();

  TapAndWait(GetPointInside(page_view()));

  EXPECT_TRUE(IsVisible());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotFlickerTextInputWhenOpeningAssistantUI) {
  // If the text input field is visible at any time when opening the Assistant
  // UI, it will cause an unwanted visible animation (of the voice input
  // animating in).

  CreateAllViews();
  VisibilityObserver text_field_observer(input_text_field());

  ShowAssistantUiInVoiceMode();

  EXPECT_FALSE(text_field_observer.was_drawn());
}

TEST_F(AssistantPageViewTabletModeTest,
       ShouldNotFlickerTextInputWhenClosingAssistantUI) {
  // Same as above but for the closing animation.
  ShowAssistantUiInVoiceMode();

  VisibilityObserver text_field_observer(input_text_field());

  CloseAssistantUi();

  EXPECT_FALSE(text_field_observer.was_drawn());
}

TEST_F(AssistantPageViewTabletModeTest, ShouldCloseAssistantUIInOverviewMode) {
  ShowAssistantUi(AssistantEntryPoint::kLongPressLauncher);
  EXPECT_TRUE(IsVisible());

  StartOverview();
  EXPECT_FALSE(IsVisible());
}

}  // namespace
}  // namespace ash