Commit d6f26ce4 authored by David Tseng's avatar David Tseng Committed by Commit Bot

Add initial support for AutomationPosition

This patch introduces AutomationPosition. It acts as a wrapper of an
AXNodePosition and utilizes gin::Wrappable to ease the burden of surfacing
positions in javascript. gin::Wrappable handles the details of object lifetime
between the object within V8 and the native C++ object.

This change has gone to great lengths to make it as easy as possible to define methods and properties on this wrapper object so that they are self-contained and trivial to add, remove, and edit. This will facilitate future changes in the underlying AXPosition class and make it straightforward for anyone changing the api.

Bug: 1048367
Test: browser_tests
Change-Id: I9c881002ffcb991e4f8a1db3c269e5be0e9144da
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1814650
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738880}
parent d4d8ebd1
......@@ -448,6 +448,12 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTestWithDeviceScaleFactor, HitTest) {
<< message_;
}
IN_PROC_BROWSER_TEST_F(AutomationApiTest, Position) {
StartEmbeddedTestServer();
ASSERT_TRUE(RunExtensionSubtest("automation/tests/desktop", "position.html"))
<< message_;
}
#endif // defined(OS_CHROMEOS)
} // namespace extensions
<!--
* 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.
-->
<html>
<head>
<title>Automation Tests - Positions</title>
</head>
<body>
<button aria-label="Tree Position"></button>
<p>some text</p>
<script>
const button = document.querySelector('button');
button.addEventListener('focus', () => {
button.remove();
});
</script>
</body>
</html>
<!--
* 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.
-->
<script src="common.js"></script>
<script src="position.js"></script>
// 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.
var allTests = [
function testPositionType() {
var button = rootNode.find({role: chrome.automation.RoleType.BUTTON});
var position = button.createPosition(0 /* offset */);
assertEq(button, position.node);
assertFalse(position.isNullPosition());
assertTrue(position.isTreePosition());
assertFalse(position.isTextPosition());
assertFalse(position.isLeafTextPosition());
assertEq(0, position.childIndex);
assertEq(-1, position.textOffset);
assertEq('downstream', position.affinity);
chrome.test.succeed();
},
function testBackingObjectsDiffer() {
var childOfRoot = rootNode.lastChild;
var pos1 = childOfRoot.createPosition(-1);
var pos2 = rootNode.createPosition(-1);
assertFalse(pos1.node == pos2.node, 'Nodes expected to differ');
chrome.test.succeed();
},
function testCrossRoots() {
chrome.automation.getDesktop(() => {
var rootPosition = rootNode.createPosition(-1);
rootPosition.moveToParentPosition();
assertTrue(!!rootPosition.node);
chrome.test.succeed();
});
}
];
setUpAndRunTestsInPage(allTests, 'position.html');
......@@ -18,6 +18,8 @@ jumbo_source_set("renderer") {
"api/automation/automation_ax_tree_wrapper.h",
"api/automation/automation_internal_custom_bindings.cc",
"api/automation/automation_internal_custom_bindings.h",
"api/automation/automation_position.cc",
"api/automation/automation_position.h",
"api/display_source/display_source_session.cc",
"api/display_source/display_source_session.h",
"api_activity_logger.cc",
......
......@@ -28,6 +28,7 @@
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/automation.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/renderer/api/automation/automation_position.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/script_context.h"
#include "gin/converter.h"
......@@ -597,6 +598,7 @@ void AutomationInternalCustomBindings::AddRoutes() {
ROUTE_FUNCTION(GetFocus);
ROUTE_FUNCTION(GetHtmlAttributes);
ROUTE_FUNCTION(GetState);
ROUTE_FUNCTION(CreateAutomationPosition);
#undef ROUTE_FUNCTION
// Bindings that take a Tree ID and return a property of the tree.
......@@ -1857,6 +1859,36 @@ void AutomationInternalCustomBindings::GetState(
args.GetReturnValue().Set(state.Build());
}
void AutomationInternalCustomBindings::CreateAutomationPosition(
const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = GetIsolate();
if (args.Length() < 4 || !args[0]->IsString() /* tree id */ ||
!args[1]->IsInt32() /* node id */ || !args[2]->IsInt32() /* offset */ ||
!args[3]->IsBoolean() /* is upstream affinity */) {
ThrowInvalidArgumentsException(this);
}
ui::AXTreeID tree_id =
ui::AXTreeID::FromString(*v8::String::Utf8Value(isolate, args[0]));
int node_id = args[1]->Int32Value(context()->v8_context()).ToChecked();
AutomationAXTreeWrapper* tree_wrapper =
GetAutomationAXTreeWrapperFromTreeID(tree_id);
if (!tree_wrapper)
return;
ui::AXNode* node = tree_wrapper->tree()->GetFromId(node_id);
if (!node)
return;
int offset = args[2]->Int32Value(context()->v8_context()).ToChecked();
bool is_upstream = args[3]->BooleanValue(isolate);
gin::Handle<AutomationPosition> handle = gin::CreateHandle(
isolate, new AutomationPosition(*node, offset, is_upstream));
args.GetReturnValue().Set(handle.ToV8().As<v8::Object>());
}
void AutomationInternalCustomBindings::UpdateOverallTreeChangeObserverFilter() {
tree_change_observer_overall_filter_ = 0;
for (const auto& observer : tree_change_observers_)
......
......@@ -198,6 +198,13 @@ class AutomationInternalCustomBindings : public ObjectBackedNativeHandler {
// Returns: JS object with a string key for each state flag that's set.
void GetState(const v8::FunctionCallbackInfo<v8::Value>& args);
// Creates the backing AutomationPosition native object given a request from
// javascript.
// Args: string ax_tree_id, int node_id, int offset, bool is_downstream
// Returns: JS object with bindings back to the native AutomationPosition.
void CreateAutomationPosition(
const v8::FunctionCallbackInfo<v8::Value>& args);
//
// Helper functions.
//
......
This diff is collapsed.
// 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.
#ifndef EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_POSITION_H_
#define EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_POSITION_H_
#include "gin/wrappable.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_tree_id.h"
namespace gin {
class Arguments;
}
namespace extensions {
// A class that wraps an ui::AXPosition to make available in javascript.
class AutomationPosition final : public gin::Wrappable<AutomationPosition> {
public:
AutomationPosition(const ui::AXNode& node, int offset, bool is_upstream);
~AutomationPosition() override;
static gin::WrapperInfo kWrapperInfo;
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
private:
std::string GetTreeID(gin::Arguments* arguments);
int GetAnchorID(gin::Arguments* arguments);
int GetChildIndex(gin::Arguments* arguments);
int GetTextOffset(gin::Arguments* arguments);
std::string GetAffinity(gin::Arguments* arguments);
bool IsNullPosition(gin::Arguments* arguments);
bool IsTreePosition(gin::Arguments* arguments);
bool IsTextPosition(gin::Arguments* arguments);
bool IsLeafTextPosition(gin::Arguments* arguments);
bool AtStartOfAnchor(gin::Arguments* arguments);
bool AtEndOfAnchor(gin::Arguments* arguments);
bool AtStartOfWord(gin::Arguments* arguments);
bool AtEndOfWord(gin::Arguments* arguments);
bool AtStartOfLine(gin::Arguments* arguments);
bool AtEndOfLine(gin::Arguments* arguments);
bool AtStartOfParagraph(gin::Arguments* arguments);
bool AtEndOfParagraph(gin::Arguments* arguments);
bool AtStartOfPage(gin::Arguments* arguments);
bool AtEndOfPage(gin::Arguments* arguments);
bool AtStartOfFormat(gin::Arguments* arguments);
bool AtEndOfFormat(gin::Arguments* arguments);
bool AtStartOfDocument(gin::Arguments* arguments);
bool AtEndOfDocument(gin::Arguments* arguments);
void AsTreePosition(gin::Arguments* arguments);
void AsTextPosition(gin::Arguments* arguments);
void AsLeafTextPosition(gin::Arguments* arguments);
void MoveToPositionAtStartOfAnchor(gin::Arguments* arguments);
void MoveToPositionAtEndOfAnchor(gin::Arguments* arguments);
void MoveToPositionAtStartOfDocument(gin::Arguments* arguments);
void MoveToPositionAtEndOfDocument(gin::Arguments* arguments);
void MoveToParentPosition(gin::Arguments* arguments);
void MoveToNextLeafTreePosition(gin::Arguments* arguments);
void MoveToPreviousLeafTreePosition(gin::Arguments* arguments);
void MoveToNextLeafTextPosition(gin::Arguments* arguments);
void MoveToPreviousLeafTextPosition(gin::Arguments* arguments);
void MoveToNextCharacterPosition(gin::Arguments* arguments);
void MoveToPreviousCharacterPosition(gin::Arguments* arguments);
void MoveToNextWordStartPosition(gin::Arguments* arguments);
void MoveToPreviousWordStartPosition(gin::Arguments* arguments);
void MoveToNextWordEndPosition(gin::Arguments* arguments);
void MoveToPreviousWordEndPosition(gin::Arguments* arguments);
void MoveToNextLineStartPosition(gin::Arguments* arguments);
void MoveToPreviousLineStartPosition(gin::Arguments* arguments);
void MoveToNextLineEndPosition(gin::Arguments* arguments);
void MoveToPreviousLineEndPosition(gin::Arguments* arguments);
void MoveToPreviousFormatStartPosition(gin::Arguments* arguments);
void MoveToNextFormatEndPosition(gin::Arguments* arguments);
void MoveToNextParagraphStartPosition(gin::Arguments* arguments);
void MoveToPreviousParagraphStartPosition(gin::Arguments* arguments);
void MoveToNextParagraphEndPosition(gin::Arguments* arguments);
void MoveToPreviousParagraphEndPosition(gin::Arguments* arguments);
void MoveToNextPageStartPosition(gin::Arguments* arguments);
void MoveToPreviousPageStartPosition(gin::Arguments* arguments);
void MoveToNextPageEndPosition(gin::Arguments* arguments);
void MoveToPreviousPageEndPosition(gin::Arguments* arguments);
void MoveToNextAnchorPosition(gin::Arguments* arguments);
void MoveToPreviousAnchorPosition(gin::Arguments* arguments);
int MaxTextOffset(gin::Arguments* arguments);
bool IsInLineBreak(gin::Arguments* arguments);
bool IsInTextObject(gin::Arguments* arguments);
bool IsInWhiteSpace(gin::Arguments* arguments);
bool IsValid(gin::Arguments* arguments);
base::string16 GetText(gin::Arguments* arguments);
ui::AXNodePosition::AXPositionInstance position_;
DISALLOW_COPY_AND_ASSIGN(AutomationPosition);
};
} // namespace extensions
#endif // EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_POSITION_H_
......@@ -749,6 +749,7 @@ std::vector<Dispatcher::JsResourceInfo> Dispatcher::GetJsResources() {
{"automation", IDR_AUTOMATION_CUSTOM_BINDINGS_JS},
{"automationEvent", IDR_AUTOMATION_EVENT_JS},
{"automationNode", IDR_AUTOMATION_NODE_JS},
{"automationTreeCache", IDR_AUTOMATION_TREE_CACHE_JS},
{"app.runtime", IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS},
{"app.window", IDR_APP_WINDOW_CUSTOM_BINDINGS_JS},
{"declarativeWebRequest", IDR_DECLARATIVE_WEBREQUEST_CUSTOM_BINDINGS_JS},
......
......@@ -4,6 +4,7 @@
var AutomationEvent = require('automationEvent').AutomationEvent;
var automationInternal = getInternalApi('automationInternal');
var AutomationTreeCache = require('automationTreeCache').AutomationTreeCache;
var exceptionHandler = require('uncaught_exception_handler');
var natives = requireNative('automationInternal');
......@@ -522,6 +523,15 @@ var EventListenerRemoved = natives.EventListenerRemoved;
*/
var GetMarkers = natives.GetMarkers;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {number} offset
* @param {boolean} isUpstream
* @return {!Object}
*/
var createAutomationPosition = natives.CreateAutomationPosition;
var logging = requireNative('logging');
var utils = require('utils');
......@@ -789,6 +799,25 @@ AutomationNodeImpl.prototype = {
return GetMarkers(this.treeID, this.id);
},
createPosition: function(offset, opt_isUpstream) {
var nativePosition = createAutomationPosition(
this.treeID, this.id, offset, !!opt_isUpstream);
// Attach a getter for the node, which is only available in js.
Object.defineProperty(nativePosition, 'node', {
get: function() {
var tree =
AutomationTreeCache.idToAutomationRootNode[nativePosition.treeID];
if (!tree)
return null;
return privates(tree).impl.get(nativePosition.anchorID);
}
});
return nativePosition;
},
doDefault: function() {
this.performAction_('doDefault');
},
......@@ -1440,19 +1469,16 @@ function AutomationRootNodeImpl(treeID) {
this.axNodeDataCache_ = {__proto__: null};
}
utils.defineProperty(AutomationRootNodeImpl, 'idToAutomationRootNode_',
{__proto__: null});
utils.defineProperty(AutomationRootNodeImpl, 'get', function(treeID) {
var result = AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
var result = AutomationTreeCache.idToAutomationRootNode[treeID];
return result || undefined;
});
utils.defineProperty(AutomationRootNodeImpl, 'getOrCreate', function(treeID) {
if (AutomationRootNodeImpl.idToAutomationRootNode_[treeID])
return AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
if (AutomationTreeCache.idToAutomationRootNode[treeID])
return AutomationTreeCache.idToAutomationRootNode[treeID];
var result = new AutomationRootNode(treeID);
AutomationRootNodeImpl.idToAutomationRootNode_[treeID] = result;
AutomationTreeCache.idToAutomationRootNode[treeID] = result;
return result;
});
......@@ -1467,7 +1493,7 @@ utils.defineProperty(
});
utils.defineProperty(AutomationRootNodeImpl, 'destroy', function(treeID) {
delete AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
delete AutomationTreeCache.idToAutomationRootNode[treeID];
});
/**
......@@ -1731,6 +1757,7 @@ function AutomationNode() {
}
utils.expose(AutomationNode, AutomationNodeImpl, {
functions: [
'createPosition',
'doDefault',
'find',
'findAll',
......
// 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.
var utils = require('utils');
function AutomationTreeCacheImpl() {}
function AutomationTreeCache() {
privates(AutomationTreeCache).constructPrivate(this, arguments);
}
utils.defineProperty(
AutomationTreeCache, 'idToAutomationRootNode', {__proto__: null});
exports.$set('AutomationTreeCache', AutomationTreeCache);
......@@ -51,6 +51,7 @@
<include name="IDR_AUTOMATION_CUSTOM_BINDINGS_JS" file="automation\automation_custom_bindings.js" type="BINDATA" />
<include name="IDR_AUTOMATION_EVENT_JS" file="automation\automation_event.js" type="BINDATA" />
<include name="IDR_AUTOMATION_NODE_JS" file="automation\automation_node.js" type="BINDATA" />
<include name="IDR_AUTOMATION_TREE_CACHE_JS" file="automation\automation_tree_cache.js" type="BINDATA" />
<include name="IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS" file="app_runtime_custom_bindings.js" type="BINDATA" />
<include name="IDR_APP_WINDOW_CUSTOM_BINDINGS_JS" file="app_window_custom_bindings.js" type="BINDATA" />
<include name="IDR_CONTEXT_MENUS_CUSTOM_BINDINGS_JS" file="context_menus_custom_bindings.js" type="BINDATA" />
......
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