// Copyright 2018 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/accessibility/accessibility_controller.h"
#include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/shell.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/accessibility_test_utils.h"
#include "chrome/browser/ash/accessibility/dictation_bubble_test_helper.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/base_event_utils.h"
namespace ash {
using ContextType = ::extensions::ExtensionBrowserTest::ContextType;
class AccessibilityPrivateApiTest
: public extensions::ExtensionApiTest,
public testing::WithParamInterface<ApiTestConfig> {
public:
AccessibilityPrivateApiTest() : ExtensionApiTest(GetParam().context_type()) {}
~AccessibilityPrivateApiTest() override = default;
AccessibilityPrivateApiTest& operator=(const AccessibilityPrivateApiTest&) =
delete;
AccessibilityPrivateApiTest(const AccessibilityPrivateApiTest&) = delete;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ExtensionApiTest::SetUpCommandLine(command_line);
// Required for the installFaceGazeAssets API to work.
scoped_feature_list_.InitAndEnableFeature(
::features::kAccessibilityFaceGaze);
}
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
dictation_bubble_test_helper_ =
std::make_unique<DictationBubbleTestHelper>();
}
[[nodiscard]] bool RunSubtest(const char* subtest) {
std::string path;
if (GetParam().version() == ManifestVersion::kTwo) {
path = "accessibility_private";
} else {
path = "accessibility_private/mv3";
}
return RunExtensionTest(path.c_str(), {.custom_arg = subtest});
}
DictationBubbleTestHelper* dictation_bubble_test_helper() {
return dictation_bubble_test_helper_.get();
}
private:
std::unique_ptr<DictationBubbleTestHelper> dictation_bubble_test_helper_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, SendSyntheticKeyEvent) {
ASSERT_TRUE(RunSubtest("testSendSyntheticKeyEvent")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetDisplayNameForLocaleTest) {
ASSERT_TRUE(RunSubtest("testGetDisplayNameForLocale")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, OpenSettingsSubpage) {
Profile* profile = AccessibilityManager::Get()->profile();
// Install the Settings App.
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ASSERT_TRUE(RunSubtest("testOpenSettingsSubpage")) << message_;
chrome::SettingsWindowManager* settings_manager =
chrome::SettingsWindowManager::GetInstance();
Browser* settings_browser = settings_manager->FindBrowserForProfile(profile);
EXPECT_NE(nullptr, settings_browser);
content::WebContents* web_contents =
settings_browser->tab_strip_model()->GetWebContentsAt(0);
EXPECT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(GURL(chrome::GetOSSettingsUrl("manageAccessibility/tts")),
web_contents->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
OpenSettingsSubpage_InvalidSubpage) {
Profile* profile = AccessibilityManager::Get()->profile();
// Install the Settings App.
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ASSERT_TRUE(RunSubtest("testOpenSettingsSubpageInvalidSubpage")) << message_;
chrome::SettingsWindowManager* settings_manager =
chrome::SettingsWindowManager::GetInstance();
// Invalid subpage should not open settings window.
Browser* settings_browser = settings_manager->FindBrowserForProfile(profile);
EXPECT_EQ(nullptr, settings_browser);
}
template <bool enabled>
class AccessibilityPrivateApiFeatureTest : public AccessibilityPrivateApiTest {
public:
AccessibilityPrivateApiFeatureTest() = default;
~AccessibilityPrivateApiFeatureTest() override = default;
AccessibilityPrivateApiFeatureTest& operator=(
const AccessibilityPrivateApiFeatureTest&) = delete;
AccessibilityPrivateApiFeatureTest(
const AccessibilityPrivateApiFeatureTest&) = delete;
// AccessibilityPrivateApiTest:
void SetUpCommandLine(base::CommandLine* command_line) override {
AccessibilityPrivateApiTest::SetUpCommandLine(command_line);
if (enabled) {
scoped_feature_list_.InitAndEnableFeature(
::features::kExperimentalAccessibilityDictationContextChecking);
} else {
scoped_feature_list_.InitAndDisableFeature(
::features::kExperimentalAccessibilityDictationContextChecking);
}
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
using AccessibilityPrivateApiFeatureDisabledTest =
AccessibilityPrivateApiFeatureTest<false>;
using AccessibilityPrivateApiFeatureEnabledTest =
AccessibilityPrivateApiFeatureTest<true>;
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiFeatureDisabledTest,
IsFeatureEnabled_FeatureDisabled) {
ASSERT_TRUE(RunSubtest("testFeatureDisabled")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiFeatureEnabledTest,
IsFeatureEnabled_FeatureEnabled) {
ASSERT_TRUE(RunSubtest("testFeatureEnabled")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, IsFeatureUnknown) {
ASSERT_TRUE(RunSubtest("testFeatureUnknown")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, AcceptConfirmationDialog) {
ASSERT_TRUE(RunSubtest("testAcceptConfirmationDialog")) << message_;
// The test has requested to open the confirmation dialog. Check that
// it was created, then confirm it.
AccessibilityConfirmationDialog* dialog_ =
Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
ASSERT_NE(dialog_, nullptr);
EXPECT_EQ(dialog_->GetWindowTitle(), u"Confirm me! 🐶");
// Accept the dialog and wait for the JS test to get the confirmation.
extensions::ResultCatcher catcher;
dialog_->Accept();
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, CancelConfirmationDialog) {
ASSERT_TRUE(RunSubtest("testCancelConfirmationDialog")) << message_;
// The test has requested to open the confirmation dialog. Check that
// it was created, then cancel it.
AccessibilityConfirmationDialog* dialog_ =
Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
ASSERT_NE(dialog_, nullptr);
EXPECT_EQ(dialog_->GetWindowTitle(), u"Cancel me!");
// Cancel the dialog and wait for the JS test to get the callback.
extensions::ResultCatcher catcher;
dialog_->Cancel();
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, CloseConfirmationDialog) {
ASSERT_TRUE(RunSubtest("testCancelConfirmationDialog")) << message_;
// The test has requested to open the confirmation dialog. Check that
// it was created, then close it.
AccessibilityConfirmationDialog* dialog_ =
Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
ASSERT_TRUE(dialog_ != nullptr);
EXPECT_EQ(dialog_->GetWindowTitle(), u"Cancel me!");
// Close the dialog and wait for the JS test to get the callback.
extensions::ResultCatcher catcher;
dialog_->Close();
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, UpdateDictationBubble) {
// Enable Dictation to allow the API to work.
Shell::Get()->accessibility_controller()->dictation().SetEnabled(true);
// This test requires some back and forth communication between C++ and JS.
// Use message listeners to force the synchronicity of this test.
ExtensionTestMessageListener standby_listener("Standby",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener show_text_listener("Show text",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener macro_success_listener(
"Show macro success", ReplyBehavior::kWillReply);
ExtensionTestMessageListener reset_listener("Reset",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener hide_listener("Hide");
extensions::ResultCatcher result_catcher;
ASSERT_TRUE(RunSubtest("testUpdateDictationBubble")) << message_;
ASSERT_TRUE(standby_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_EQ(std::u16string(), dictation_bubble_test_helper()->GetText());
EXPECT_EQ(DictationBubbleIconType::kStandby,
dictation_bubble_test_helper()->GetVisibleIcon());
standby_listener.Reply("Continue");
ASSERT_TRUE(show_text_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_EQ(u"Hello", dictation_bubble_test_helper()->GetText());
EXPECT_EQ(DictationBubbleIconType::kHidden,
dictation_bubble_test_helper()->GetVisibleIcon());
show_text_listener.Reply("Continue");
ASSERT_TRUE(macro_success_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_EQ(u"Hello", dictation_bubble_test_helper()->GetText());
EXPECT_EQ(DictationBubbleIconType::kMacroSuccess,
dictation_bubble_test_helper()->GetVisibleIcon());
macro_success_listener.Reply("Continue");
ASSERT_TRUE(reset_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_EQ(std::u16string(), dictation_bubble_test_helper()->GetText());
EXPECT_EQ(DictationBubbleIconType::kStandby,
dictation_bubble_test_helper()->GetVisibleIcon());
reset_listener.Reply("Continue");
ASSERT_TRUE(hide_listener.WaitUntilSatisfied());
EXPECT_FALSE(dictation_bubble_test_helper()->IsVisible());
EXPECT_EQ(std::u16string(), dictation_bubble_test_helper()->GetText());
EXPECT_EQ(DictationBubbleIconType::kHidden,
dictation_bubble_test_helper()->GetVisibleIcon());
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
UpdateDictationBubbleWithHints) {
Shell::Get()->accessibility_controller()->dictation().SetEnabled(true);
ExtensionTestMessageListener show_listener("Some hints",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener no_hints_listener("No hints");
extensions::ResultCatcher result_catcher;
ASSERT_TRUE(RunSubtest("testUpdateDictationBubbleWithHints")) << message_;
ASSERT_TRUE(show_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_TRUE(dictation_bubble_test_helper()->HasVisibleHints(
std::vector<std::u16string>{u"Try saying:", u"\"Type [word / phrase]\"",
u"\"Help\""}));
show_listener.Reply("Continue");
ASSERT_TRUE(no_hints_listener.WaitUntilSatisfied());
EXPECT_TRUE(dictation_bubble_test_helper()->IsVisible());
EXPECT_TRUE(dictation_bubble_test_helper()->HasVisibleHints(
std::vector<std::u16string>()));
ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
InstallPumpkinForDictationFail) {
// Enable Dictation to allow the API to work.
Shell::Get()->accessibility_controller()->dictation().SetEnabled(true);
ASSERT_TRUE(RunSubtest("testInstallPumpkinForDictationFail")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
InstallPumpkinForDictationSuccess) {
// Enable Dictation to allow the API to work.
Shell::Get()->accessibility_controller()->dictation().SetEnabled(true);
// Initialize Pumpkin DLC directory.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir pumpkin_root_dir;
ASSERT_TRUE(pumpkin_root_dir.CreateUniqueTempDir());
// Create subdirectories for each locale supported by Pumpkin.
std::vector<std::string> locales{"en_us", "fr_fr", "it_it", "de_de", "es_es"};
std::vector<base::ScopedTempDir> sub_dirs(locales.size());
for (size_t i = 0; i < locales.size(); ++i) {
ASSERT_TRUE(sub_dirs[i].Set(pumpkin_root_dir.GetPath().Append(locales[i])));
}
// Create fake DLC files.
AccessibilityManager::Get()->SetDlcPathForTest(pumpkin_root_dir.GetPath());
ASSERT_TRUE(base::WriteFile(
pumpkin_root_dir.GetPath().Append("js_pumpkin_tagger_bin.js"),
"Fake js pumpkin tagger"));
ASSERT_TRUE(
base::WriteFile(pumpkin_root_dir.GetPath().Append("tagger_wasm_main.js"),
"Fake tagger wasm js"));
ASSERT_TRUE(base::WriteFile(
pumpkin_root_dir.GetPath().Append("tagger_wasm_main.wasm"),
"Fake tagger wasm wasm"));
for (size_t j = 0; j < locales.size(); ++j) {
std::string locale = locales[j];
ASSERT_TRUE(
base::WriteFile(sub_dirs[j].GetPath().Append("action_config.binarypb"),
"Fake " + locale + " action config"));
ASSERT_TRUE(
base::WriteFile(sub_dirs[j].GetPath().Append("pumpkin_config.binarypb"),
"Fake " + locale + " pumpkin config"));
}
ASSERT_TRUE(RunSubtest("testInstallPumpkinForDictationSuccess")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetDlcContentsDlcNotOnDevice) {
ASSERT_TRUE(RunSubtest("testGetDlcContentsDlcNotOnDevice")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, GetDlcContentsSuccess) {
// Create a fake DLC file. We need to put this in a ScopedTempDir because this
// test doesn't have write access to the actual DLC directory
// (/run/imageloader/).
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir dlc_dir;
ASSERT_TRUE(dlc_dir.CreateUniqueTempDir());
AccessibilityManager::Get()->SetDlcPathForTest(dlc_dir.GetPath());
std::string content = "Fake DLC file content";
ASSERT_TRUE(
base::WriteFile(dlc_dir.GetPath().Append("voice.zvoice"), content));
ASSERT_TRUE(RunSubtest("testGetDlcContentsSuccess")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetTtsDlcContentsDlcNotOnDevice) {
ASSERT_TRUE(RunSubtest("testGetTtsDlcContentsDlcNotOnDevice")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, GetTtsDlcContentsSuccess) {
// Create a fake DLC file. We need to put this in a ScopedTempDir because this
// test doesn't have write access to the actual DLC directory
// (/run/imageloader/).
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir dlc_dir;
ASSERT_TRUE(dlc_dir.CreateUniqueTempDir());
AccessibilityManager::Get()->SetDlcPathForTest(dlc_dir.GetPath());
std::string content = "Fake DLC file content";
ASSERT_TRUE(
base::WriteFile(dlc_dir.GetPath().Append("voice.zvoice"), content));
ASSERT_TRUE(RunSubtest("testGetTtsDlcContentsSuccess")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetVariantTtsDlcContentsDlcNotOnDevice) {
ASSERT_TRUE(RunSubtest("testGetVariantTtsDlcContentsDlcNotOnDevice"))
<< message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetVariantTtsDlcContentsSuccess) {
// Create a fake DLC file. We need to put this in a ScopedTempDir because this
// test doesn't have write access to the actual DLC directory
// (/run/imageloader/).
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir dlc_dir;
ASSERT_TRUE(dlc_dir.CreateUniqueTempDir());
AccessibilityManager::Get()->SetDlcPathForTest(dlc_dir.GetPath());
std::string content = "Fake DLC file content";
ASSERT_TRUE(base::WriteFile(dlc_dir.GetPath().Append("voice-standard.zvoice"),
content));
ASSERT_TRUE(RunSubtest("testGetVariantTtsDlcContentsSuccess")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, SetCursorPosition) {
const std::string kTestCases[] = {"800x600", "1000x800*2.0",
"801+0-400x300,1+0-400x300"};
for (const auto& test : kTestCases) {
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.UpdateDisplay(test);
ScreenOrientationControllerTestApi(
Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
// The setCursorPosition method takes density-independent pixels.
ASSERT_TRUE(RunSubtest("testSetCursorPosition")) << message_;
// The screen point is in density-independent pixels, so it should always be
// the same as what the JS has set, (450, 350), assuming all the
// multiple-display and DPI math was correct.
const gfx::Point point =
display::Screen::GetScreen()->GetCursorScreenPoint();
EXPECT_EQ(point, gfx::Point(450, 350));
}
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, GetDisplayBoundsSimple) {
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.UpdateDisplay("800x600");
ScreenOrientationControllerTestApi(
Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
ASSERT_TRUE(RunSubtest("testGetDisplayBoundsSimple")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, GetDisplayBoundsHighDPI) {
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.UpdateDisplay("1000x800*2.0");
ScreenOrientationControllerTestApi(
Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
ASSERT_TRUE(RunSubtest("testGetDisplayBoundsHighDPI")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetDisplayBoundsMultipleDisplays) {
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.UpdateDisplay("801+0-400x300,1+0-800x600*2.0");
ScreenOrientationControllerTestApi(
Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
ASSERT_TRUE(RunSubtest("testGetDisplayBoundsMultipleDisplays")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, InstallFaceGazeAssetsFail) {
Shell::Get()->accessibility_controller()->face_gaze().SetEnabled(true);
ASSERT_TRUE(RunSubtest("testInstallFaceGazeAssetsFail")) << message_;
}
IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
InstallFaceGazeAssetsSuccess) {
Shell::Get()->accessibility_controller()->face_gaze().SetEnabled(true);
// Initialize DLC directory.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir face_gaze_assets_root_dir;
ASSERT_TRUE(face_gaze_assets_root_dir.CreateUniqueTempDir());
// Create fake DLC files.
AccessibilityManager::Get()->SetDlcPathForTest(
face_gaze_assets_root_dir.GetPath());
ASSERT_TRUE(base::WriteFile(
face_gaze_assets_root_dir.GetPath().Append("face_landmarker.task"),
"Fake facelandmarker model"));
ASSERT_TRUE(base::WriteFile(
face_gaze_assets_root_dir.GetPath().Append("vision_wasm_internal.wasm"),
"Fake mediapipe web assembly"));
ASSERT_TRUE(RunSubtest("testInstallFaceGazeAssetsSuccess")) << message_;
}
INSTANTIATE_TEST_SUITE_P(
PersistentBackground,
AccessibilityPrivateApiTest,
::testing::Values(ApiTestConfig(ContextType::kPersistentBackground,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
PersistentBackground,
AccessibilityPrivateApiFeatureDisabledTest,
::testing::Values(ApiTestConfig(ContextType::kPersistentBackground,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
PersistentBackground,
AccessibilityPrivateApiFeatureEnabledTest,
::testing::Values(ApiTestConfig(ContextType::kPersistentBackground,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
ServiceWorker,
AccessibilityPrivateApiTest,
::testing::Values(ApiTestConfig(ContextType::kServiceWorker,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
ServiceWorker,
AccessibilityPrivateApiFeatureDisabledTest,
::testing::Values(ApiTestConfig(ContextType::kServiceWorker,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
ServiceWorker,
AccessibilityPrivateApiFeatureEnabledTest,
::testing::Values(ApiTestConfig(ContextType::kServiceWorker,
ManifestVersion::kTwo)));
INSTANTIATE_TEST_SUITE_P(
ManifestV3,
AccessibilityPrivateApiTest,
::testing::Values(ApiTestConfig(ContextType::kNone,
ManifestVersion::kThree)));
} // namespace ash