Commit b49c7c19 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

ChromeVox: Implement and test NodeIdentifier objects and methods.

Phase 1 of ChromeVox custom annotations project.

This change adds a new file to the ChromeVox codebase called
node_identifier.js. This file contains objects and methods used to
uniquely identify AutomationNodes. Several basic tests have also been
implemented in node_identifier_test.js.

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: Id5c60ec9257bef0738366d045100915e26d19e6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1992594
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#734949}
parent 8df0186d
......@@ -93,6 +93,7 @@ chromevox_modules = [
"background/media_automation_handler.js",
"background/mouse_handler.js",
"background/next_earcons.js",
"background/node_identifier.js",
"background/notifications.js",
"background/output.js",
"background/logging/output_logger.js",
......@@ -444,6 +445,7 @@ 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",
......
......@@ -40,6 +40,7 @@ goog.require('ChromeVoxBackground');
goog.require('ChromeVoxEditableTextBase');
goog.require('ExtensionBridge');
goog.require('NavBraille');
goog.require('NodeIdentifier');
goog.scope(function() {
var AutomationNode = chrome.automation.AutomationNode;
......
// 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 Defines a class used to identify AutomationNodes.
*/
goog.provide('NodeIdentifier');
/**
* Stores all identifying attributes for an AutomationNode.
* A helper object for NodeIdentifier.
* @typedef {{
* id: !string,
* name: !string,
* role: !string,
* description: !string,
* restriction: !string,
* childCount: number,
* indexInParent: number,
* className: !string,
* htmlTag: !string }}
*/
var Attributes;
/**
* @param {!AutomationNode} node
* @constructor
*/
NodeIdentifier = function(node) {
/**
* @type {!Attributes}
*/
this.attributes = this.createAttributes_(node);
/**
* @type {!string}
*/
this.pageUrl = node.root.docUrl || '';
/**
* @type {!Array<!Attributes>}
*/
this.ancestry = this.createAttributesAncestry_(node);
};
NodeIdentifier.prototype = {
/**
* Returns true if |this| is equal to |other|.
* @param {!NodeIdentifier} other
* @return {boolean}
*/
equals: function(other) {
// If pageUrl and HTML Id match, we know they refer to the same node.
if (this.pageUrl && this.attributes.id && this.pageUrl === other.pageUrl &&
this.attributes.id === other.attributes.id) {
return true;
}
// Ensure both NodeIdentifiers are composed of matching Attributes.
if (!this.matchingAttributes_(this.attributes, other.attributes)) {
return false;
}
if (this.ancestry.length !== other.ancestry.length) {
return false;
}
for (var i = 0; i < this.ancestry.length; ++i) {
if (!this.matchingAttributes_(this.ancestry[i], other.ancestry[i])) {
return false;
}
}
return true;
},
/**
* @param {!AutomationNode} node
* @return {!Attributes}
* @private
*/
createAttributes_: function(node) {
return {
id: (node.htmlAttributes) ? node.htmlAttributes['id'] || '' : '',
name: node.name || '',
role: node.role || '',
description: node.description || '',
restriction: node.restriction || '',
childCount: node.childCount || 0,
indexInParent: node.indexInParent || 0,
className: node.className || '',
htmlTag: node.htmlTag || ''
};
},
/**
* @param {!AutomationNode} node
* @return {!Array<!Attributes>}
* @private
*/
createAttributesAncestry_: function(node) {
var ancestry = [];
var scanNode = node.parent;
var treeRoot = node.root;
while (scanNode && scanNode !== treeRoot) {
ancestry.push(this.createAttributes_(scanNode));
scanNode = scanNode.parent;
}
return ancestry;
},
/**
* @param {!Attributes} target
* @param {!Attributes} candidate
* @return {boolean}
* @private
*/
matchingAttributes_: function(target, candidate) {
for (var [key, targetValue] of Object.entries(target)) {
if (candidate[key] !== targetValue) {
return false;
}
}
return true;
}
};
// 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']);
/**
* Test fixture for NodeIdentifier.
* @constructor
* @extends {ChromeVoxE2ETest}
*/
function ChromeVoxNodeIdentifierTest() {
ChromeVoxNextE2ETest.call(this);
}
ChromeVoxNodeIdentifierTest.prototype = {
__proto__: ChromeVoxNextE2ETest.prototype,
/**
* Returns the start node of the current ChromeVox range.
*/
getRangeStart: function() {
return ChromeVoxState.instance.getCurrentRange().start.node;
},
/** @override */
setUp: function() {
window.RoleType = chrome.automation.RoleType;
},
// Test documents //
basicButtonDoc: `
<p>Start here</p>
<button id="apple-button">Apple</button>
<button>Orange</button>
`,
duplicateButtonDoc: `
<p>Start here</p>
<button>Click me</button>
<button>Click me</button>
`,
identicalListsDoc: `
<p>Start here</p>
<ul>
<li>Apple</li>
<li>Orange</li>
</ul>
<ul>
<li>Apple</li>
<li>Orange</li>
</ul>
`
};
// Tests that we can distinguish between two similar buttons.
TEST_F('ChromeVoxNodeIdentifierTest', 'BasicButtonTest', function() {
this.runWithLoadedTree(this.basicButtonDoc, function(rootNode) {
var appleNode =
rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Apple'}});
var orangeNode =
rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Orange'}});
assertFalse(!appleNode);
assertFalse(!orangeNode);
var appleId = new NodeIdentifier(appleNode);
var duplicateAppleId = new NodeIdentifier(appleNode);
var orangeId = new NodeIdentifier(orangeNode);
assertTrue(appleId.equals(duplicateAppleId));
assertFalse(appleId.equals(orangeId));
});
});
// Tests that we can distinguish two buttons with the same name.
TEST_F('ChromeVoxNodeIdentifierTest', 'DuplicateButtonTest', function() {
this.runWithLoadedTree(this.duplicateButtonDoc, function() {
CommandHandler.onCommand('nextButton');
var firstButton = this.getRangeStart();
var firstButtonId = new NodeIdentifier(firstButton);
CommandHandler.onCommand('nextButton');
var secondButton = this.getRangeStart();
var secondButtonId = new NodeIdentifier(secondButton);
assertFalse(firstButtonId.equals(secondButtonId));
});
});
// Tests that we can differentiate between the list items of two identical
// lists.
TEST_F('ChromeVoxNodeIdentifierTest', 'IdenticalListsTest', function() {
this.runWithLoadedTree(this.identicalListsDoc, function() {
// Create NodeIdentifiers for each item.
CommandHandler.onCommand('nextObject');
CommandHandler.onCommand('nextObject');
var firstApple = new NodeIdentifier(this.getRangeStart());
CommandHandler.onCommand('nextObject');
CommandHandler.onCommand('nextObject');
var firstOrange = new NodeIdentifier(this.getRangeStart());
CommandHandler.onCommand('nextObject');
CommandHandler.onCommand('nextObject');
var secondApple = new NodeIdentifier(this.getRangeStart());
CommandHandler.onCommand('nextObject');
CommandHandler.onCommand('nextObject');
var secondOrange = new NodeIdentifier(this.getRangeStart());
assertFalse(firstApple.equals(secondApple));
assertFalse(firstOrange.equals(secondOrange));
});
});
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