| // 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/app_list_model_provider.h" |
| #include "ash/app_list/model/app_list_item.h" |
| #include "ash/app_list/views/apps_grid_view.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/public/cpp/tablet_mode.h" |
| #include "ash/public/cpp/test/app_list_test_api.h" |
| #include "ash/public/cpp/test/shell_test_api.h" |
| #include "ash/shell.h" |
| #include "base/memory/raw_ptr_exclusion.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "chrome/browser/ash/accessibility/spoken_feedback_browsertest.h" |
| #include "chrome/browser/ash/app_list/app_list_client_impl.h" |
| #include "chrome/browser/ash/app_list/chrome_app_list_model_updater.h" |
| #include "chrome/browser/ash/app_list/search/chrome_search_result.h" |
| #include "chrome/browser/ash/app_list/search/search_controller.h" |
| #include "chrome/browser/ash/app_list/search/search_provider.h" |
| #include "chrome/browser/ash/app_list/test/chrome_app_list_test_support.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/test/base/interactive_test_utils.h" |
| #include "components/user_manager/user_names.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/test/browser_test.h" |
| #include "extensions/browser/browsertest_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/display/display.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/test/event_generator.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| void SendKeyPressWithShiftAndControl(ui::KeyboardCode key) { |
| ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| nullptr, key, true, true, false, false))); |
| } |
| |
| class TestSearchResult : public ChromeSearchResult { |
| public: |
| TestSearchResult(const std::string& id, double relevance) { |
| set_id(id); |
| SetTitle(base::UTF8ToUTF16(id)); |
| SetDisplayScore(relevance); |
| } |
| |
| TestSearchResult(const TestSearchResult&) = delete; |
| TestSearchResult& operator=(const TestSearchResult&) = delete; |
| |
| ~TestSearchResult() override {} |
| |
| // ChromeSearchResult overrides: |
| void Open(int event_flags) override {} |
| }; |
| |
| class TestSearchProvider : public app_list::SearchProvider { |
| public: |
| TestSearchProvider(const std::string& prefix, |
| ChromeSearchResult::DisplayType display_type, |
| ChromeSearchResult::Category category, |
| ChromeSearchResult::ResultType result_type) |
| : prefix_(prefix), |
| display_type_(display_type), |
| category_(category), |
| result_type_(result_type) {} |
| |
| TestSearchProvider(const TestSearchProvider&) = delete; |
| TestSearchProvider& operator=(const TestSearchProvider&) = delete; |
| |
| ~TestSearchProvider() override {} |
| |
| // SearchProvider overrides: |
| void Start(const std::u16string& query) override { |
| auto create_result = |
| [this](int index) -> std::unique_ptr<ChromeSearchResult> { |
| const std::string id = |
| base::StringPrintf("%s %d", prefix_.c_str(), index); |
| double relevance = 1.0f - index / 100.0; |
| auto result = std::make_unique<TestSearchResult>(id, relevance); |
| |
| result->SetDisplayType(display_type_); |
| result->SetCategory(category_); |
| result->SetResultType(result_type_); |
| |
| return result; |
| }; |
| |
| std::vector<std::unique_ptr<ChromeSearchResult>> results; |
| for (size_t i = 0; i < count_ + best_match_count_; ++i) { |
| std::unique_ptr<ChromeSearchResult> result = create_result(i); |
| result->SetBestMatch(i < best_match_count_); |
| if (result_type_ == ChromeSearchResult::ResultType::kImageSearch) { |
| result->SetIcon(ChromeSearchResult::IconInfo( |
| ui::ImageModel::FromVectorIcon(vector_icons::kGoogleColorIcon), |
| /*dimension=*/100)); |
| } |
| results.push_back(std::move(result)); |
| } |
| |
| SwapResults(&results); |
| } |
| |
| ChromeSearchResult::ResultType ResultType() const override { |
| return result_type_; |
| } |
| |
| void set_count(size_t count) { count_ = count; } |
| void set_best_match_count(size_t count) { best_match_count_ = count; } |
| |
| private: |
| std::string prefix_; |
| size_t count_ = 0; |
| size_t best_match_count_ = 0; |
| ChromeSearchResult::DisplayType display_type_; |
| ChromeSearchResult::Category category_; |
| ChromeSearchResult::ResultType result_type_; |
| }; |
| |
| // Adds two test providers to `search_controller` - one for app results, and |
| // another one for omnibox results. Returns pointers to created providers |
| // through `apps_provder_ptr` and `web_provider_ptr`. |
| void InitializeTestSearchProviders( |
| app_list::SearchController* search_controller, |
| TestSearchProvider** apps_provider_ptr, |
| TestSearchProvider** web_provider_ptr, |
| TestSearchProvider** image_provider_ptr) { |
| std::unique_ptr<TestSearchProvider> apps_provider = |
| std::make_unique<TestSearchProvider>( |
| "app", ChromeSearchResult::DisplayType::kList, |
| ChromeSearchResult::Category::kApps, |
| ChromeSearchResult::ResultType::kInstalledApp); |
| *apps_provider_ptr = apps_provider.get(); |
| search_controller->AddProvider(std::move(apps_provider)); |
| |
| std::unique_ptr<TestSearchProvider> web_provider = |
| std::make_unique<TestSearchProvider>( |
| "item", ChromeSearchResult::DisplayType::kList, |
| ChromeSearchResult::Category::kWeb, |
| ChromeSearchResult::ResultType::kOmnibox); |
| *web_provider_ptr = web_provider.get(); |
| search_controller->AddProvider(std::move(web_provider)); |
| |
| std::unique_ptr<TestSearchProvider> image_provider = |
| std::make_unique<TestSearchProvider>( |
| "image", ChromeSearchResult::DisplayType::kImage, |
| ChromeSearchResult::Category::kFiles, |
| ChromeSearchResult::ResultType::kImageSearch); |
| *image_provider_ptr = image_provider.get(); |
| search_controller->AddProvider(std::move(image_provider)); |
| } |
| |
| } // namespace |
| |
| enum SpokenFeedbackAppListTestVariant { kTestAsNormalUser, kTestAsGuestUser }; |
| |
| class SpokenFeedbackAppListBaseTest : public LoggedInSpokenFeedbackTest { |
| public: |
| explicit SpokenFeedbackAppListBaseTest( |
| SpokenFeedbackAppListTestVariant variant) |
| : variant_(variant) {} |
| ~SpokenFeedbackAppListBaseTest() override = default; |
| |
| // LoggedInSpokenFeedbackTest: |
| void SetUp() override { |
| // Do not run expand arrow hinting animation to avoid msan test crash. |
| // (See https://crbug.com/926038) |
| zero_duration_mode_ = |
| std::make_unique<ui::ScopedAnimationDurationScaleMode>( |
| ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); |
| |
| // Disable the app list nudge in the spoken feedback app list test. |
| AppListTestApi().DisableAppListNudge(true); |
| |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kProductivityLauncherImageSearch); |
| |
| LoggedInSpokenFeedbackTest::SetUp(); |
| } |
| |
| void TearDown() override { |
| LoggedInSpokenFeedbackTest::TearDown(); |
| zero_duration_mode_.reset(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| if (variant_ == kTestAsGuestUser) { |
| command_line->AppendSwitch(switches::kGuestSession); |
| command_line->AppendSwitch(::switches::kIncognito); |
| command_line->AppendSwitchASCII(switches::kLoginProfile, "user"); |
| command_line->AppendSwitchASCII( |
| switches::kLoginUser, user_manager::GuestAccountId().GetUserEmail()); |
| } |
| } |
| |
| void SetUpOnMainThread() override { |
| LoggedInSpokenFeedbackTest::SetUpOnMainThread(); |
| AppListClientImpl::GetInstance()->UpdateProfile(); |
| } |
| |
| // Populate apps grid with |num| items. |
| void PopulateApps(size_t num) { |
| // Only folders or page breaks are allowed to be added from the Ash side. |
| // Therefore new apps should be added through `ChromeAppListModelUpdater`. |
| ::test::PopulateDummyAppListItems(num); |
| } |
| |
| // Moves to the first test app in a populated list of apps. |
| // Returns the index of that item. |
| int MoveToFirstTestApp() { |
| // Focus the shelf. This selects the launcher button. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| // Activate the launcher button. This opens bubble launcher. |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); }); |
| sm_.ExpectSpeech("Button"); |
| |
| int test_item_index = 0; |
| AppListItem* test_item = FindItemByName("app 0", &test_item_index); |
| EXPECT_TRUE(test_item); |
| |
| // Skip over apps that were installed before the test item. |
| // This selects the first app installed by the test. |
| for (int i = 0; i < test_item_index; ++i) { |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); }); |
| } |
| sm_.ExpectSpeech("app 0"); |
| sm_.ExpectSpeech("Button"); |
| |
| return test_item_index; |
| } |
| |
| AppListItem* FindItemByName(const std::string& name, int* index) { |
| AppListModel* const model = AppListModelProvider::Get()->model(); |
| AppListItemList* item_list = model->top_level_item_list(); |
| for (size_t i = 0; i < item_list->item_count(); ++i) { |
| if (item_list->item_at(i)->name() == name) { |
| if (index) { |
| *index = i; |
| } |
| return item_list->item_at(i); |
| } |
| } |
| return nullptr; |
| } |
| |
| void ReadWindowTitle() { |
| extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait( |
| browser()->profile(), extension_misc::kChromeVoxExtensionId, |
| "import('/chromevox/background/" |
| "command_handler_interface.js').then(module => " |
| "module.CommandHandlerInterface.instance.onCommand('readCurrentTitle'))" |
| ";"); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| const SpokenFeedbackAppListTestVariant variant_; |
| std::unique_ptr<ui::ScopedAnimationDurationScaleMode> zero_duration_mode_; |
| }; |
| |
| class SpokenFeedbackAppListTest |
| : public SpokenFeedbackAppListBaseTest, |
| public ::testing::WithParamInterface<SpokenFeedbackAppListTestVariant> { |
| public: |
| SpokenFeedbackAppListTest() |
| : SpokenFeedbackAppListBaseTest(/*variant=*/GetParam()) {} |
| ~SpokenFeedbackAppListTest() override = default; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(TestAsNormalAndGuestUser, |
| SpokenFeedbackAppListTest, |
| ::testing::Values(kTestAsNormalUser, |
| kTestAsGuestUser)); |
| |
| class NotificationSpokenFeedbackAppListTest : public SpokenFeedbackAppListTest { |
| protected: |
| NotificationSpokenFeedbackAppListTest() = default; |
| ~NotificationSpokenFeedbackAppListTest() override = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| SpokenFeedbackAppListTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kAshEnableTabletMode); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(TestAsNormalAndGuestUser, |
| NotificationSpokenFeedbackAppListTest, |
| ::testing::Values(kTestAsNormalUser, |
| kTestAsGuestUser)); |
| |
| class SpokenFeedbackAppListSearchTest |
| : public SpokenFeedbackAppListBaseTest, |
| public ::testing::WithParamInterface< |
| std::tuple<SpokenFeedbackAppListTestVariant, bool /*tablet_mode*/>> { |
| public: |
| SpokenFeedbackAppListSearchTest() |
| : SpokenFeedbackAppListBaseTest(/*variant=*/std::get<0>(GetParam())), |
| tablet_mode_(std::get<1>(GetParam())) {} |
| ~SpokenFeedbackAppListSearchTest() override = default; |
| |
| // SpokenFeedbackAppListTest: |
| void SetUpOnMainThread() override { |
| SpokenFeedbackAppListBaseTest::SetUpOnMainThread(); |
| |
| AppListClientImpl* app_list_client = AppListClientImpl::GetInstance(); |
| |
| // Reset default search controller, so the test has better control over the |
| // set of results shown in the search result UI. |
| std::unique_ptr<app_list::SearchController> search_controller = |
| std::make_unique<app_list::SearchController>( |
| app_list_client->GetModelUpdaterForTest(), app_list_client, nullptr, |
| browser()->profile(), nullptr); |
| search_controller->Initialize(); |
| // Disable ranking, which may override the explicitly set relevance scores |
| // and best match status of results. |
| search_controller->disable_ranking_for_test(); |
| InitializeTestSearchProviders(search_controller.get(), &apps_provider_, |
| &web_provider_, &image_provider_); |
| ASSERT_TRUE(apps_provider_); |
| ASSERT_TRUE(web_provider_); |
| ASSERT_TRUE(image_provider_); |
| app_list_client->SetSearchControllerForTest(std::move(search_controller)); |
| |
| ShellTestApi().SetTabletModeEnabledForTest(tablet_mode_); |
| } |
| |
| void TearDownOnMainThread() override { |
| AppListClientImpl::GetInstance()->SetSearchControllerForTest(nullptr); |
| SpokenFeedbackAppListBaseTest::TearDownOnMainThread(); |
| } |
| |
| void ShowAppList() { |
| if (tablet_mode_) { |
| // Minimize the test window to transition to tablet mode home screen. |
| sm_.Call([this]() { browser()->window()->Minimize(); }); |
| } else { |
| // Focus the home button and press it to open the bubble launcher. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| } |
| } |
| |
| protected: |
| // Whether the test runs in tablet mode. |
| const bool tablet_mode_; |
| |
| // This field is not a raw_ptr<> because it was filtered by the rewriter |
| // for: #addr-of |
| RAW_PTR_EXCLUSION TestSearchProvider* apps_provider_ = nullptr; |
| // This field is not a raw_ptr<> because it was filtered by the rewriter |
| // for: #addr-of |
| RAW_PTR_EXCLUSION TestSearchProvider* web_provider_ = nullptr; |
| // This field is not a raw_ptr<> because it was filtered by the rewriter |
| // for: #addr-of |
| RAW_PTR_EXCLUSION TestSearchProvider* image_provider_ = nullptr; |
| }; |
| |
| // Instantiate test by user variant and tablet mode state. |
| INSTANTIATE_TEST_SUITE_P(TestAsNormalAndGuestUserInTabletAndClamshell, |
| SpokenFeedbackAppListSearchTest, |
| ::testing::Combine(::testing::Values(kTestAsNormalUser, |
| kTestAsGuestUser), |
| ::testing::Bool())); |
| |
| // Checks that when an app list item with a notification badge is focused, an |
| // announcement is made that the item requests your attention. |
| IN_PROC_BROWSER_TEST_P(NotificationSpokenFeedbackAppListTest, |
| AppListItemNotificationBadgeAnnounced) { |
| PopulateApps(1); |
| |
| int test_item_index = 0; |
| AppListItem* test_item = FindItemByName("app 0", &test_item_index); |
| ASSERT_TRUE(test_item); |
| test_item->UpdateNotificationBadgeForTesting(true); |
| |
| EnableChromeVox(); |
| |
| // Focus the shelf. This selects the launcher button. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| // Activate the launcher button. This opens bubble launcher. |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| // Skip over apps that were installed before the test item. |
| sm_.Call([this, &test_item_index]() { |
| for (int i = 0; i < test_item_index + 1; ++i) { |
| SendKeyPressWithSearch(ui::VKEY_RIGHT); |
| } |
| }); |
| |
| // Check that the announcement for items with a notification badge occurs. |
| sm_.ExpectSpeech("app 0 requests your attention."); |
| sm_.Replay(); |
| } |
| |
| // Checks that when a paused app list item is focused, an announcement 'Paused' |
| // is made. |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, |
| AppListItemPausedAppAnnounced) { |
| PopulateApps(1); |
| |
| int test_item_index = 0; |
| AppListItem* test_item = FindItemByName("app 0", &test_item_index); |
| ASSERT_TRUE(test_item); |
| test_item->UpdateAppStatusForTesting(AppStatus::kPaused); |
| |
| EnableChromeVox(); |
| |
| // Focus the shelf. This selects the launcher button. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| // Activate the launcher button. This opens bubble launcher. |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| // Skip over apps that were installed before the test item. |
| sm_.Call([this, &test_item_index]() { |
| for (int i = 0; i < test_item_index + 1; ++i) { |
| SendKeyPressWithSearch(ui::VKEY_RIGHT); |
| } |
| }); |
| |
| // Check that the announcement for items with a pause badge occurs. |
| sm_.ExpectSpeech("app 0"); |
| sm_.ExpectSpeech("Paused"); |
| sm_.Replay(); |
| } |
| |
| // Checks that when a blocked app list item is focused, an announcement |
| // 'Blocked' is made. |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, |
| AppListItemBlockedAppAnnounced) { |
| PopulateApps(1); |
| |
| int test_item_index = 0; |
| AppListItem* test_item = FindItemByName("app 0", &test_item_index); |
| ASSERT_TRUE(test_item); |
| test_item->UpdateAppStatusForTesting(AppStatus::kBlocked); |
| |
| EnableChromeVox(); |
| |
| // Focus the shelf. This selects the launcher button. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| // Activate the launcher button. This opens bubble launcher. |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| // Skip over apps that were installed before the test item. |
| sm_.Call([this, &test_item_index]() { |
| for (int i = 0; i < test_item_index + 1; ++i) { |
| SendKeyPressWithSearch(ui::VKEY_RIGHT); |
| } |
| }); |
| |
| // Check that the announcement for items with a block badge occurs. |
| sm_.ExpectSpeech("app 0"); |
| sm_.ExpectSpeech("Blocked"); |
| sm_.Replay(); |
| } |
| |
| // Checks that entering and exiting tablet mode with a browser window open does |
| // not generate an accessibility event. |
| IN_PROC_BROWSER_TEST_P( |
| SpokenFeedbackAppListTest, |
| HiddenAppListDoesNotCreateAccessibilityEventWhenTransitioningToTabletMode) { |
| EnableChromeVox(); |
| |
| sm_.Call([]() { ShellTestApi().SetTabletModeEnabledForTest(true); }); |
| sm_.ExpectNextSpeechIsNot("Launcher, all apps"); |
| sm_.Call([]() { ShellTestApi().SetTabletModeEnabledForTest(false); }); |
| sm_.ExpectNextSpeechIsNot("Launcher, all apps"); |
| sm_.Replay(); |
| } |
| |
| // Checks that rotating the display in tablet mode does not generate an |
| // accessibility event. |
| IN_PROC_BROWSER_TEST_P( |
| SpokenFeedbackAppListTest, |
| LauncherAppListScreenRotationDoesNotCreateAccessibilityEvent) { |
| display::DisplayManager* display_manager = Shell::Get()->display_manager(); |
| const int display_id = display_manager->GetDisplayAt(0).id(); |
| EnableChromeVox(); |
| |
| sm_.Call([]() { ShellTestApi().SetTabletModeEnabledForTest(true); }); |
| |
| sm_.Call([this]() { browser()->window()->Minimize(); }); |
| // Set screen rotation to 90 degrees. No ChromeVox event should be created. |
| sm_.Call([&, display_manager, display_id]() { |
| display_manager->SetDisplayRotation(display_id, display::Display::ROTATE_90, |
| display::Display::RotationSource::USER); |
| }); |
| sm_.ExpectNextSpeechIsNot("Launcher, all apps"); |
| |
| // Set screen rotation to 0 degrees. No ChromeVox event should be created. |
| sm_.Call([&, display_manager, display_id]() { |
| display_manager->SetDisplayRotation(display_id, display::Display::ROTATE_0, |
| display::Display::RotationSource::USER); |
| }); |
| sm_.ExpectNextSpeechIsNot("Launcher, all apps"); |
| |
| sm_.Replay(); |
| } |
| |
| // TODO(https://crbug.com/1393235): Update this browser test to test recent |
| // apps. |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, ClamshellLauncher) { |
| PopulateApps(3); |
| |
| int test_item_index = 0; |
| AppListItem* test_item = FindItemByName("app 0", &test_item_index); |
| ASSERT_TRUE(test_item); |
| |
| EnableChromeVox(); |
| |
| // Focus the shelf. This selects the launcher button. |
| sm_.Call([this]() { |
| EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf)); |
| }); |
| sm_.ExpectSpeechPattern("Launcher"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectSpeech("Shelf"); |
| sm_.ExpectSpeech("Tool bar"); |
| |
| // Activate the launcher button. This opens bubble launcher. |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| sm_.ExpectSpeech("Launcher, all apps"); |
| sm_.Call([this]() { ReadWindowTitle(); }); |
| sm_.ExpectSpeech("Launcher"); |
| |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); }); |
| sm_.ExpectSpeech("Button"); |
| |
| // Skip over apps that were installed before the test item. |
| // This selects the first app installed by the test. |
| for (int i = 0; i < test_item_index; ++i) { |
| sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); }); |
| } |
| sm_.ExpectSpeech("app 0"); |
| sm_.ExpectSpeech("Button"); |
| |
| // Move the focused item to the right. The announcement does not include a |
| // page because the bubble launcher apps grid is scrollable, not paged. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_RIGHT); }); |
| |
| sm_.ExpectSpeech( |
| base::StringPrintf("Moved to row 1, column %d.", test_item_index + 2)); |
| |
| sm_.Replay(); |
| } |
| |
| // Checks that app list keyboard reordering is announced. |
| // TODO(mmourgos): The current method of accessibility announcements for item |
| // reordering uses alerts, this works for spoken feedback but does not work as |
| // well for braille users. The preferred way to handle this is to actually |
| // change focus as the user navigates, and to have each object's |
| // accessible name describe its position. (See crbug.com/1098495) |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, AppListReordering) { |
| PopulateApps(22); |
| EnableChromeVox(); |
| const int test_item_index = MoveToFirstTestApp(); |
| |
| // The default column of app 0. |
| const int original_column = test_item_index + 1; |
| |
| // The column of app 0 after rightward move. |
| const int column_after_horizontal_move = original_column + 1; |
| |
| // Move the first item to the right. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_RIGHT); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| |
| sm_.ExpectSpeech(base::StringPrintf("Moved to row 1, column %d.", |
| column_after_horizontal_move)); |
| |
| // Move the focused item down. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_DOWN); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech(base::StringPrintf("Moved to row 2, column %d.", |
| column_after_horizontal_move)); |
| |
| // Move the focused item down. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_DOWN); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech(base::StringPrintf("Moved to row 3, column %d.", |
| column_after_horizontal_move)); |
| |
| // Move the focused item down. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_DOWN); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech(base::StringPrintf("Moved to row 4, column %d.", |
| column_after_horizontal_move)); |
| |
| // Move the focused item left. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_LEFT); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech( |
| base::StringPrintf("Moved to row 4, column %d.", original_column)); |
| |
| // Move the focused item back up. |
| sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_UP); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech( |
| base::StringPrintf("Moved to row 3, column %d.", original_column)); |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, AppListFoldering) { |
| // Add 3 apps and move to the first one. |
| PopulateApps(3); |
| EnableChromeVox(); |
| const int test_item_index = MoveToFirstTestApp(); |
| |
| // Combine items and create a new folder. |
| sm_.Call([]() { SendKeyPressWithShiftAndControl(ui::VKEY_RIGHT); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech("Folder Unnamed"); |
| sm_.ExpectSpeech("Expanded"); |
| sm_.ExpectSpeech("app 0 combined with app 1 to create new folder."); |
| |
| // Move focus to the first item in folder. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech("app 1"); |
| sm_.ExpectSpeech("Button"); |
| |
| // Remove the first item from the folder back to the top level app list. |
| sm_.Call([]() { SendKeyPressWithShiftAndControl(ui::VKEY_LEFT); }); |
| |
| sm_.ExpectSpeech("app 1"); |
| sm_.ExpectSpeech("Button"); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| |
| sm_.ExpectSpeech( |
| base::StringPrintf("Moved to row 1, column %d.", test_item_index + 1)); |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, |
| CloseFolderMakesA11yAnnouncement) { |
| // Add 3 apps and move to the first one. |
| PopulateApps(3); |
| EnableChromeVox(); |
| MoveToFirstTestApp(); |
| |
| // Combine items and create a new folder. |
| sm_.Call([]() { SendKeyPressWithShiftAndControl(ui::VKEY_RIGHT); }); |
| sm_.ExpectNextSpeechIsNot("Alert"); |
| sm_.ExpectSpeech("Folder Unnamed"); |
| sm_.ExpectSpeech("Expanded"); |
| sm_.ExpectSpeech("app 0 combined with app 1 to create new folder."); |
| |
| // Move focus to the first item in folder. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech("app 1"); |
| sm_.ExpectSpeech("Button"); |
| |
| // Press Escape to close the folder. ChromeVox should announce that the |
| // folder has been closed. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_ESCAPE); }); |
| sm_.ExpectSpeech("Close folder"); |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListTest, |
| SortAppsMakesA11yAnnouncement) { |
| // Add 3 apps and move to the first one. |
| PopulateApps(3); |
| EnableChromeVox(); |
| MoveToFirstTestApp(); |
| |
| // The AppListTestApi's ReorderByMouseClickAtToplevelAppsGridMenu times out |
| // when called with ReorderAnimationEndState::kCompleted due to the run loop |
| // in WaitForReorderAnimationAndVerifyItemVisibility. So we do some of that |
| // work here manually. |
| |
| // Show the context menu for the AppsGridView. |
| sm_.Call([]() { |
| AppsGridView* grid_view = AppListTestApi().GetTopLevelAppsGridView(); |
| EXPECT_TRUE(grid_view); |
| grid_view->ShowContextMenu(grid_view->GetBoundsInScreen().CenterPoint(), |
| ui::MENU_SOURCE_KEYBOARD); |
| }); |
| sm_.ExpectSpeech("menu opened"); |
| |
| // Press N to activate sort by name. This will result in focus being moved |
| // to a button to undo the sort just completed. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_N); }); |
| sm_.ExpectSpeech("Apps are sorted by name"); |
| sm_.ExpectSpeech("Undo sort order by name"); |
| sm_.ExpectSpeech("Button"); |
| |
| // Press the undo button. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeech("Sort undo successful"); |
| |
| // Show the context menu for the AppsGridView. |
| sm_.Call([]() { |
| AppsGridView* grid_view = AppListTestApi().GetTopLevelAppsGridView(); |
| EXPECT_TRUE(grid_view); |
| grid_view->ShowContextMenu(grid_view->GetBoundsInScreen().CenterPoint(), |
| ui::MENU_SOURCE_KEYBOARD); |
| }); |
| sm_.ExpectSpeech("menu opened"); |
| |
| // Press C to activate sort by color. This will result in focus being moved |
| // to a button to undo the sort just completed. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_C); }); |
| sm_.ExpectSpeech("Apps are sorted by color"); |
| sm_.ExpectSpeech("Undo sort order by color"); |
| sm_.ExpectSpeech("Button"); |
| |
| // Press the undo button. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); }); |
| sm_.ExpectSpeech("Sort undo successful"); |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListSearchTest, LauncherSearch) { |
| EnableChromeVox(); |
| ShowAppList(); |
| |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| sm_.Call([this]() { ReadWindowTitle(); }); |
| sm_.ExpectSpeech("Launcher"); |
| |
| sm_.Call([this]() { |
| apps_provider_->set_best_match_count(2); |
| apps_provider_->set_count(3); |
| web_provider_->set_count(4); |
| image_provider_->set_count(3); |
| SendKeyPress(ui::VKEY_G); |
| }); |
| |
| sm_.ExpectSpeech("G"); |
| sm_.ExpectSpeech("app 0"); |
| sm_.ExpectSpeech("List item 1 of 2"); |
| sm_.ExpectSpeech("Best Match"); |
| sm_.ExpectSpeech("List box"); |
| |
| // Traverse best match results; |
| for (int i = 1; i < 2; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("app %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 2", i + 1)); |
| } |
| |
| // Traverse image results. |
| for (int i = 0; i < 3; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("image %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 3", i + 1)); |
| if (i == 0) { |
| sm_.ExpectSpeech("List box"); |
| } |
| } |
| |
| // Traverse non-best-match app results. |
| for (int i = 2; i < 5; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("app %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 3", (i - 2) % 3 + 1)); |
| if (i == 2) { |
| sm_.ExpectSpeech("Apps"); |
| sm_.ExpectSpeech("List box"); |
| } |
| } |
| |
| // Traverse omnibox results. |
| for (int i = 0; i < 3; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("item %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 3", i + 1)); |
| if (i == 0) { |
| sm_.ExpectSpeech("Websites"); |
| sm_.ExpectSpeech("List box"); |
| } |
| } |
| |
| // Cycle focus to the close button. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech("Clear searchbox text"); |
| |
| // Go back to the last result. |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_UP); }); |
| sm_.ExpectSpeech("item 2"); |
| |
| // Update the query, to initiate new search. |
| sm_.Call([this]() { |
| apps_provider_->set_best_match_count(0); |
| apps_provider_->set_count(3); |
| web_provider_->set_count(2); |
| image_provider_->set_count(2); |
| SendKeyPress(ui::VKEY_A); |
| }); |
| |
| sm_.ExpectSpeech("A"); |
| sm_.ExpectSpeech("image 0"); |
| sm_.ExpectSpeech("List item 1 of 2"); |
| sm_.ExpectSpeech("List box"); |
| |
| // Traverse image results. |
| for (int i = 1; i < 2; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("image %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 2", i + 1)); |
| } |
| |
| // Verify traversal works after result change. |
| for (int i = 0; i < 3; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("app %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 3", i + 1)); |
| } |
| |
| for (int i = 0; i < 2; ++i) { |
| sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); }); |
| sm_.ExpectSpeech(base::StringPrintf("item %d", i)); |
| sm_.ExpectSpeech(base::StringPrintf("List item %d of 2", i + 1)); |
| if (i == 0) { |
| sm_.ExpectSpeech("Websites"); |
| sm_.ExpectSpeech("List box"); |
| } |
| } |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListSearchTest, |
| TouchExploreLauncherSearchResult) { |
| EnableChromeVox(); |
| ShowAppList(); |
| |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| sm_.Call([this]() { |
| apps_provider_->set_best_match_count(2); |
| apps_provider_->set_count(3); |
| web_provider_->set_count(4); |
| SendKeyPress(ui::VKEY_G); |
| }); |
| |
| sm_.ExpectSpeech("G"); |
| sm_.ExpectSpeech("Displaying 8 results for g"); |
| |
| base::SimpleTestTickClock clock; |
| clock.SetNowTicks(base::TimeTicks::Now()); |
| auto* clock_ptr = &clock; |
| ui::SetEventTickClockForTesting(clock_ptr); |
| |
| auto* root_window = Shell::Get()->GetPrimaryRootWindow(); |
| ui::test::EventGenerator generator(root_window); |
| auto* generator_ptr = &generator; |
| |
| // Start touch exploration, and go to the third result in the UI (expected to |
| // be "app 2"). |
| sm_.Call([clock_ptr, generator_ptr]() { |
| views::View* target_view = |
| AppListTestApi().GetVisibleSearchResultView(/*index=*/2); |
| ASSERT_TRUE(target_view); |
| |
| gfx::Point touch_point = target_view->GetBoundsInScreen().CenterPoint(); |
| ui::TouchEvent touch_press( |
| ui::ET_TOUCH_PRESSED, touch_point, base::TimeTicks::Now(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, 0)); |
| generator_ptr->Dispatch(&touch_press); |
| |
| clock_ptr->Advance(base::Seconds(1)); |
| |
| ui::TouchEvent touch_move( |
| ui::ET_TOUCH_MOVED, touch_point, base::TimeTicks::Now(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, 0)); |
| generator_ptr->Dispatch(&touch_move); |
| }); |
| |
| // The result under touch pointer should be announced. |
| sm_.ExpectSpeech("app 2"); |
| sm_.ExpectSpeech("List item 1 of 3"); |
| sm_.ExpectSpeech("Apps"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SpokenFeedbackAppListSearchTest, VocalizeResultCount) { |
| EnableChromeVox(); |
| ShowAppList(); |
| |
| sm_.ExpectSpeechPattern("Search your *"); |
| sm_.ExpectSpeech("Edit text"); |
| |
| sm_.Call([this]() { |
| apps_provider_->set_best_match_count(2); |
| apps_provider_->set_count(3); |
| web_provider_->set_count(4); |
| SendKeyPress(ui::VKEY_G); |
| }); |
| |
| sm_.ExpectSpeech("G"); |
| sm_.ExpectSpeech("Displaying 8 results for g"); |
| |
| // Update the query, to initiate new search. |
| sm_.Call([this]() { |
| apps_provider_->set_best_match_count(0); |
| apps_provider_->set_count(3); |
| web_provider_->set_count(2); |
| SendKeyPress(ui::VKEY_A); |
| }); |
| |
| sm_.ExpectSpeech("A"); |
| sm_.ExpectSpeech("Displaying 5 results for ga"); |
| |
| sm_.Replay(); |
| } |
| |
| } // namespace ash |