#include "ash/system/ime_menu/ime_menu_tray.h"
#include <string_view>
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/accessibility/a11y_feature_type.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/ime/test_ime_controller_client.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/ime_info.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/ime_menu/ime_list_view.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/test/ash_test_base.h"
#include "base/containers/contains.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "components/session_manager/session_manager_types.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/emoji/emoji_panel_helper.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/devices/device_data_manager_test_api.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/devices/keyboard_device.h"
#include "ui/events/devices/touchscreen_device.h"
#include "ui/events/event.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
const int kEmojiButtonId = 1;
const int kSettingsButtonId = 2;
const int kVoiceButtonId = 3;
ImeMenuTray* GetTray() {
return StatusAreaWidgetTestHelper::GetStatusAreaWidget()->ime_menu_tray();
}
void SetCurrentIme(const std::string& current_ime_id,
const std::vector<ImeInfo>& available_imes) {
std::vector<ImeInfo> available_ime_ptrs;
for (const auto& ime : available_imes) {
available_ime_ptrs.push_back(ime);
}
Shell::Get()->ime_controller()->RefreshIme(current_ime_id,
std::move(available_ime_ptrs),
std::vector<ImeMenuItem>());
}
}
class ImeMenuTrayTest : public AshTestBase {
public:
ImeMenuTrayTest() = default;
ImeMenuTrayTest(const ImeMenuTrayTest&) = delete;
ImeMenuTrayTest& operator=(const ImeMenuTrayTest&) = delete;
~ImeMenuTrayTest() override = default;
protected:
bool IsVisible() { return GetTray()->GetVisible(); }
std::u16string_view GetTrayText() { return GetTray()->label_->GetText(); }
bool IsTrayBackgroundActive() { return GetTray()->is_active(); }
bool IsBubbleShown() { return GetTray()->GetBubbleView() != nullptr; }
bool IsEmojiEnabled() { return GetTray()->is_emoji_enabled_; }
bool IsHandwritingEnabled() { return GetTray()->is_handwriting_enabled_; }
bool IsVoiceEnabled() { return GetTray()->is_voice_enabled_; }
views::Button* GetEmojiButton() const {
return static_cast<views::Button*>(
GetTray()->bubble_->bubble_view()->GetViewByID(kEmojiButtonId));
}
views::View* GetSettingsButton() const {
auto* bubble = GetTray()->GetBubbleView();
return bubble == nullptr ? nullptr : bubble->GetViewByID(kSettingsButtonId);
}
views::View* GetVoiceButton() const {
return static_cast<views::View*>(
GetTray()->bubble_->bubble_view()->GetViewByID(kVoiceButtonId));
}
void SetUpKioskSession() {
SessionInfo info;
info.is_running_in_app_mode = true;
info.state = session_manager::SessionState::ACTIVE;
Shell::Get()->session_controller()->SetSessionInfo(info);
}
void ExpectValidImeList(const std::vector<ImeInfo>& expected_imes,
const ImeInfo& expected_current_ime) {
const std::map<views::View*, std::string>& ime_map =
ImeListViewTestApi(GetTray()->ime_list_view_).ime_map();
EXPECT_EQ(expected_imes.size(), ime_map.size());
std::vector<std::string> expected_ime_ids;
for (const auto& ime : expected_imes) {
expected_ime_ids.push_back(ime.id);
}
for (const auto& ime : ime_map) {
EXPECT_TRUE(base::Contains(expected_ime_ids, ime.second));
ui::AXNodeData node_data;
ime.first->GetViewAccessibility().GetAccessibleNodeData(&node_data);
const auto checked_state = static_cast<ax::mojom::CheckedState>(
node_data.GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
if (checked_state == ax::mojom::CheckedState::kTrue) {
EXPECT_EQ(expected_current_ime.id, ime.second);
}
}
}
void FocusInInputContext(ui::TextInputType input_type) {
IMEBridge::Get()->SetCurrentInputContext(
TextInputMethod::InputContext(input_type));
}
bool MenuHasOnScreenKeyboardToggle() const {
if (!GetTray()->ime_list_view_) {
return false;
}
return ImeListViewTestApi(GetTray()->ime_list_view_).GetToggleView();
}
};
TEST_F(ImeMenuTrayTest, ImeMenuTrayVisibility) {
ASSERT_FALSE(IsVisible());
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
EXPECT_TRUE(IsVisible());
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(false);
EXPECT_FALSE(IsVisible());
}
TEST_F(ImeMenuTrayTest, TrayLabelTest) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ImeInfo info1;
info1.id = "ime1";
info1.name = u"English";
info1.short_name = u"US";
info1.third_party = false;
ImeInfo info2;
info2.id = "ime2";
info2.name = u"English UK";
info2.short_name = u"UK";
info2.third_party = true;
SetCurrentIme("ime1", {info1, info2});
EXPECT_EQ(u"US", GetTrayText());
SetCurrentIme("ime2", {info1, info2});
EXPECT_EQ(u"UK*", GetTrayText());
}
TEST_F(ImeMenuTrayTest, TrayLabelExludesDictation) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ImeInfo info1;
info1.id = "ime1";
info1.name = u"English";
info1.short_name = u"US";
info1.third_party = false;
ImeInfo info2;
info2.id = "ime2";
info2.name = u"English UK";
info2.short_name = u"UK";
info2.third_party = true;
ImeInfo dictation;
dictation.id = "_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
dictation.name = u"Dictation";
SetCurrentIme("ime1", {info1, dictation, info2});
EXPECT_EQ(u"US", GetTrayText());
SetCurrentIme("ime2", {info1, dictation, info2});
EXPECT_EQ(u"UK*", GetTrayText());
SetCurrentIme(dictation.id, {info1, dictation, info2});
EXPECT_EQ(u"", GetTrayText());
}
TEST_F(ImeMenuTrayTest, PerformActionGestureTap) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
StatusAreaWidget* status = StatusAreaWidgetTestHelper::GetStatusAreaWidget();
EXPECT_FALSE(status->ShouldShowShelf());
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
EXPECT_TRUE(status->ShouldShowShelf());
GestureTapOn(GetTray());
EXPECT_FALSE(IsTrayBackgroundActive());
EXPECT_FALSE(IsBubbleShown());
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(false);
EXPECT_FALSE(IsVisible());
EXPECT_FALSE(IsBubbleShown());
EXPECT_FALSE(IsTrayBackgroundActive());
EXPECT_FALSE(status->ShouldShowShelf());
}
TEST_F(ImeMenuTrayTest, PerformActionLeftClick) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
LeftClickOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
LeftClickOn(GetTray());
EXPECT_FALSE(IsTrayBackgroundActive());
EXPECT_FALSE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, RefreshImeWithListViewCreated) {
GetTray()->SetVisiblePreferred(true);
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
ImeInfo info1, info2, info3;
info1.id = "ime1";
info1.name = u"English";
info1.short_name = u"US";
info1.third_party = false;
info2.id = "ime2";
info2.name = u"English UK";
info2.short_name = u"UK";
info2.third_party = true;
info3.id = "ime3";
info3.name = u"Pinyin";
info3.short_name = u"拼";
info3.third_party = false;
std::vector<ImeInfo> ime_info_list{info1, info2, info3};
SetCurrentIme("ime1", ime_info_list);
EXPECT_EQ(u"US", GetTrayText());
ExpectValidImeList(ime_info_list, info1);
SetCurrentIme("ime3", ime_info_list);
EXPECT_EQ(u"拼", GetTrayText());
ExpectValidImeList(ime_info_list, info3);
GestureTapOn(GetTray());
EXPECT_FALSE(IsTrayBackgroundActive());
EXPECT_FALSE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, QuitChromeWithMenuOpen) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, TestAccelerator) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
AcceleratorAction::kToggleImeMenuBubble, {});
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
GestureTapOn(GetTray());
EXPECT_FALSE(IsTrayBackgroundActive());
EXPECT_FALSE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, ShowingEmojiKeysetHidesBubble) {
ui::SetShowEmojiKeyboardCallback(base::DoNothing());
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
TestImeControllerClient client;
Shell::Get()->ime_controller()->SetClient(&client);
GetTray()->ShowKeyboardWithKeyset(input_method::ImeKeyset::kEmoji);
EXPECT_FALSE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, ImeBubbleAccelerator) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
EXPECT_FALSE(IsBubbleShown());
PressAndReleaseKey(ui::VKEY_K, ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(IsBubbleShown());
PressAndReleaseKey(ui::VKEY_K, ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsBubbleShown());
}
TEST_F(ImeMenuTrayTest, TapEmojiButton) {
int call_count = 0;
ui::SetShowEmojiKeyboardCallback(base::BindLambdaForTesting(
[&](ui::EmojiPickerCategory unused, ui::EmojiPickerFocusBehavior,
const std::string&) { ++call_count; }));
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true , true ,
true , true );
GestureTapOn(GetTray());
views::Button* emoji_button = GetEmojiButton();
ASSERT_TRUE(emoji_button);
GestureTapOn(emoji_button);
EXPECT_FALSE(IsBubbleShown());
EXPECT_EQ(call_count, 1);
}
TEST_F(ImeMenuTrayTest, ShouldShowBottomButtons) {
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true , true ,
true , true );
FocusInInputContext(ui::TEXT_INPUT_TYPE_TEXT);
GetTray()->ShowBubble();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetTray()->AnyBottomButtonShownForTest());
EXPECT_TRUE(IsEmojiEnabled());
EXPECT_TRUE(IsHandwritingEnabled());
EXPECT_TRUE(IsVoiceEnabled());
FocusInInputContext(ui::TEXT_INPUT_TYPE_PASSWORD);
GetTray()->CloseBubble();
GetTray()->ShowBubble();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetTray()->AnyBottomButtonShownForTest());
EXPECT_FALSE(IsEmojiEnabled());
EXPECT_FALSE(IsHandwritingEnabled());
EXPECT_FALSE(IsVoiceEnabled());
}
TEST_F(ImeMenuTrayTest, ShouldShowBottomButtonsSeperate) {
FocusInInputContext(ui::TEXT_INPUT_TYPE_TEXT);
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true , false ,
true , true );
GetTray()->ShowBubble();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetTray()->AnyBottomButtonShownForTest());
EXPECT_FALSE(IsEmojiEnabled());
EXPECT_TRUE(IsHandwritingEnabled());
EXPECT_TRUE(IsVoiceEnabled());
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true , true ,
false , false );
GetTray()->CloseBubble();
GetTray()->ShowBubble();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetTray()->AnyBottomButtonShownForTest());
EXPECT_TRUE(IsEmojiEnabled());
EXPECT_FALSE(IsHandwritingEnabled());
EXPECT_FALSE(IsVoiceEnabled());
}
TEST_F(ImeMenuTrayTest, KioskImeTraySettingsButton) {
SetUpKioskSession();
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
GestureTapOn(GetTray());
views::View* settings_button = GetSettingsButton();
EXPECT_FALSE(settings_button);
}
TEST_F(ImeMenuTrayTest, UserSessionImeTraySettingsButton) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
GestureTapOn(GetTray());
views::View* settings_button = GetSettingsButton();
EXPECT_TRUE(settings_button);
}
TEST_F(ImeMenuTrayTest, ShowOnScreenKeyboardToggle) {
Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
ASSERT_TRUE(IsVisible());
ASSERT_FALSE(IsTrayBackgroundActive());
GestureTapOn(GetTray());
EXPECT_TRUE(IsTrayBackgroundActive());
EXPECT_TRUE(IsBubbleShown());
EXPECT_FALSE(MenuHasOnScreenKeyboardToggle());
std::vector<ui::TouchscreenDevice> screens;
screens.push_back(
ui::TouchscreenDevice(1, ui::InputDeviceType::INPUT_DEVICE_INTERNAL,
"Touchscreen", gfx::Size(1024, 768), 0));
ui::DeviceDataManagerTestApi().SetTouchscreenDevices(screens);
std::vector<ui::KeyboardDevice> keyboard_devices;
keyboard_devices.push_back(ui::KeyboardDevice(
1, ui::InputDeviceType::INPUT_DEVICE_USB, "external keyboard"));
ui::DeviceDataManagerTestApi().SetKeyboardDevices(keyboard_devices);
EXPECT_FALSE(IsBubbleShown());
GestureTapOn(GetTray());
EXPECT_TRUE(IsBubbleShown());
EXPECT_TRUE(MenuHasOnScreenKeyboardToggle());
ImeInfo info;
info.id = "ime";
info.name = u"English UK";
info.short_name = u"UK";
info.third_party = true;
SetCurrentIme("ime", {info});
EXPECT_TRUE(MenuHasOnScreenKeyboardToggle());
keyboard_devices.push_back(ui::KeyboardDevice(
1, ui::InputDeviceType::INPUT_DEVICE_USB, "external keyboard"));
keyboard_devices.push_back(ui::KeyboardDevice(
1, ui::InputDeviceType::INPUT_DEVICE_INTERNAL, "internal keyboard"));
ui::DeviceDataManagerTestApi().SetKeyboardDevices(keyboard_devices);
EXPECT_FALSE(IsBubbleShown());
GestureTapOn(GetTray());
EXPECT_TRUE(IsBubbleShown());
EXPECT_FALSE(MenuHasOnScreenKeyboardToggle());
}
TEST_F(ImeMenuTrayTest, ShowVoiceButtonWhenDictationDisabled) {
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true,
true,
true, true);
Shell::Get()
->accessibility_controller()
->GetFeature(A11yFeatureType::kDictation)
.SetEnabled(false);
GetTray()->ShowBubble();
views::View* voice_button = GetVoiceButton();
EXPECT_TRUE(voice_button);
}
TEST_F(ImeMenuTrayTest, HideVoiceButtonWhenDictationEnabled) {
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true,
true,
true, true);
Shell::Get()
->accessibility_controller()
->GetFeature(A11yFeatureType::kDictation)
.SetEnabled(true);
GetTray()->ShowBubble();
views::View* voice_button = GetVoiceButton();
EXPECT_FALSE(voice_button);
}
TEST_F(ImeMenuTrayTest, ImeMenuHasBottomInsetsOnLockScreen) {
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true , true ,
true , true );
GetTray()->ShowBubble();
gfx::Insets* container_margins =
GetTray()
->GetBubbleView()
->GetViewByID(VIEW_ID_IME_LIST_VIEW_SCROLLER)
->GetProperty(views::kMarginsKey);
EXPECT_EQ(container_margins->bottom(), 0);
BlockUserSession(BLOCKED_BY_LOCK_SCREEN);
GetTray()->ShowBubble();
container_margins = GetTray()
->GetBubbleView()
->GetViewByID(VIEW_ID_IME_LIST_VIEW_SCROLLER)
->GetProperty(views::kMarginsKey);
EXPECT_GT(container_margins->bottom(), 0);
}
TEST_F(ImeMenuTrayTest, AccessibleNames) {
Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
true, true,
true, true);
{
ui::AXNodeData node_data;
GetTray()->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ(node_data.GetString16Attribute(ax::mojom::StringAttribute::kName),
l10n_util::GetStringUTF16(IDS_ASH_IME_MENU_ACCESSIBLE_NAME));
}
GetTray()->ShowBubble();
TrayBubbleView* bubble_view = GetTray()->GetBubbleView();
{
ui::AXNodeData node_data;
bubble_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ(node_data.GetString16Attribute(ax::mojom::StringAttribute::kName),
GetTray()->GetAccessibleNameForBubble());
}
}
}