Commit 655b8048 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

ChromeVox: Implement and test UserAnnotationHandler.

Phase 2 of ChromeVox custom annotations project.

This change adds a new file to the ChromeVox codebase called
user_annotation_handler.js. This file exposes methods to create
and get annotations for AutomationNodes. It also modifies the Output
module to check for annotations when outputting the node name, and
reports one to the user if found (and the feature is enabled). Lastly,
this change adds tests in the user_annotation_handler_test.js file,
which act as integration tests by asserting speech output.

This file is being added to support ChromeVox custom annotations.
For more details on that project, please see the following design

document: 
https: //docs.google.com/document/d/1K6Sg0wLOjUdz7ycJqxPnFWNvie7yaJ3enlss10cXKqM/edit
Change-Id: Ib35c77f8df295f9dbae5ba702ffef6af6668b269
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1992805
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#739136}
parent 58df285a
......@@ -93,7 +93,8 @@ chromevox_modules = [
"background/media_automation_handler.js",
"background/mouse_handler.js",
"background/next_earcons.js",
"background/node_identifier.js",
"background/annotation/node_identifier.js",
"background/annotation/user_annotation_handler.js",
"background/notifications.js",
"background/output.js",
"background/logging/output_logger.js",
......@@ -434,6 +435,8 @@ if (is_chromeos) {
js2gtest("chromevox_extjs_tests") {
test_type = "extension"
sources = [
"background/annotation/node_identifier_test.js",
"background/annotation/user_annotation_handler_test.js",
"background/automation_util_test.js",
"background/background_test.js",
"background/braille_command_data_test.js",
......@@ -445,7 +448,6 @@ if (is_chromeos) {
"background/language_switching_test.js",
"background/live_regions_test.js",
"background/logging/log_store_test.js",
"background/node_identifier_test.js",
"background/output_test.js",
"background/panel/i_search_test.js",
"background/panel/panel_test.js",
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
// Include test fixture.
GEN_INCLUDE(['../testing/chromevox_next_e2e_test_base.js']);
GEN_INCLUDE(['../../testing/chromevox_next_e2e_test_base.js']);
/**
* Test fixture for NodeIdentifier.
......
// 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.
/**
* @fileoverview Manages ChromeVox users' custom annotations.
*/
goog.provide('UserAnnotationHandler');
goog.require('NodeIdentifier');
/**
* Stores annotation-related data.
* @typedef {{
* annotation: !string,
* identifier: !NodeIdentifier
* }}
*/
let AnnotationData;
UserAnnotationHandler = class {
static init() {
UserAnnotationHandler.instance = new UserAnnotationHandler();
}
/**
* @private
*/
constructor() {
/**
* Stores user annotations.
* Maps URLs to arrays of AnnotationData objects for that page.
* @private {Object<string, !Array<!AnnotationData>>}
*/
this.annotations_ = {};
/**
* Whether this feature is enabled or not.
* @type {boolean}
* @private
*/
this.enabled_ = false;
chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-chromevox-annotations',
(enabled) => {
this.enabled_ = enabled;
});
}
/**
* Creates a new, or updates an existing, annotation for the node.
* @param {!AutomationNode} node
* @param {!string} annotation
*/
static setAnnotationForNode(node, annotation) {
const url = node.root.docUrl || '';
if (!UserAnnotationHandler.instance.enabled_ || !url) {
return;
}
const annotationData = {annotation, identifier: new NodeIdentifier(node)};
if (!UserAnnotationHandler.instance.annotations_[url]) {
UserAnnotationHandler.instance.annotations_[url] = [];
}
// Either update an existing annotation or add a new one.
const annotations = UserAnnotationHandler.instance.annotations_[url];
const target = annotationData.identifier;
let updated = false;
for (let i = 0; i < annotations.length; ++i) {
if (target.equals(annotations[i].identifier)) {
UserAnnotationHandler.instance.annotations_[url][i] = annotationData;
updated = true;
break;
}
}
if (!updated) {
UserAnnotationHandler.instance.annotations_[url].push(annotationData);
}
}
/**
* Returns the annotation for node, if one exists. Otherwise, returns null.
* @param {!AutomationNode} node
* @return {?string}
*/
static getAnnotationForNode(node) {
const url = node.root.docUrl || '';
if (!UserAnnotationHandler.instance.enabled_ || !url) {
return null;
}
const candidates = UserAnnotationHandler.instance.annotations_[url];
if (!candidates) {
return null;
}
const target = new NodeIdentifier(node);
for (let i = 0; i < candidates.length; ++i) {
const candidate = candidates[i];
if (target.equals(candidate.identifier)) {
return candidate.annotation;
}
}
return null;
}
};
\ No newline at end of file
// 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.
// Include test fixture.
GEN_INCLUDE(['../../testing/chromevox_next_e2e_test_base.js']);
GEN_INCLUDE(['../../testing/fake_objects.js']);
GEN_INCLUDE(['../../testing/mock_feedback.js']);
/**
* Test fixture for UserAnnotationHandler.
* @constructor
* @extends {ChromeVoxE2ETest}
*/
ChromeVoxAnnotationTest = class extends ChromeVoxNextE2ETest {
/** @override */
testGenCppIncludes() {
GEN(`
// The following includes are copy-pasted from chromevox_e2e_test_base.js.
#include "ash/accessibility/accessibility_delegate.h"
#include "ash/shell.h"
#include "base/bind.h"
#include "base/callback.h"
#include "chrome/browser/chromeos/accessibility/accessibility_manager.h"
#include "chrome/common/extensions/extension_constants.h"
#include "extensions/common/extension_l10n_util.h"
// The following includes are necessary for this test file.
#include "base/command_line.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/base/ui_base_switches.h"
`);
}
/** @override */
testGenPreamble() {
GEN(`
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kEnableExperimentalAccessibilityChromeVoxAnnotations);
// Copy-pasted from chromevox_e2e_test_base.js.
auto allow = extension_l10n_util::AllowGzippedMessagesAllowedForTest();
base::Closure load_cb =
base::Bind(&chromeos::AccessibilityManager::EnableSpokenFeedback,
base::Unretained(chromeos::AccessibilityManager::Get()),
true);
WaitForExtension(extension_misc::kChromeVoxExtensionId, load_cb);
`);
}
assertNumberOfAnnotationsForUrl(url, numAnnotations) {
const annotations = UserAnnotationHandler.instance.annotations_[url];
if (!annotations) {
console.error('No annotations for provided url');
assertFalse(true);
}
assertEquals(numAnnotations, annotations.length);
}
/**
* @return{!MockFeedback}
*/
createMockFeedback() {
const mockFeedback =
new MockFeedback(this.newCallback(), this.newCallback.bind(this));
mockFeedback.install();
return mockFeedback;
}
/**
* Create a function which performs the command |cmd|.
* @param {string} cmd
* @return {function(): void}
*/
doCmd(cmd) {
return function() {
CommandHandler.onCommand(cmd);
};
}
/**
* Returns the start node of the current ChromeVox range.
* @return {AutomationNode}
*/
getRangeStart() {
return ChromeVoxState.instance.getCurrentRange().start.node;
}
/** @override */
setUp() {
window.doCmd = this.doCmd;
window.RoleType = chrome.automation.RoleType;
}
// Test documents //
get basicButtonDoc() {
return `
<p>Start here</p>
<button>Apple</button>
<button>Orange</button>
`;
}
get duplicateButtonDoc() {
return `
<p>Start here</p>
<button>Click me</button>
<button>Click me</button>
`;
}
};
TEST_F('ChromeVoxAnnotationTest', 'BasicButtonTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.basicButtonDoc, function(root) {
const pageUrl = root.docUrl;
// Create annotations for both buttons.
CommandHandler.onCommand('nextButton');
const appleNode = this.getRangeStart();
UserAnnotationHandler.setAnnotationForNode(appleNode, 'Batman');
CommandHandler.onCommand('nextButton');
const orangeNode = this.getRangeStart();
UserAnnotationHandler.setAnnotationForNode(orangeNode, 'Robin');
this.assertNumberOfAnnotationsForUrl(pageUrl, 2);
assertEquals(
UserAnnotationHandler.getAnnotationForNode(appleNode), 'Batman');
assertEquals(
UserAnnotationHandler.getAnnotationForNode(orangeNode), 'Robin');
CommandHandler.onCommand('jumpToTop');
mockFeedback.call(doCmd('nextButton'))
.expectSpeech('Batman', 'Button')
.call(doCmd('nextButton'))
.expectSpeech('Robin', 'Button')
.replay();
});
});
TEST_F('ChromeVoxAnnotationTest', 'DuplicateButtonTest', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.duplicateButtonDoc, function(root) {
const pageUrl = root.docUrl;
CommandHandler.onCommand('nextButton');
const firstButton = this.getRangeStart();
UserAnnotationHandler.setAnnotationForNode(firstButton, 'First button');
CommandHandler.onCommand('nextButton');
const secondButton = this.getRangeStart();
UserAnnotationHandler.setAnnotationForNode(secondButton, 'Second button');
this.assertNumberOfAnnotationsForUrl(pageUrl, 2);
assertEquals(
UserAnnotationHandler.getAnnotationForNode(firstButton),
'First button');
assertEquals(
UserAnnotationHandler.getAnnotationForNode(secondButton),
'Second button');
CommandHandler.onCommand('jumpToTop');
mockFeedback.call(doCmd('nextButton'))
.expectSpeech('First button', 'Button')
.call(doCmd('nextButton'))
.expectSpeech('Second button', 'Button')
.replay();
});
});
TEST_F('ChromeVoxAnnotationTest', 'UpdateAnnotationTest', function() {
this.runWithLoadedTree(this.basicButtonDoc, function(root) {
const pageUrl = root.docUrl;
let appleButton =
root.find({role: RoleType.BUTTON, attributes: {name: 'Apple'}});
assertFalse(!appleButton);
UserAnnotationHandler.setAnnotationForNode(appleButton, 'Good morning');
this.assertNumberOfAnnotationsForUrl(pageUrl, 1);
assertEquals(
'Good morning',
UserAnnotationHandler.getAnnotationForNode(appleButton));
// Update annotation.
appleButton =
root.find({role: RoleType.BUTTON, attributes: {name: 'Apple'}});
assertFalse(!appleButton);
UserAnnotationHandler.setAnnotationForNode(appleButton, 'Good night');
this.assertNumberOfAnnotationsForUrl(root.docUrl, 1);
assertEquals(
'Good night', UserAnnotationHandler.getAnnotationForNode(appleButton));
});
});
......@@ -41,6 +41,7 @@ goog.require('ChromeVoxEditableTextBase');
goog.require('ExtensionBridge');
goog.require('NavBraille');
goog.require('NodeIdentifier');
goog.require('UserAnnotationHandler');
goog.scope(function() {
const AutomationNode = chrome.automation.AutomationNode;
......@@ -143,6 +144,7 @@ Background = class extends ChromeVoxState {
DownloadHandler.init();
LanguageSwitching.init();
PhoneticData.init();
UserAnnotationHandler.init();
Notifications.onStartup();
......
......@@ -28,6 +28,7 @@ goog.require('ValueSelectionSpan');
goog.require('ValueSpan');
goog.require('goog.i18n.MessageFormat');
goog.require('LanguageSwitching');
goog.require('UserAnnotationHandler');
goog.scope(function() {
const AutomationNode = chrome.automation.AutomationNode;
......@@ -767,13 +768,11 @@ Output = class {
node, 'name',
appendStringWithLanguage.bind(this, buff, options));
} else {
// Append entire node name.
// TODO(akihiroota): Follow-up with dtseng about why we append
// empty string.
this.append_(buff, node.name || '', options);
const nameOrAnnotation =
UserAnnotationHandler.getAnnotationForNode(node) || node.name;
this.append_(buff, nameOrAnnotation || '', options);
}
ruleStr.writeTokenWithValue(token, node.name);
} else if (token == 'description') {
if (node.name == node.description) {
return;
......
......@@ -351,7 +351,7 @@ MockFeedback = class {
}
/**
* Processes any feedback that has been received so far and treis to
* Processes any feedback that has been received so far and tries to
* satisfy the registered expectations. Any feedback that is received
* after this call (via the installed mock objects) is processed immediately.
* When all expectations are satisfied and registered callbacks called,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment