Commit 70a4ec71 authored by David Tseng's avatar David Tseng Committed by Chromium LUCI CQ

Move ChromeVox's cursors module to accessibility common

ChromeVox's cursors module contains a clean, clear, and simple interface
to work with the ideas of positions (cursors) and ranges over the
automation tree.

It predates and largely motivates the creation of AXPosition/AXRange in
ui/accessibility. Future changes will attempt to rebase these js classes
partly or completely on top of their native counterparts.

R=dmazzoni@chromium.org

Test: existing (moved) tests.
Change-Id: Ie27cd9ae2c2b406c8f2edcc5148dee03d7bb6281
AX-Relnotes: n/a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2622053Reviewed-by: default avatarAnastasia Helfinstein <anastasi@google.com>
Commit-Queue: David Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#842640}
parent abd59b7f
......@@ -26,9 +26,13 @@ chromevox_modules = [
"../common/automation_predicate.js",
"../common/automation_util.js",
"../common/constants.js",
"../common/cursors/cursor.js",
"../common/cursors/range.js",
"../common/cursors/recovery_strategy.js",
"../common/event_generator.js",
"../common/key_code.js",
"../common/instance_checker.js",
"../common/string_util.js",
"../common/tree_walker.js",
"background/annotation/node_identifier.js",
"background/annotation/user_annotation_handler.js",
......@@ -43,7 +47,6 @@ chromevox_modules = [
"background/classic_background.js",
"background/color.js",
"background/command_handler.js",
"background/cursors.js",
"background/custom_automation_event.js",
"background/desktop_automation_handler.js",
"background/download_handler.js",
......@@ -77,7 +80,6 @@ chromevox_modules = [
"background/phonetic_data.js",
"background/prefs.js",
"background/range_automation_handler.js",
"background/recovery_strategy.js",
"background/smart_sticky_mode.js",
"background/tabs_api_handler.js",
"background/user_action_monitor.js",
......@@ -109,7 +111,6 @@ chromevox_modules = [
"common/msgs.js",
"common/nav_description.js",
"common/spannable.js",
"common/string_util.js",
"common/tts_background.js",
"common/tts_base.js",
"common/tts_interface.js",
......@@ -450,7 +451,6 @@ if (is_chromeos_ash) {
"background/background_test.js",
"background/braille_command_data_test.js",
"background/color_test.js",
"background/cursors_test.js",
"background/download_handler_test.js",
"background/editing/editing_test.js",
"background/editing/intent_handler_test.js",
......@@ -460,7 +460,6 @@ if (is_chromeos_ash) {
"background/logging/log_store_test.js",
"background/output_test.js",
"background/portals_test.js",
"background/recovery_strategy_test.js",
"background/settings_test.js",
"background/smart_sticky_mode_test.js",
"background/user_action_monitor_test.js",
......
// 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 test fixture.
GEN_INCLUDE([
'//chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js',
]);
/**
* Test fixture for recovery strategy tests.
*/
ChromeVoxRecoveryStrategyTest = class extends ChromeVoxNextE2ETest {
constructor() {
super();
}
};
TEST_F('ChromeVoxRecoveryStrategyTest', 'ReparentedRecovery', function() {
this.runWithLoadedTree(
`
<input type="text"></input>
<p id="p">hi</p>
<button id="go"</button>
<script>
document.getElementById('go').addEventListener('click', function() {
let p = document.getElementById('p');
p.remove();
document.body.appendChild(p);
});
</script>
`,
function(root) {
const p = root.find({role: RoleType.PARAGRAPH});
const s = root.find({role: RoleType.STATIC_TEXT});
const b = root.find({role: RoleType.BUTTON});
const bAncestryRecovery = new AncestryRecoveryStrategy(b);
const pAncestryRecovery = new AncestryRecoveryStrategy(p);
const sAncestryRecovery = new AncestryRecoveryStrategy(s);
const bTreePathRecovery = new TreePathRecoveryStrategy(b);
const pTreePathRecovery = new TreePathRecoveryStrategy(p);
const sTreePathRecovery = new TreePathRecoveryStrategy(s);
this.listenOnce(b, 'clicked', function() {
assertFalse(
bAncestryRecovery.requiresRecovery(),
'bAncestryRecovery.requiresRecovery');
assertTrue(
pAncestryRecovery.requiresRecovery(),
'pAncestryRecovery.requiresRecovery()');
assertTrue(
sAncestryRecovery.requiresRecovery(),
'sAncestryRecovery.requiresRecovery()');
assertFalse(
bTreePathRecovery.requiresRecovery(),
'bTreePathRecovery.requiresRecovery()');
assertTrue(
pTreePathRecovery.requiresRecovery(),
'pTreePathRecovery.requiresRecovery()');
assertTrue(
sTreePathRecovery.requiresRecovery(),
'sTreePathRecovery.requiresRecovery()');
assertEquals(RoleType.BUTTON, bAncestryRecovery.node.role);
assertEquals(root, pAncestryRecovery.node);
assertEquals(root, sAncestryRecovery.node);
assertEquals(b, bTreePathRecovery.node);
assertEquals(b, pTreePathRecovery.node);
assertEquals(b, sTreePathRecovery.node);
assertFalse(
bAncestryRecovery.requiresRecovery(),
'bAncestryRecovery.requiresRecovery()');
assertFalse(
pAncestryRecovery.requiresRecovery(),
'pAncestryRecovery.requiresRecovery()');
assertFalse(
sAncestryRecovery.requiresRecovery(),
'sAncestryRecovery.requiresRecovery()');
assertFalse(
bTreePathRecovery.requiresRecovery(),
'bTreePathRecovery.requiresRecovery()');
assertFalse(
pTreePathRecovery.requiresRecovery(),
'pTreePathRecovery.requiresRecovery()');
assertFalse(
sTreePathRecovery.requiresRecovery(),
'sTreePathRecovery.requiresRecovery()');
});
// Trigger the change.
b.doDefault();
});
});
......@@ -30,6 +30,9 @@ run_jsbundler("accessibility_common_copied_files") {
"automation_util.js",
"closure_shim.js",
"constants.js",
"cursors/cursor.js",
"cursors/range.js",
"cursors/recovery_strategy.js",
"event_generator.js",
"event_handler.js",
"gdocs_script.js",
......@@ -38,6 +41,7 @@ run_jsbundler("accessibility_common_copied_files") {
"rect_util.js",
"repeated_event_handler.js",
"repeated_tree_change_handler.js",
"string_util.js",
"tree_walker.js",
]
rewrite_rules = [ rebase_path(".", root_build_dir) + ":" ]
......@@ -145,6 +149,8 @@ js2gtest("accessibility_tests") {
sources = [
"array_util_test.js",
"automation_util_test.js",
"cursors/cursors_test.js",
"cursors/recovery_strategy_test.js",
"event_generator_test.js",
"repeated_event_handler_test.js",
"repeated_tree_change_handler_test.js",
......
......@@ -3,13 +3,12 @@
// found in the LICENSE file.
/**
* @fileoverview Classes related to cursors that point to and select parts of
* @fileoverview Structures related to cursors that point to and select parts of
* the automation tree.
*/
goog.provide('cursors.Cursor');
goog.provide('cursors.Movement');
goog.provide('cursors.Range');
goog.provide('cursors.Unit');
goog.require('AncestryRecoveryStrategy');
......@@ -699,274 +698,4 @@ cursors.WrappingCursor = class extends cursors.Cursor {
return new cursors.WrappingCursor(result.node, result.index);
}
};
/**
* Represents a range in the automation tree. There is no visible selection on
* the page caused by usage of this object.
* It is assumed that the caller provides |start| and |end| in document order.
*/
cursors.Range = class {
/**
* @param {!cursors.Cursor} start
* @param {!cursors.Cursor} end
*/
constructor(start, end) {
/** @type {!cursors.Cursor} @private */
this.start_ = start;
/** @type {!cursors.Cursor} @private */
this.end_ = end;
}
/**
* Convenience method to construct a Range surrounding one node.
* @param {!AutomationNode} node
* @return {!cursors.Range}
*/
static fromNode(node) {
const cursor = cursors.WrappingCursor.fromNode(node);
return new cursors.Range(cursor, cursor);
}
/**
* Given |rangeA| and |rangeB| in order, determine which |Dir|
* relates them.
* @param {!cursors.Range} rangeA
* @param {!cursors.Range} rangeB
* @return {Dir}
*/
static getDirection(rangeA, rangeB) {
if (!rangeA || !rangeB) {
return Dir.FORWARD;
}
if (!rangeA.start.node || !rangeA.end.node || !rangeB.start.node ||
!rangeB.end.node) {
return Dir.FORWARD;
}
// They are the same range.
if (rangeA.start.node === rangeB.start.node &&
rangeB.end.node === rangeA.end.node) {
return Dir.FORWARD;
}
const testDirA =
AutomationUtil.getDirection(rangeA.start.node, rangeB.end.node);
const testDirB =
AutomationUtil.getDirection(rangeB.start.node, rangeA.end.node);
// The two ranges are either partly overlapping or non overlapping.
if (testDirA === Dir.FORWARD && testDirB === Dir.BACKWARD) {
return Dir.FORWARD;
} else if (testDirA === Dir.BACKWARD && testDirB === Dir.FORWARD) {
return Dir.BACKWARD;
} else {
return testDirA;
}
}
/**
* Returns true if |rhs| is equal to this range.
* Use this for strict equality between ranges.
* @param {!cursors.Range} rhs
* @return {boolean}
*/
equals(rhs) {
return this.start_.equals(rhs.start) && this.end_.equals(rhs.end);
}
equalsWithoutRecovery(rhs) {
return this.start_.equalsWithoutRecovery(rhs.start) &&
this.end_.equalsWithoutRecovery(rhs.end);
}
/**
* Returns true if |rhs| is equal to this range.
* Use this for loose equality between ranges.
* @param {!cursors.Range} rhs
* @return {boolean}
*/
contentEquals(rhs) {
return this.start_.contentEquals(rhs.start) &&
this.end_.contentEquals(rhs.end);
}
/**
* Gets the directed end cursor of this range.
* @param {Dir} dir Which endpoint cursor to return;
* Dir.FORWARD for end,
* Dir.BACKWARD for start.
* @return {!cursors.Cursor}
*/
getBound(dir) {
return dir === Dir.FORWARD ? this.end_ : this.start_;
}
/**
* @return {!cursors.Cursor}
*/
get start() {
return this.start_;
}
/**
* @return {!cursors.Cursor}
*/
get end() {
return this.end_;
}
/**
* Returns true if this range covers less than a node.
* @return {boolean}
*/
isSubNode() {
const startIndex = this.start.index;
const endIndex = this.end.index;
return this.start.node === this.end.node && startIndex !== -1 &&
endIndex !== -1 && startIndex !== endIndex &&
(startIndex !== 0 || endIndex !== this.start.getText().length);
}
/**
* Returns true if this range covers inline text (i.e. each end points to an
* inlineTextBox).
* @return {boolean?}
*/
isInlineText() {
return this.start.node && this.end.node &&
this.start.node.role === this.end.node.role &&
this.start.node.role === RoleType.INLINE_TEXT_BOX;
}
/**
* Makes a Range which has been moved from this range by the given unit and
* direction.
* @param {cursors.Unit} unit
* @param {Dir} dir
* @return {cursors.Range}
*/
move(unit, dir) {
let newStart = this.start_;
if (!newStart.node) {
return this;
}
let newEnd;
switch (unit) {
case cursors.Unit.CHARACTER:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newEnd = newStart.move(unit, cursors.Movement.DIRECTIONAL, Dir.FORWARD);
// Character crossed a node; collapses to the end of the node.
if (newStart.node !== newEnd.node) {
newEnd = new cursors.Cursor(newStart.node, newStart.index + 1);
}
break;
case cursors.Unit.WORD:
case cursors.Unit.LINE:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newStart = newStart.move(unit, cursors.Movement.BOUND, Dir.BACKWARD);
newEnd = newStart.move(unit, cursors.Movement.BOUND, Dir.FORWARD);
break;
case cursors.Unit.NODE:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newEnd = newStart;
break;
default:
throw Error('Invalid unit: ' + unit);
}
return new cursors.Range(newStart, newEnd);
}
/**
* Select the text contained within this range.
*/
select() {
let start = this.start_, end = this.end_;
if (this.start.compare(this.end) === Dir.BACKWARD) {
start = this.end;
end = this.start;
}
const startNode = start.selectionNode_;
const endNode = end.selectionNode_;
if (!startNode || !endNode) {
return;
}
// Only allow selections within the same web tree.
if (startNode.root && startNode.root.role === RoleType.ROOT_WEB_AREA &&
startNode.root === endNode.root) {
// We want to adjust to select the entire node for node offsets;
// otherwise, use the plain character offset.
const startIndex = start.selectionIndex_;
let endIndex = end.index_ === cursors.NODE_INDEX ?
end.selectionIndex_ + 1 :
end.selectionIndex_;
// Richly editables should always set a caret, but not select. This
// makes it possible to navigate through content editables using
// ChromeVox keys and not hear selections as you go.
if (startNode.state[StateType.RICHLY_EDITABLE] ||
endNode.state[StateType.RICHLY_EDITABLE]) {
endIndex = startIndex;
}
chrome.automation.setDocumentSelection({
anchorObject: startNode,
anchorOffset: startIndex,
focusObject: endNode,
focusOffset: endIndex
});
}
}
/**
* Returns true if this range has either cursor end on web content.
* @return {boolean}
*/
isWebRange() {
return this.isValid() &&
(this.start.node.root.role !== RoleType.DESKTOP ||
this.end.node.root.role !== RoleType.DESKTOP);
}
/**
* Returns whether this range has valid start and end cursors.
* @return {boolean}
*/
isValid() {
return this.start.isValid() && this.end.isValid();
}
/**
* Compares this range with |rhs|.
* @param {cursors.Range} rhs
* @return {Dir|undefined} Dir.BACKWARD if |rhs| comes
* before this range in
* document order. Dir.FORWARD if |rhs| comes after this range.
* Undefined otherwise.
*/
compare(rhs) {
const startDir = this.start.compare(rhs.start);
const endDir = this.end.compare(rhs.end);
if (startDir !== endDir) {
return undefined;
}
return startDir;
}
/**
* Returns an undirected version of this range.
* @return {!cursors.Range}
*/
normalize() {
if (this.start.compare(this.end) === Dir.BACKWARD) {
return new cursors.Range(this.end, this.start);
}
return this;
}
};
}); // goog.scope
// Copyright 2021 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 Structures related to ranges, which are pairs of cursors over
* the automation tree.
*/
goog.provide('cursors.Range');
goog.require('AutomationUtil');
goog.require('constants');
goog.require('cursors.Cursor');
goog.scope(function() {
const AutomationNode = chrome.automation.AutomationNode;
const Dir = constants.Dir;
const RoleType = chrome.automation.RoleType;
const StateType = chrome.automation.StateType;
/**
* Represents a range in the automation tree. There is no visible selection on
* the page caused by usage of this object.
* It is assumed that the caller provides |start| and |end| in document order.
*/
cursors.Range = class {
/**
* @param {!cursors.Cursor} start
* @param {!cursors.Cursor} end
*/
constructor(start, end) {
/** @type {!cursors.Cursor} @private */
this.start_ = start;
/** @type {!cursors.Cursor} @private */
this.end_ = end;
}
/**
* Convenience method to construct a Range surrounding one node.
* @param {!AutomationNode} node
* @return {!cursors.Range}
*/
static fromNode(node) {
const cursor = cursors.WrappingCursor.fromNode(node);
return new cursors.Range(cursor, cursor);
}
/**
* Given |rangeA| and |rangeB| in order, determine which |Dir|
* relates them.
* @param {!cursors.Range} rangeA
* @param {!cursors.Range} rangeB
* @return {Dir}
*/
static getDirection(rangeA, rangeB) {
if (!rangeA || !rangeB) {
return Dir.FORWARD;
}
if (!rangeA.start.node || !rangeA.end.node || !rangeB.start.node ||
!rangeB.end.node) {
return Dir.FORWARD;
}
// They are the same range.
if (rangeA.start.node === rangeB.start.node &&
rangeB.end.node === rangeA.end.node) {
return Dir.FORWARD;
}
const testDirA =
AutomationUtil.getDirection(rangeA.start.node, rangeB.end.node);
const testDirB =
AutomationUtil.getDirection(rangeB.start.node, rangeA.end.node);
// The two ranges are either partly overlapping or non overlapping.
if (testDirA === Dir.FORWARD && testDirB === Dir.BACKWARD) {
return Dir.FORWARD;
} else if (testDirA === Dir.BACKWARD && testDirB === Dir.FORWARD) {
return Dir.BACKWARD;
} else {
return testDirA;
}
}
/**
* Returns true if |rhs| is equal to this range.
* Use this for strict equality between ranges.
* @param {!cursors.Range} rhs
* @return {boolean}
*/
equals(rhs) {
return this.start_.equals(rhs.start) && this.end_.equals(rhs.end);
}
equalsWithoutRecovery(rhs) {
return this.start_.equalsWithoutRecovery(rhs.start) &&
this.end_.equalsWithoutRecovery(rhs.end);
}
/**
* Returns true if |rhs| is equal to this range.
* Use this for loose equality between ranges.
* @param {!cursors.Range} rhs
* @return {boolean}
*/
contentEquals(rhs) {
return this.start_.contentEquals(rhs.start) &&
this.end_.contentEquals(rhs.end);
}
/**
* Gets the directed end cursor of this range.
* @param {Dir} dir Which endpoint cursor to return;
* Dir.FORWARD for end,
* Dir.BACKWARD for start.
* @return {!cursors.Cursor}
*/
getBound(dir) {
return dir === Dir.FORWARD ? this.end_ : this.start_;
}
/**
* @return {!cursors.Cursor}
*/
get start() {
return this.start_;
}
/**
* @return {!cursors.Cursor}
*/
get end() {
return this.end_;
}
/**
* Returns true if this range covers less than a node.
* @return {boolean}
*/
isSubNode() {
const startIndex = this.start.index;
const endIndex = this.end.index;
return this.start.node === this.end.node && startIndex !== -1 &&
endIndex !== -1 && startIndex !== endIndex &&
(startIndex !== 0 || endIndex !== this.start.getText().length);
}
/**
* Returns true if this range covers inline text (i.e. each end points to an
* inlineTextBox).
* @return {boolean?}
*/
isInlineText() {
return this.start.node && this.end.node &&
this.start.node.role === this.end.node.role &&
this.start.node.role === RoleType.INLINE_TEXT_BOX;
}
/**
* Makes a Range which has been moved from this range by the given unit and
* direction.
* @param {cursors.Unit} unit
* @param {Dir} dir
* @return {cursors.Range}
*/
move(unit, dir) {
let newStart = this.start_;
if (!newStart.node) {
return this;
}
let newEnd;
switch (unit) {
case cursors.Unit.CHARACTER:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newEnd = newStart.move(unit, cursors.Movement.DIRECTIONAL, Dir.FORWARD);
// Character crossed a node; collapses to the end of the node.
if (newStart.node !== newEnd.node) {
newEnd = new cursors.Cursor(newStart.node, newStart.index + 1);
}
break;
case cursors.Unit.WORD:
case cursors.Unit.LINE:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newStart = newStart.move(unit, cursors.Movement.BOUND, Dir.BACKWARD);
newEnd = newStart.move(unit, cursors.Movement.BOUND, Dir.FORWARD);
break;
case cursors.Unit.NODE:
newStart = newStart.move(unit, cursors.Movement.DIRECTIONAL, dir);
newEnd = newStart;
break;
default:
throw Error('Invalid unit: ' + unit);
}
return new cursors.Range(newStart, newEnd);
}
/**
* Select the text contained within this range.
*/
select() {
let start = this.start_, end = this.end_;
if (this.start.compare(this.end) === Dir.BACKWARD) {
start = this.end;
end = this.start;
}
const startNode = start.selectionNode_;
const endNode = end.selectionNode_;
if (!startNode || !endNode) {
return;
}
// Only allow selections within the same web tree.
if (startNode.root && startNode.root.role === RoleType.ROOT_WEB_AREA &&
startNode.root === endNode.root) {
// We want to adjust to select the entire node for node offsets;
// otherwise, use the plain character offset.
const startIndex = start.selectionIndex_;
let endIndex = end.index_ === cursors.NODE_INDEX ?
end.selectionIndex_ + 1 :
end.selectionIndex_;
// Richly editables should always set a caret, but not select. This
// makes it possible to navigate through content editables using
// ChromeVox keys and not hear selections as you go.
if (startNode.state[StateType.RICHLY_EDITABLE] ||
endNode.state[StateType.RICHLY_EDITABLE]) {
endIndex = startIndex;
}
chrome.automation.setDocumentSelection({
anchorObject: startNode,
anchorOffset: startIndex,
focusObject: endNode,
focusOffset: endIndex
});
}
}
/**
* Returns true if this range has either cursor end on web content.
* @return {boolean}
*/
isWebRange() {
return this.isValid() &&
(this.start.node.root.role !== RoleType.DESKTOP ||
this.end.node.root.role !== RoleType.DESKTOP);
}
/**
* Returns whether this range has valid start and end cursors.
* @return {boolean}
*/
isValid() {
return this.start.isValid() && this.end.isValid();
}
/**
* Compares this range with |rhs|.
* @param {cursors.Range} rhs
* @return {Dir|undefined} Dir.BACKWARD if |rhs| comes
* before this range in
* document order. Dir.FORWARD if |rhs| comes after this range.
* Undefined otherwise.
*/
compare(rhs) {
const startDir = this.start.compare(rhs.start);
const endDir = this.end.compare(rhs.end);
if (startDir !== endDir) {
return undefined;
}
return startDir;
}
/**
* Returns an undirected version of this range.
* @return {!cursors.Range}
*/
normalize() {
if (this.start.compare(this.end) === Dir.BACKWARD) {
return new cursors.Range(this.end, this.start);
}
return this;
}
};
}); // goog.scope
// 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 test fixture.
GEN_INCLUDE([
'//chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js',
]);
/**
* Test fixture for recovery strategy tests.
*/
AccessibilityExtensionRecoveryStrategyTest =
class extends ChromeVoxNextE2ETest {
constructor() {
super();
}
};
TEST_F(
'AccessibilityExtensionRecoveryStrategyTest', 'ReparentedRecovery',
function() {
this.runWithLoadedTree(
`
<input type="text"></input>
<p id="p">hi</p>
<button id="go"</button>
<script>
document.getElementById('go').addEventListener('click', function() {
let p = document.getElementById('p');
p.remove();
document.body.appendChild(p);
});
</script>
`,
function(root) {
const p = root.find({role: RoleType.PARAGRAPH});
const s = root.find({role: RoleType.STATIC_TEXT});
const b = root.find({role: RoleType.BUTTON});
const bAncestryRecovery = new AncestryRecoveryStrategy(b);
const pAncestryRecovery = new AncestryRecoveryStrategy(p);
const sAncestryRecovery = new AncestryRecoveryStrategy(s);
const bTreePathRecovery = new TreePathRecoveryStrategy(b);
const pTreePathRecovery = new TreePathRecoveryStrategy(p);
const sTreePathRecovery = new TreePathRecoveryStrategy(s);
this.listenOnce(b, 'clicked', function() {
assertFalse(
bAncestryRecovery.requiresRecovery(),
'bAncestryRecovery.requiresRecovery');
assertTrue(
pAncestryRecovery.requiresRecovery(),
'pAncestryRecovery.requiresRecovery()');
assertTrue(
sAncestryRecovery.requiresRecovery(),
'sAncestryRecovery.requiresRecovery()');
assertFalse(
bTreePathRecovery.requiresRecovery(),
'bTreePathRecovery.requiresRecovery()');
assertTrue(
pTreePathRecovery.requiresRecovery(),
'pTreePathRecovery.requiresRecovery()');
assertTrue(
sTreePathRecovery.requiresRecovery(),
'sTreePathRecovery.requiresRecovery()');
assertEquals(RoleType.BUTTON, bAncestryRecovery.node.role);
assertEquals(root, pAncestryRecovery.node);
assertEquals(root, sAncestryRecovery.node);
assertEquals(b, bTreePathRecovery.node);
assertEquals(b, pTreePathRecovery.node);
assertEquals(b, sTreePathRecovery.node);
assertFalse(
bAncestryRecovery.requiresRecovery(),
'bAncestryRecovery.requiresRecovery()');
assertFalse(
pAncestryRecovery.requiresRecovery(),
'pAncestryRecovery.requiresRecovery()');
assertFalse(
sAncestryRecovery.requiresRecovery(),
'sAncestryRecovery.requiresRecovery()');
assertFalse(
bTreePathRecovery.requiresRecovery(),
'bTreePathRecovery.requiresRecovery()');
assertFalse(
pTreePathRecovery.requiresRecovery(),
'pTreePathRecovery.requiresRecovery()');
assertFalse(
sTreePathRecovery.requiresRecovery(),
'sTreePathRecovery.requiresRecovery()');
});
// Trigger the change.
b.doDefault();
});
});
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