blob: 784909189358be4de6dccd323673ed28efdcbd3e [file] [log] [blame]
// Copyright 2020 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.
GEN_INCLUDE([
'accessibility_test_base.js',
'assert_additions.js',
'callback_helper.js',
'common.js',
'doc_utils.js',
]);
/**
* Base test fixture for end to end tests (tests that need a full extension
* renderer) for accessibility component extensions. These tests run inside of
* the extension's background page context.
*/
E2ETestBase = class extends AccessibilityTestBase {
constructor() {
super();
this.callbackHelper_ = new CallbackHelper(this);
this.desktop_;
}
/** @override */
async setUpDeferred() {
await super.setUpDeferred();
await importModule('EventGenerator', '/common/event_generator.js');
await importModule('KeyCode', '/common/key_code.js');
}
/** @override */
testGenCppIncludes() {
GEN(`
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/speech/extension_api/tts_engine_extension_api.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"
`);
}
/** @override */
testGenPreamble() {
GEN(`
TtsExtensionEngine::GetInstance()->DisableBuiltInTTSEngineForTesting();
if (ash_starter()->HasLacrosArgument()) {
crosapi::BrowserManager::Get()->NewTab(false);
ASSERT_TRUE(crosapi::BrowserManager::Get()->IsRunning());
}
`);
}
/** @override */
testGenPostamble() {
GEN(`
if (fail_on_console_error) {
EXPECT_EQ(0u, console_observer.messages().size())
<< "Found console.warn or console.error with message: "
<< console_observer.GetMessageAt(0);
}
`);
}
testGenPreambleCommon(extensionIdName, failOnConsoleError = true) {
GEN(`
WaitForExtension(extension_misc::${extensionIdName}, std::move(load_cb));
extensions::ExtensionHost* host =
extensions::ProcessManager::Get(browser()->profile())
->GetBackgroundHostForExtension(
extension_misc::${extensionIdName});
bool fail_on_console_error = ${failOnConsoleError};
content::WebContentsConsoleObserver console_observer(host->host_contents());
// A11y extensions should not log warnings or errors: these should cause
// test failures.
auto filter =
[](const content::WebContentsConsoleObserver::Message& message) {
return message.log_level ==
blink::mojom::ConsoleMessageLevel::kWarning ||
message.log_level == blink::mojom::ConsoleMessageLevel::kError;
};
if (fail_on_console_error) {
console_observer.SetFilter(base::BindRepeating(filter));
}
`);
}
/**
* Listens and waits for the first event on the given node of the given type.
* @param {!chrome.automation.AutomationNode} node
* @param {!chrome.automation.EventType} eventType
* @param {!function()} callback
* @param {boolean} capture
*/
listenOnce(node, eventType, callback, capture) {
const innerCallback = this.newCallback(function() {
node.removeEventListener(eventType, innerCallback, capture);
callback.apply(this, arguments);
});
node.addEventListener(eventType, innerCallback, capture);
}
/**
* Listens to and waits for the specified event type on the given node until
* |predicate| is satisfied.
* @param {!function(): boolean} predicate
* @param {!chrome.automation.AutomationNode} node
* @param {!chrome.automation.EventType} eventType
* @param {!function()} callback
* @param {boolean} capture
*/
listenUntil(predicate, node, eventType, callback, capture = false) {
callback = this.newCallback(callback);
if (predicate()) {
callback();
return;
}
const listener = () => {
if (predicate()) {
node.removeEventListener(eventType, listener, capture);
callback.apply(this, arguments);
}
};
node.addEventListener(eventType, listener, capture);
}
/**
* Waits for the given |eventType| to be fired on |node|.
* @param {!chrome.automation.AutomationNode} node
* @param {!chrome.automation.EventType} eventType
* @param {boolean=} capture
*/
async waitForEvent(node, eventType, capture) {
return new Promise(this.newCallback(resolve => {
const callback = this.newCallback(() => {
node.removeEventListener(eventType, callback, capture);
resolve();
});
node.addEventListener(eventType, callback, capture);
}));
}
/**
* @param {!chrome.automation.AutomationNode} app
* @return {boolean}
*/
isInLacrosWindow(app) {
// We validate we're actually within a Lacros window by scanning upward
// until we see the presence of an app id, which indicates an app subtree.
// See go/lacros-accessibility for details.
while (app && !app.appId) {
app = app.parent;
}
return Boolean(app);
}
/**
* @param {string} url
* @param {!chrome.automation.AutomationNode} addressBar
*/
async navigateToUrlForLacros(url, addressBar) {
// This populates the address bar as if we typed the url.
addressBar.setValue(url);
// We have two choices to confirm navigation.
if (!this.navigateLacrosWithAutoComplete) {
// 1. (default), hit enter.
await this.waitForEvent(addressBar, 'valueChanged');
console.log('Sending key press');
EventGenerator.sendKeyPress(KeyCode.RETURN);
} else {
// 2. use the auto completion.
await this.waitForEvent(addressBar, 'controlsChanged');
// The text field relates to the auto complete list box via controlledBy.
// The |controls| node structure here nests several levels until the
// listBoxOption we want.
const autoCompleteListBoxOption =
addressBar.controls[0].firstChild.firstChild;
assertEquals('listBoxOption', autoCompleteListBoxOption.role);
autoCompleteListBoxOption.doDefault();
}
}
/**
* Creates a callback that optionally calls {@code opt_callback} when
* called. If this method is called one or more times, then
* {@code testDone()} will be called when all callbacks have been called.
* @param {Function=} opt_callback Wrapped callback that will have its this
* reference bound to the test fixture. Optionally, return a promise to
* defer completion.
* @return {Function}
*/
newCallback(opt_callback) {
return this.callbackHelper_.wrap(opt_callback);
}
/**
* Gets the desktop from the automation API and runs |callback|.
* Arranges to call |testDone()| after |callback| returns.
* NOTE: Callbacks created inside |callback| must be wrapped with
* |this.newCallback| if passed to asynchronous calls. Otherwise, the test
* will be finished prematurely.
* @param {function(chrome.automation.AutomationNode)} callback
* Called with the desktop node once it's retrieved.
*/
runWithLoadedDesktop(callback) {
chrome.automation.getDesktop(this.newCallback(callback));
}
/**
* Gets the desktop from the automation API and Launches a new tab with
* the given document, and returns the root web area when a load complete
* fires.
* @param {string|function(): string} doc An HTML snippet, optionally wrapped
* inside of a function.
* @param {{url: (string=)}}
* opt_params
* url Optional url to wait for. Defaults to undefined.
* @return {chrome.automation.AutomationNode} the root web area node, only
* returned once the document is ready.
*/
async runWithLoadedTree(doc, opt_params = {}) {
return new Promise(this.newCallback(async resolve => {
// Make sure the test doesn't finish until this function has resolved.
let callback = this.newCallback(resolve);
this.desktop_ = await new Promise(r => chrome.automation.getDesktop(r));
const url = opt_params.url || DocUtils.createUrlForDoc(doc);
const hasLacrosChromePath = await new Promise(
r => chrome.commandLinePrivate.hasSwitch('lacros-chrome-path', r));
// The below block handles opening a url either in a Lacros tab or Ash
// tab. For Lacros, we re-use an already open Lacros tab. For Ash, we use
// the chrome.tabs api.
// This flag controls whether we've requested navigation to |url| within
// the open Lacros tab.
let didNavigateForLacros = false;
// Listener for both load complete and focus events that eventually
// triggers the test.
const listener = async event => {
if (hasLacrosChromePath && !didNavigateForLacros) {
// We have yet to request navigation in the Lacros tab. Do so now by
// getting the default focus (the address bar), setting the value to
// the url and then performing do default on the auto completion node.
const focus = await new Promise(r => chrome.automation.getFocus(r));
// It's possible focus is elsewhere; wait until it lands on the
// address bar text field.
if (focus.role !== chrome.automation.RoleType.TEXT_FIELD) {
return;
}
if (this.isInLacrosWindow(focus)) {
didNavigateForLacros = true;
await this.navigateToUrlForLacros(url, focus);
}
return; // exit listener.
}
// Navigation has occurred, but we need to ensure the url we want has
// loaded.
if (event.target.root.url !== url || !event.target.root.docLoaded) {
return; // exit listener.
}
// Finally, when we get here, we've successfully navigated to
// the |url| in either Lacros or Ash.
this.desktop_.removeEventListener('focus', listener, true);
this.desktop_.removeEventListener('loadComplete', listener, true);
if (callback) {
callback(event.target.root);
}
// Avoid calling |callback| twice (which would cause the test to fail).
callback = null;
}; // end listener.
// Setup the listener above for focus and load complete listening.
this.desktop_.addEventListener('focus', listener, true);
this.desktop_.addEventListener('loadComplete', listener, true);
// The easy case -- just open the Ash tab.
if (!hasLacrosChromePath) {
const createParams = {active: true, url};
chrome.tabs.create(createParams);
} else {
chrome.automation.getFocus(f => {
listener({target: f});
});
}
}));
}
/**
* Opens the options page for the running extension and calls |callback| with
* the options page root once ready.
* @param {function(chrome.automation.AutomationNode)} callback
* @param {!RegExp} matchUrlRegExp The url pattern of the options page if
* different than the supplied default pattern below.
*/
runWithLoadedOptionsPage(callback, matchUrlRegExp = /options.html/) {
callback = this.newCallback(callback);
chrome.automation.getDesktop(desktop => {
const listener = event => {
if (!matchUrlRegExp.test(event.target.docUrl) ||
!event.target.docLoaded) {
return;
}
desktop.removeEventListener(
chrome.automation.EventType.LOAD_COMPLETE, listener);
callback(event.target);
};
desktop.addEventListener(
chrome.automation.EventType.LOAD_COMPLETE, listener);
chrome.runtime.openOptionsPage();
});
}
/**
* Finds one specific node in the automation tree.
* This function is expected to run within a callback passed to
* runWithLoadedTree().
* @param {function(chrome.automation.AutomationNode): boolean} predicate A
* predicate that uniquely specifies one automation node.
* @param {string=} nodeDescription An optional description of what node was
* being looked for.
* @return {!chrome.automation.AutomationNode}
*/
findNodeMatchingPredicate(
predicate, nodeDescription = 'node matching the predicate') {
assertNotNullNorUndefined(
this.desktop_,
'findNodeMatchingPredicate called from invalid location.');
const treeWalker = new AutomationTreeWalker(
this.desktop_, constants.Dir.FORWARD, {visit: predicate});
const node = treeWalker.next().node;
assertNotNullNorUndefined(node, 'Could not find ' + nodeDescription + '.');
assertNullOrUndefined(
treeWalker.next().node, 'Found more than one ' + nodeDescription + '.');
return node;
}
};
/** @override */
E2ETestBase.prototype.isAsync = true;