// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/input_method/assistive_window_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "base/feature_list.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chrome/browser/ash/input_method/assistive_window_controller_delegate.h"
#include "chrome/browser/ui/ash/input_method/announcement_view.h"
#include "chrome/browser/ui/ash/input_method/suggestion_details.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/standalone_browser/feature_refs.h"
#include "chromeos/ash/services/ime/public/cpp/assistive_suggestions.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/base/ime/ash/ime_assistive_window_handler_interface.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/test/test_views_delegate.h"
#include "ui/wm/core/window_util.h"
namespace {
const char16_t kAnnounceString[] = u"announce string";
} // namespace
namespace ash {
namespace input_method {
constexpr size_t kShowSuggestionDelay = 5;
class MockDelegate : public AssistiveWindowControllerDelegate {
public:
~MockDelegate() override = default;
void AssistiveWindowButtonClicked(
const ui::ime::AssistiveWindowButton& button) const override {}
void AssistiveWindowChanged(
const ash::ime::AssistiveWindow& window) const override {}
};
class TestAnnouncementView : public ui::ime::AnnouncementView {
public:
TestAnnouncementView() = default;
void VerifyAnnouncement(const std::u16string& expected_text) {
EXPECT_EQ(text_, expected_text);
}
void Announce(const std::u16string& text) override { text_ = text; }
void AnnounceAfterDelay(const std::u16string& text,
base::TimeDelta delay) override {
text_ = text;
}
private:
std::u16string text_;
};
class AssistiveWindowControllerTest : public ChromeAshTestBase {
protected:
AssistiveWindowControllerTest()
: ChromeAshTestBase(std::make_unique<content::BrowserTaskEnvironment>(
base::test::TaskEnvironment::TimeSource::MOCK_TIME)) {}
~AssistiveWindowControllerTest() override = default;
void SetUp() override {
ChromeAshTestBase::SetUp();
std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithId(1));
wm::ActivateWindow(window.get());
profile_ = std::make_unique<TestingProfile>();
announcement_view_ = std::make_unique<TestAnnouncementView>();
controller_ = std::make_unique<AssistiveWindowController>(
delegate_.get(), profile_.get(), announcement_view_.get());
IMEBridge::Get()->SetAssistiveWindowHandler(controller_.get());
// TODO(crbug.com/40138718): Create MockSuggestionWindowView to be
// independent of SuggestionWindowView's implementation.
static_cast<views::TestViewsDelegate*>(views::ViewsDelegate::GetInstance())
->set_layout_provider(ChromeLayoutProvider::CreateLayoutProvider());
}
void TearDown() override {
controller_.reset();
ChromeAshTestBase::TearDown();
}
std::vector<std::u16string> Candidates() {
std::vector<std::u16string> candidates;
for (int i = 0; i < 3; i++) {
std::string candidate = base::NumberToString(i);
candidates.push_back(base::UTF8ToUTF16(candidate));
}
return candidates;
}
void InitEmojiSuggestionWindow() {
emoji_window_.type = ash::ime::AssistiveWindowType::kEmojiSuggestion;
emoji_window_.visible = true;
emoji_window_.candidates = Candidates();
}
void InitEmojiButton() {
emoji_button_.window_type = ash::ime::AssistiveWindowType::kEmojiSuggestion;
emoji_button_.announce_string = kAnnounceString;
}
void EnableLacros() {
feature_list_.Reset();
feature_list_.InitWithFeatures(
/*enabled_features=*/ash::standalone_browser::GetFeatureRefs(),
/*disabled_features=*/{});
scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
ash::switches::kEnableLacrosForTesting);
}
void WaitForSuggestionWindowDelay() {
task_environment()->FastForwardBy(
base::Milliseconds(kShowSuggestionDelay + 1));
}
base::test::ScopedFeatureList feature_list_;
base::test::ScopedCommandLine scoped_command_line_;
std::unique_ptr<AssistiveWindowController> controller_;
std::unique_ptr<MockDelegate> delegate_ = std::make_unique<MockDelegate>();
std::unique_ptr<TestingProfile> profile_;
const std::u16string suggestion_ = u"test";
ui::ime::AssistiveWindowButton emoji_button_;
AssistiveWindowProperties emoji_window_;
std::unique_ptr<TestAnnouncementView> announcement_view_;
};
TEST_F(AssistiveWindowControllerTest, ShowSuggestionDelaysWindowDisplay) {
ui::ime::SuggestionDetails details;
details.text = u"asdf";
details.confirmed_length = 3;
controller_->ShowSuggestion(details);
ui::ime::SuggestionWindowView* window_before_delay =
controller_->GetSuggestionWindowViewForTesting();
WaitForSuggestionWindowDelay();
ui::ime::SuggestionWindowView* window_after_delay =
controller_->GetSuggestionWindowViewForTesting();
EXPECT_EQ(window_before_delay, nullptr);
EXPECT_NE(window_after_delay, nullptr);
}
TEST_F(AssistiveWindowControllerTest,
SetBoundsAfterShowSuggestionCancelsDelay) {
ui::ime::SuggestionDetails details;
details.text = u"asdf";
details.confirmed_length = 3;
gfx::Rect caret_bounds(0, 0, 100, 100);
controller_->ShowSuggestion(details);
controller_->SetBounds(Bounds{.caret = caret_bounds});
ui::ime::SuggestionWindowView* suggestion_window_view =
controller_->GetSuggestionWindowViewForTesting();
EXPECT_NE(suggestion_window_view, nullptr);
}
TEST_F(AssistiveWindowControllerTest, ShowSuggestionSetsConfirmedLength) {
ui::ime::SuggestionDetails details;
details.text = u"asdf";
details.confirmed_length = 3;
controller_->ShowSuggestion(details);
EXPECT_EQ(controller_->GetConfirmedLength(), 3u);
}
TEST_F(AssistiveWindowControllerTest, ConfirmedLength0SetsBoundsToCaretBounds) {
ui::ime::SuggestionDetails details;
details.text = suggestion_;
details.confirmed_length = 0;
controller_->ShowSuggestion(details);
WaitForSuggestionWindowDelay();
ui::ime::SuggestionWindowView* suggestion_view =
controller_->GetSuggestionWindowViewForTesting();
ASSERT_NE(suggestion_view, nullptr);
gfx::Rect current_bounds = suggestion_view->GetAnchorRect();
gfx::Rect caret_bounds(0, 0, 100, 100);
Bounds bounds;
bounds.caret = caret_bounds;
controller_->SetBounds(bounds);
EXPECT_NE(suggestion_view->GetAnchorRect(), current_bounds);
EXPECT_EQ(suggestion_view->GetAnchorRect(), caret_bounds);
}
TEST_F(AssistiveWindowControllerTest, ConfirmedLengthNSetsBoundsToCaretBounds) {
ui::ime::SuggestionDetails details;
details.text = suggestion_;
details.confirmed_length = 1;
controller_->ShowSuggestion(details);
WaitForSuggestionWindowDelay();
ui::ime::SuggestionWindowView* suggestion_view =
controller_->GetSuggestionWindowViewForTesting();
ASSERT_NE(suggestion_view, nullptr);
gfx::Rect current_bounds = suggestion_view->GetAnchorRect();
gfx::Rect caret_bounds(0, 0, 100, 100);
Bounds bounds;
bounds.caret = caret_bounds;
controller_->SetBounds(bounds);
EXPECT_NE(suggestion_view->GetAnchorRect(), current_bounds);
EXPECT_EQ(suggestion_view->GetAnchorRect(), caret_bounds);
}
TEST_F(AssistiveWindowControllerTest, WindowTracksCaretBounds) {
ui::ime::SuggestionDetails details;
details.text = suggestion_;
details.confirmed_length = 0;
controller_->ShowSuggestion(details);
WaitForSuggestionWindowDelay();
ui::ime::SuggestionWindowView* suggestion_view =
controller_->GetSuggestionWindowViewForTesting();
ASSERT_NE(suggestion_view, nullptr);
gfx::Rect current_bounds = suggestion_view->GetAnchorRect();
gfx::Rect caret_bounds_after_one_key(current_bounds.width() + 1,
current_bounds.height());
gfx::Rect caret_bounds_after_two_key(current_bounds.width() + 2,
current_bounds.height());
// One char entered
controller_->SetBounds(Bounds{.caret = caret_bounds_after_one_key});
// Mimic tracking the last suggestion
details.confirmed_length = 1;
controller_->ShowSuggestion(details);
// Second char entered to text input
controller_->SetBounds(Bounds{.caret = caret_bounds_after_two_key});
// Anchor should track the new caret position.
EXPECT_EQ(suggestion_view->GetAnchorRect(), caret_bounds_after_two_key);
}
TEST_F(AssistiveWindowControllerTest,
SuggestionViewBoundIsResetAfterHideSuggestionThenShowAgain) {
// Sets up suggestion_view with confirmed_length = 1.
ui::ime::SuggestionDetails details;
details.text = suggestion_;
details.confirmed_length = 1;
controller_->ShowSuggestion(details);
WaitForSuggestionWindowDelay();
gfx::Rect current_bounds =
controller_->GetSuggestionWindowViewForTesting()->GetAnchorRect();
controller_->HideSuggestion();
// Create new suggestion window.
AssistiveWindowProperties properties;
properties.type = ash::ime::AssistiveWindowType::kEmojiSuggestion;
properties.visible = true;
properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(properties);
gfx::Rect new_caret_bounds(current_bounds.width() + 1,
current_bounds.height());
Bounds bounds;
bounds.caret = new_caret_bounds;
controller_->SetBounds(bounds);
EXPECT_EQ(controller_->GetSuggestionWindowViewForTesting()->GetAnchorRect(),
new_caret_bounds);
}
TEST_F(AssistiveWindowControllerTest, SetsUndoWindowAnchorRectCorrectly) {
gfx::Rect autocorrect_bounds(1, 1);
gfx::Rect caret_bounds(2, 2);
Bounds bounds;
bounds.caret = caret_bounds;
bounds.autocorrect = autocorrect_bounds;
controller_->SetBounds(bounds);
AssistiveWindowProperties window;
window.type = ash::ime::AssistiveWindowType::kUndoWindow;
window.visible = true;
controller_->SetAssistiveWindowProperties(window);
ASSERT_TRUE(controller_->GetUndoWindowForTesting() != nullptr);
autocorrect_bounds.Inset(-4);
EXPECT_EQ(controller_->GetUndoWindowForTesting()->GetAnchorRect(),
autocorrect_bounds);
}
TEST_F(AssistiveWindowControllerTest, SetsEmojiWindowOrientationVertical) {
// Create new suggestion window.
AssistiveWindowProperties properties;
properties.type = ash::ime::AssistiveWindowType::kEmojiSuggestion;
properties.visible = true;
properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kVertical);
}
TEST_F(AssistiveWindowControllerTest,
SetsPersonalInfoWindowOrientationVertical) {
// Create new suggestion window.
AssistiveWindowProperties properties;
properties.type = ash::ime::AssistiveWindowType::kPersonalInfoSuggestion;
properties.visible = true;
properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kVertical);
}
TEST_F(AssistiveWindowControllerTest, SetsMultiWordWindowOrientationVertical) {
// Create new suggestion window.
AssistiveWindowProperties properties;
properties.type = ash::ime::AssistiveWindowType::kMultiWordSuggestion;
properties.visible = true;
properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kVertical);
}
TEST_F(AssistiveWindowControllerTest,
SetsDiacriticsWindowOrientationHorizontal) {
// Create new suggestion window.
AssistiveWindowProperties properties;
properties.type =
ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion;
properties.visible = true;
properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kHorizontal);
}
TEST_F(AssistiveWindowControllerTest,
SetsWindowOrientationHorizontalWhenVerticalWindowAlreadyInitialised) {
AssistiveWindowProperties init_vertical_properties;
init_vertical_properties.type =
ash::ime::AssistiveWindowType::kMultiWordSuggestion;
init_vertical_properties.visible = true;
init_vertical_properties.candidates =
std::vector<std::u16string>({u"vertical"});
controller_->SetAssistiveWindowProperties(init_vertical_properties);
AssistiveWindowProperties horizontal_properties;
horizontal_properties.type =
ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion;
horizontal_properties.visible = true;
horizontal_properties.candidates =
std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(horizontal_properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kHorizontal);
}
TEST_F(AssistiveWindowControllerTest,
SetsWindowOrientationVerticalWhenHorizontalWindowAlreadyInitialised) {
AssistiveWindowProperties init_horizontal_properties;
init_horizontal_properties.type =
ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion;
init_horizontal_properties.visible = true;
init_horizontal_properties.candidates =
std::vector<std::u16string>({u"horizontal"});
controller_->SetAssistiveWindowProperties(init_horizontal_properties);
AssistiveWindowProperties vertical_properties;
vertical_properties.type =
ash::ime::AssistiveWindowType::kMultiWordSuggestion;
vertical_properties.visible = true;
vertical_properties.candidates = std::vector<std::u16string>({u"candidate"});
controller_->SetAssistiveWindowProperties(vertical_properties);
ASSERT_TRUE(controller_->GetSuggestionWindowViewForTesting() != nullptr);
views::BoxLayout::Orientation layout_orientation =
static_cast<views::BoxLayout*>(
controller_->GetSuggestionWindowViewForTesting()
->multiple_candidate_area_for_testing()
->GetLayoutManager())
->GetOrientation();
EXPECT_EQ(layout_orientation, views::BoxLayout::Orientation::kVertical);
}
TEST_F(AssistiveWindowControllerTest,
AnnouncesWhenSetButtonHighlightedInEmojiWindowHasAnnounceString) {
profile_->GetPrefs()->SetBoolean(
ash::prefs::kAccessibilitySpokenFeedbackEnabled, true);
InitEmojiSuggestionWindow();
InitEmojiButton();
controller_->SetAssistiveWindowProperties(emoji_window_);
controller_->SetButtonHighlighted(emoji_button_, true);
task_environment()->RunUntilIdle();
announcement_view_->VerifyAnnouncement(kAnnounceString);
}
TEST_F(
AssistiveWindowControllerTest,
DoesNotAnnounceWhenSetButtonHighlightedInEmojiWindowDoesNotHaveAnnounceString) {
profile_->GetPrefs()->SetBoolean(
ash::prefs::kAccessibilitySpokenFeedbackEnabled, true);
InitEmojiSuggestionWindow();
InitEmojiButton();
emoji_button_.announce_string.clear();
controller_->SetAssistiveWindowProperties(emoji_window_);
controller_->SetButtonHighlighted(emoji_button_, true);
task_environment()->RunUntilIdle();
announcement_view_->VerifyAnnouncement(std::u16string());
}
TEST_F(AssistiveWindowControllerTest,
AnnouncesWhenSetButtonHighlightedInUndoWindowHasAnnounceString) {
profile_->GetPrefs()->SetBoolean(
ash::prefs::kAccessibilitySpokenFeedbackEnabled, true);
AssistiveWindowProperties window;
window.type = ash::ime::AssistiveWindowType::kUndoWindow;
window.visible = true;
ui::ime::AssistiveWindowButton button;
button.window_type = ash::ime::AssistiveWindowType::kUndoWindow;
button.announce_string = kAnnounceString;
controller_->SetAssistiveWindowProperties(window);
controller_->SetButtonHighlighted(button, true);
task_environment()->RunUntilIdle();
announcement_view_->VerifyAnnouncement(kAnnounceString);
}
TEST_F(
AssistiveWindowControllerTest,
DoesNotAnnounceWhenSetButtonHighlightedAndSuggestionWindowViewIsNotActive) {
profile_->GetPrefs()->SetBoolean(
ash::prefs::kAccessibilitySpokenFeedbackEnabled, true);
InitEmojiButton();
controller_->SetButtonHighlighted(emoji_button_, true);
task_environment()->RunUntilIdle();
announcement_view_->VerifyAnnouncement(std::u16string());
}
} // namespace input_method
} // namespace ash