blob: 69ae8eaeeebb7213b32d5779b4d6ecc222ccf4f1 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <vector>
#include "ash/accessibility/accessibility_focus_ring_controller.h"
#include "ash/accessibility/accessibility_focus_ring_layer.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/interfaces/constants.mojom.h"
#include "ash/public/interfaces/status_area_widget_test_api.test-mojom-test-utils.h"
#include "ash/public/interfaces/status_area_widget_test_api.test-mojom.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/unified/unified_system_tray.h"
#include "base/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/pattern.h"
#include "chrome/browser/chromeos/accessibility/accessibility_manager.h"
#include "chrome/browser/chromeos/accessibility/speech_monitor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "content/public/common/service_manager_connection.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/notification_types.h"
#include "extensions/browser/process_manager.h"
#include "services/service_manager/public/cpp/connector.h"
#include "ui/events/test/event_generator.h"
#include "url/url_constants.h"
namespace chromeos {
class SelectToSpeakTest : public InProcessBrowserTest {
public:
void OnFocusRingChanged() {
if (loop_runner_) {
loop_runner_->Quit();
}
}
void OnSelectToSpeakStateChanged() {
if (tray_loop_runner_) {
tray_loop_runner_->Quit();
}
}
protected:
SelectToSpeakTest() : weak_ptr_factory_(this) {}
~SelectToSpeakTest() override {}
void SetUpOnMainThread() override {
ASSERT_FALSE(AccessibilityManager::Get()->IsSelectToSpeakEnabled());
// Connect to the ash test interface for the StatusAreaWidget.
content::ServiceManagerConnection::GetForProcess()
->GetConnector()
->BindInterface(ash::mojom::kServiceName,
&status_area_widget_test_api_);
content::WindowedNotificationObserver extension_load_waiter(
extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
content::NotificationService::AllSources());
AccessibilityManager::Get()->SetSelectToSpeakEnabled(true);
extension_load_waiter.Wait();
aura::Window* root_window = ash::Shell::Get()->GetPrimaryRootWindow();
generator_.reset(new ui::test::EventGenerator(root_window));
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL));
}
SpeechMonitor speech_monitor_;
std::unique_ptr<ui::test::EventGenerator> generator_;
gfx::Rect GetWebContentsBounds() const {
// TODO(katie): Find a way to get the exact bounds programmatically.
gfx::Rect bounds = browser()->window()->GetBounds();
bounds.Inset(8, 8, 75, 8);
return bounds;
}
void ActivateSelectToSpeakInWindowBounds(std::string url) {
ui_test_utils::NavigateToURL(browser(), GURL(url));
gfx::Rect bounds = GetWebContentsBounds();
// Hold down Search and drag over the web contents to select everything.
generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */);
generator_->MoveMouseTo(bounds.x(), bounds.y());
generator_->PressLeftButton();
generator_->MoveMouseTo(bounds.x() + bounds.width(),
bounds.y() + bounds.height());
generator_->ReleaseLeftButton();
generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */);
}
void PrepareToWaitForSelectToSpeakStatusChanged() {
tray_loop_runner_ = new content::MessageLoopRunner();
}
// Blocks until the select-to-speak tray status is changed.
void WaitForSelectToSpeakStatusChanged() {
tray_loop_runner_->Run();
tray_loop_runner_ = nullptr;
}
void TapSelectToSpeakTray() {
ash::mojom::StatusAreaWidgetTestApiAsyncWaiter status_area(
status_area_widget_test_api_.get());
PrepareToWaitForSelectToSpeakStatusChanged();
status_area.TapSelectToSpeakTray();
WaitForSelectToSpeakStatusChanged();
}
void PrepareToWaitForFocusRingChanged() {
loop_runner_ = new content::MessageLoopRunner();
}
// Blocks until the focus ring is changed.
void WaitForFocusRingChanged() {
loop_runner_->Run();
loop_runner_ = nullptr;
}
base::WeakPtr<SelectToSpeakTest> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void RunJavaScriptInSelectToSpeakBackgroundPage(const std::string& script) {
extensions::ExtensionHost* host =
extensions::ProcessManager::Get(browser()->profile())
->GetBackgroundHostForExtension(
extension_misc::kSelectToSpeakExtensionId);
CHECK(content::ExecuteScript(host->host_contents(), script));
}
content::WebContents* GetWebContents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
void ExecuteJavaScriptInForeground(const std::string& script) {
CHECK(content::ExecuteScript(GetWebContents(), script));
}
private:
ash::mojom::StatusAreaWidgetTestApiPtr status_area_widget_test_api_;
scoped_refptr<content::MessageLoopRunner> loop_runner_;
scoped_refptr<content::MessageLoopRunner> tray_loop_runner_;
base::WeakPtrFactory<SelectToSpeakTest> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(SelectToSpeakTest);
};
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SpeakStatusTray) {
gfx::Rect tray_bounds = ash::Shell::Get()
->GetPrimaryRootWindowController()
->GetStatusAreaWidget()
->unified_system_tray()
->GetBoundsInScreen();
// Hold down Search and click a few pixels into the status tray bounds.
generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */);
generator_->MoveMouseTo(tray_bounds.x() + 8, tray_bounds.y() + 8);
generator_->PressLeftButton();
generator_->ReleaseLeftButton();
generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */);
EXPECT_TRUE(
base::MatchPattern(speech_monitor_.GetNextUtterance(), "Status tray*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, ActivatesWithTapOnSelectToSpeakTray) {
base::RepeatingCallback<void()> callback = base::BindRepeating(
&SelectToSpeakTest::OnSelectToSpeakStateChanged, GetWeakPtr());
chromeos::AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(
callback);
// Click in the tray bounds to start 'selection' mode.
TapSelectToSpeakTray();
// We should be in "selection" mode, so clicking with the mouse should
// start speech.
ui_test_utils::NavigateToURL(
browser(), GURL("data:text/html;charset=utf-8,<p>This is some text</p>"));
gfx::Rect bounds = GetWebContentsBounds();
generator_->MoveMouseTo(bounds.x(), bounds.y());
generator_->PressLeftButton();
generator_->MoveMouseTo(bounds.x() + bounds.width(),
bounds.y() + bounds.height());
generator_->ReleaseLeftButton();
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SelectToSpeakTrayNotSpoken) {
base::RepeatingCallback<void()> callback = base::BindRepeating(
&SelectToSpeakTest::OnSelectToSpeakStateChanged, GetWeakPtr());
chromeos::AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(
callback);
// Tap it once to enter selection mode.
TapSelectToSpeakTray();
// Tap again to turn off selection mode.
TapSelectToSpeakTray();
// The next should be the first thing spoken -- the tray was not spoken.
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<p>This is some text</p>");
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossInlineUrl) {
// Make sure an inline URL is read smoothly.
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<p>This is some text <a href=\"\">with a"
" node</a> in the middle");
// Should combine nodes in a paragraph into one utterance.
// Includes some wildcards between words because there may be extra
// spaces. Spaces are not pronounced, so extra spaces do not impact output.
EXPECT_TRUE(
base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*with a node*in the middle*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossMultipleLines) {
// Sentences spanning multiple lines.
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<div style=\"width:100px\">This"
" is some text with a node in the middle");
// Should combine nodes in a paragraph into one utterance.
// Includes some wildcards between words because there may be extra
// spaces, for example at line wraps. Extra wildcards included to
// reduce flakyness in case wrapping is not consistent.
// Spaces are not pronounced, so extra spaces do not impact output.
EXPECT_TRUE(
base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some*text*with*a*node*in*the*middle*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossFormattedText) {
// Bold or formatted text
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<p>This is some text <b>with a node"
"</b> in the middle");
// Should combine nodes in a paragraph into one utterance.
// Includes some wildcards between words because there may be extra
// spaces. Spaces are not pronounced, so extra spaces do not impact output.
EXPECT_TRUE(
base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*with a node*in the middle*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest,
ReadsStaticTextWithoutInlineTextChildren) {
// Bold or formatted text
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<canvas>This is some text</canvas>");
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, BreaksAtParagraphBounds) {
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<div><p>First paragraph</p>"
"<p>Second paragraph</p></div>");
// Should keep each paragraph as its own utterance.
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"First paragraph*"));
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"Second paragraph*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, FocusRingMovesWithMouse) {
// Create a callback for the focus ring observer.
base::RepeatingCallback<void()> callback =
base::BindRepeating(&SelectToSpeakTest::OnFocusRingChanged, GetWeakPtr());
chromeos::AccessibilityManager::Get()->SetFocusRingObserverForTest(callback);
ash::AccessibilityFocusRingController* controller =
ash::Shell::Get()->accessibility_focus_ring_controller();
controller->SetNoFadeForTesting();
const ash::AccessibilityFocusRingGroup* focus_ring_group =
controller->GetFocusRingGroupForTesting(
extension_misc::kSelectToSpeakExtensionId);
// No focus rings to start.
EXPECT_EQ(nullptr, focus_ring_group);
ui_test_utils::NavigateToURL(browser(), GURL("data:text/html;charset=utf-8,"
"<p>This is some text</p>"));
gfx::Rect bounds = GetWebContentsBounds();
PrepareToWaitForFocusRingChanged();
generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */);
generator_->MoveMouseTo(bounds.x(), bounds.y());
generator_->PressLeftButton();
// Expect a focus ring to have been drawn.
WaitForFocusRingChanged();
focus_ring_group = controller->GetFocusRingGroupForTesting(
extension_misc::kSelectToSpeakExtensionId);
ASSERT_NE(nullptr, focus_ring_group);
std::vector<std::unique_ptr<ash::AccessibilityFocusRingLayer>> const&
focus_rings = focus_ring_group->focus_layers_for_testing();
EXPECT_EQ(focus_rings.size(), 1u);
gfx::Rect target_bounds = focus_rings.at(0)->layer()->GetTargetBounds();
// Make sure it's in a reasonable position.
EXPECT_LT(abs(target_bounds.x() - bounds.x()), 50);
EXPECT_LT(abs(target_bounds.y() - bounds.y()), 50);
EXPECT_LT(target_bounds.width(), 50);
EXPECT_LT(target_bounds.height(), 50);
// Move the mouse.
PrepareToWaitForFocusRingChanged();
generator_->MoveMouseTo(bounds.x() + 100, bounds.y() + 100);
// Expect focus ring to have moved with the mouse.
// The size should have grown to be over 100 (the rect is now size 100,
// and the focus ring has some buffer). Position should be unchanged.
WaitForFocusRingChanged();
target_bounds = focus_rings.at(0)->layer()->GetTargetBounds();
EXPECT_LT(abs(target_bounds.x() - bounds.x()), 50);
EXPECT_LT(abs(target_bounds.y() - bounds.y()), 50);
EXPECT_GT(target_bounds.width(), 100);
EXPECT_GT(target_bounds.height(), 100);
// Move the mouse smaller again, it should shrink.
PrepareToWaitForFocusRingChanged();
generator_->MoveMouseTo(bounds.x() + 10, bounds.y() + 18);
WaitForFocusRingChanged();
target_bounds = focus_rings.at(0)->layer()->GetTargetBounds();
EXPECT_LT(target_bounds.width(), 50);
EXPECT_LT(target_bounds.height(), 50);
// Cancel this by releasing the key before the mouse.
PrepareToWaitForFocusRingChanged();
generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */);
generator_->ReleaseLeftButton();
// Expect focus ring to have been cleared, this was canceled in STS
// by releasing the key before the button.
WaitForFocusRingChanged();
EXPECT_EQ(focus_rings.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, ContinuesReadingDuringResize) {
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<p>First paragraph</p>"
"<div id='resize' style='width:300px; font-size: 10em'>"
"<p>Second paragraph is longer than 300 pixels and will wrap when "
"resized</p></div>");
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"First paragraph*"));
// Resize before second is spoken. If resizing caused errors finding the
// inlineTextBoxes in the node, speech would be stopped early.
ExecuteJavaScriptInForeground(
"document.getElementById('resize').style.width='100px'");
EXPECT_TRUE(
base::MatchPattern(speech_monitor_.GetNextUtterance(), "*when*resized*"));
}
IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, WorksWithStickyKeys) {
AccessibilityManager::Get()->EnableStickyKeys(true);
ui_test_utils::NavigateToURL(
browser(), GURL("data:text/html;charset=utf-8,<p>This is some text</p>"));
// Tap Search and click a few pixels into the window bounds.
generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */);
generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */);
// Sticky keys should remember the 'search' key was clicked, so STS is
// actually in a capturing mode now.
gfx::Rect bounds = GetWebContentsBounds();
generator_->MoveMouseTo(bounds.x(), bounds.y());
generator_->PressLeftButton();
generator_->MoveMouseTo(bounds.x() + bounds.width(),
bounds.y() + bounds.height());
generator_->ReleaseLeftButton();
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(),
"This is some text*"));
// Reset state.
AccessibilityManager::Get()->EnableStickyKeys(false);
}
} // namespace chromeos