Commit 787fcfd3 authored by dmazzoni's avatar dmazzoni Committed by Commit bot

Make ChromeVox cursor robust to deleted nodes

If a node becomes invalid, the cursor snaps to the nearest valid ancestor.
Fixing this uncovered a lot of places where we were assuming that a cursor's
node must be valid, but it's possible that even with this new check that
a cursor could still be invalid, so I added checks in a lot of places.

BUG=613694
CQ_INCLUDE_TRYBOTS=tryserver.chromium.linux:closure_compilation

Review-Url: https://codereview.chromium.org/2007183002
Cr-Commit-Position: refs/heads/master@{#397860}
parent 63d6ee6f
...@@ -769,22 +769,25 @@ Background.prototype = { ...@@ -769,22 +769,25 @@ Background.prototype = {
} }
if (pred) { if (pred) {
var node = AutomationUtil.findNextNode( var bound = current.getBound(dir).node;
current.getBound(dir).node, dir, pred, {skipInitialAncestry: true}); if (bound) {
var node = AutomationUtil.findNextNode(
if (node) { bound, dir, pred, {skipInitialAncestry: true});
node = AutomationUtil.findNodePre(
node, dir, AutomationPredicate.object) || node; if (node) {
} node = AutomationUtil.findNodePre(
node, dir, AutomationPredicate.object) || node;
}
if (node) { if (node) {
current = cursors.Range.fromNode(node); current = cursors.Range.fromNode(node);
} else { } else {
if (predErrorMsg) { if (predErrorMsg) {
cvox.ChromeVox.tts.speak(Msgs.getMsg(predErrorMsg), cvox.ChromeVox.tts.speak(Msgs.getMsg(predErrorMsg),
cvox.QueueMode.FLUSH); cvox.QueueMode.FLUSH);
}
return false;
} }
return false;
} }
} }
......
...@@ -24,6 +24,7 @@ BackgroundTest.prototype = { ...@@ -24,6 +24,7 @@ BackgroundTest.prototype = {
setUp: function() { setUp: function() {
global.backgroundObj.forceChromeVoxNextActive(); global.backgroundObj.forceChromeVoxNextActive();
window.RoleType = chrome.automation.RoleType; window.RoleType = chrome.automation.RoleType;
window.doCmd = this.doCmd;
}, },
/** /**
...@@ -74,6 +75,33 @@ BackgroundTest.prototype = { ...@@ -74,6 +75,33 @@ BackgroundTest.prototype = {
<iframe srcdoc="<button>Inside</button><h1>Inside</h1>"></iframe> <iframe srcdoc="<button>Inside</button><h1>Inside</h1>"></iframe>
<button>After</button> <button>After</button>
*/}, */},
disappearingObjectDoc: function() {/*!
<p>start</p>
<article>
<p>Before1</p>
<p>Before2</p>
<p>Before3</p>
</article>
<article>
<p id="disappearing">Disappearing</p>
</article>
<article>
<p>After1</p>
<p>After2</p>
<p>After3</p>
</article>
</div>
<div id="live" aria-live="polite"></div>
<div id="delete" role="button">Delete</div>
<script>
document.getElementById('delete').addEventListener('click', function() {
var d = document.getElementById('disappearing');
d.parentElement.removeChild(d);
document.getElementById('live').innerText = 'Deleted';
});
</script>
*/},
}; };
/** Tests that ChromeVox classic is in this context. */ /** Tests that ChromeVox classic is in this context. */
...@@ -91,8 +119,6 @@ SYNC_TEST_F('BackgroundTest', 'NextNamespaces', function() { ...@@ -91,8 +119,6 @@ SYNC_TEST_F('BackgroundTest', 'NextNamespaces', function() {
TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() { TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
var mockFeedback = this.createMockFeedback(); var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
var doCmd = this.doCmd.bind(this);
mockFeedback.expectSpeech('start').expectBraille('start'); mockFeedback.expectSpeech('start').expectBraille('start');
mockFeedback.call(doCmd('nextLink')) mockFeedback.call(doCmd('nextLink'))
...@@ -151,8 +177,6 @@ TEST_F('BackgroundTest', 'CaretNavigation', function() { ...@@ -151,8 +177,6 @@ TEST_F('BackgroundTest', 'CaretNavigation', function() {
// TODO(plundblad): Add braille expectaions when crbug.com/523285 is fixed. // TODO(plundblad): Add braille expectaions when crbug.com/523285 is fixed.
var mockFeedback = this.createMockFeedback(); var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
var doCmd = this.doCmd.bind(this);
mockFeedback.expectSpeech('start'); mockFeedback.expectSpeech('start');
mockFeedback.call(doCmd('nextCharacter')) mockFeedback.call(doCmd('nextCharacter'))
.expectSpeech('t'); .expectSpeech('t');
...@@ -213,7 +237,7 @@ TEST_F('BackgroundTest', 'ContinuousRead', function() { ...@@ -213,7 +237,7 @@ TEST_F('BackgroundTest', 'ContinuousRead', function() {
var mockFeedback = this.createMockFeedback(); var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
mockFeedback.expectSpeech('start') mockFeedback.expectSpeech('start')
.call(this.doCmd('readFromHere')) .call(doCmd('readFromHere'))
.expectSpeech( .expectSpeech(
'start', 'start',
'alpha', 'Link', 'alpha', 'Link',
...@@ -239,7 +263,7 @@ TEST_F('BackgroundTest', 'AriaLabel', function() { ...@@ -239,7 +263,7 @@ TEST_F('BackgroundTest', 'AriaLabel', function() {
var mockFeedback = this.createMockFeedback(); var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree('<a aria-label="foo" href="a">a</a>', this.runWithLoadedTree('<a aria-label="foo" href="a">a</a>',
function(rootNode) { function(rootNode) {
rootNode.find({role: 'link'}).focus(); rootNode.find({role: RoleType.link}).focus();
mockFeedback.expectSpeech('foo') mockFeedback.expectSpeech('foo')
.expectSpeech('Link') .expectSpeech('Link')
.expectBraille('foo lnk'); .expectBraille('foo lnk');
...@@ -263,7 +287,7 @@ TEST_F('BackgroundTest', 'ShowContextMenu', function() { ...@@ -263,7 +287,7 @@ TEST_F('BackgroundTest', 'ShowContextMenu', function() {
var go = rootNode.find({ role: RoleType.link }); var go = rootNode.find({ role: RoleType.link });
this.listenOnce(go, 'focus', function(e) { this.listenOnce(go, 'focus', function(e) {
this.doCmd('contextMenu')(); doCmd('contextMenu')();
}.bind(this), true); }.bind(this), true);
go.focus(); go.focus();
}.bind(this)); }.bind(this));
...@@ -293,7 +317,7 @@ TEST_F('BackgroundTest', 'BrailleRouting', function() { ...@@ -293,7 +317,7 @@ TEST_F('BackgroundTest', 'BrailleRouting', function() {
*/}, */},
function(rootNode) { function(rootNode) {
var button1 = rootNode.find({role: RoleType.button, var button1 = rootNode.find({role: RoleType.button,
name: 'Click me'}); attributes: { name: 'Click me' }});
var textField = rootNode.find( var textField = rootNode.find(
{role: RoleType.textField}); {role: RoleType.textField});
mockFeedback.expectBraille('start') mockFeedback.expectBraille('start')
...@@ -379,8 +403,6 @@ TEST_F('BackgroundTest', 'EarconsForControls', function() { ...@@ -379,8 +403,6 @@ TEST_F('BackgroundTest', 'EarconsForControls', function() {
<input type=range value=5> <input type=range value=5>
*/}, */},
function(rootNode) { function(rootNode) {
var doCmd = this.doCmd.bind(this);
mockFeedback.call(doCmd('nextObject')) mockFeedback.call(doCmd('nextObject'))
.expectSpeech('MyLink') .expectSpeech('MyLink')
.expectEarcon(cvox.Earcon.LINK) .expectEarcon(cvox.Earcon.LINK)
...@@ -450,7 +472,9 @@ TEST_F('BackgroundTest', 'ActiveOrInactive', function() { ...@@ -450,7 +472,9 @@ TEST_F('BackgroundTest', 'ActiveOrInactive', function() {
<input type="text"></input> <input type="text"></input>
*/}, */},
function(rootNode) { function(rootNode) {
var focusButton = function() { rootNode.find({role: 'button'}).focus(); }; var focusButton = function() {
rootNode.find({role: RoleType.button}).focus();
};
var on = function() { cvox.ChromeVox.isActive = true; }; var on = function() { cvox.ChromeVox.isActive = true; };
var off = function() { cvox.ChromeVox.isActive = false; }; var off = function() { cvox.ChromeVox.isActive = false; };
...@@ -467,8 +491,10 @@ TEST_F('BackgroundTest', 'ActiveOrInactive', function() { ...@@ -467,8 +491,10 @@ TEST_F('BackgroundTest', 'ActiveOrInactive', function() {
mockFeedback.call(focusButton) mockFeedback.call(focusButton)
.expectSpeech('b').expectSpeech('Button') .expectSpeech('b').expectSpeech('Button')
.call(off) .call(off)
.call(focusThen.bind(this, rootNode.find({ role: 'link' }), on)) .call(focusThen.bind(this, rootNode.find(
.call(focusThen.bind(this, rootNode.find({ role: 'textField' }))) { role: RoleType.link }), on))
.call(focusThen.bind(this, rootNode.find(
{ role: RoleType.textField })))
.expectNextSpeechUtteranceIsNot('a') .expectNextSpeechUtteranceIsNot('a')
.expectSpeech('Edit text'); .expectSpeech('Edit text');
...@@ -541,8 +567,8 @@ TEST_F('BackgroundTest', 'FocusIframe', function() { ...@@ -541,8 +567,8 @@ TEST_F('BackgroundTest', 'FocusIframe', function() {
<iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe> <iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe>
<button>outside</button> <button>outside</button>
*/}, function(root) { */}, function(root) {
var iframe = root.find({role: 'iframe'}); var iframe = root.find({role: RoleType.iframe});
var button = root.find({role: 'button'}); var button = root.find({role: RoleType.button});
assertEquals('iframe', iframe.role); assertEquals('iframe', iframe.role);
assertEquals('button', button.role); assertEquals('button', button.role);
...@@ -572,8 +598,8 @@ TEST_F('BackgroundTest', 'NoisySlider', function() { ...@@ -572,8 +598,8 @@ TEST_F('BackgroundTest', 'NoisySlider', function() {
update(); update();
</script> </script>
*/}, function(root) { */}, function(root) {
var go = root.find({role: 'button'}); var go = root.find({role: RoleType.button});
var slider = root.find({role: 'slider'}); var slider = root.find({role: RoleType.slider});
var focusButton = go.focus.bind(go); var focusButton = go.focus.bind(go);
var focusSlider = slider.focus.bind(slider); var focusSlider = slider.focus.bind(slider);
mockFeedback.call(focusButton) mockFeedback.call(focusButton)
...@@ -601,7 +627,7 @@ TEST_F('BackgroundTest', 'Checkbox', function() { ...@@ -601,7 +627,7 @@ TEST_F('BackgroundTest', 'Checkbox', function() {
}); });
</script> </script>
*/}, function(root) { */}, function(root) {
var cbx = root.find({role: 'checkBox'}); var cbx = root.find({role: RoleType.checkBox});
var click = cbx.doDefault.bind(cbx); var click = cbx.doDefault.bind(cbx);
mockFeedback.call(click) mockFeedback.call(click)
.expectSpeech('go') .expectSpeech('go')
...@@ -625,14 +651,12 @@ TEST_F('BackgroundTest', 'ForwardNavigationThroughIframeButtons', function() { ...@@ -625,14 +651,12 @@ TEST_F('BackgroundTest', 'ForwardNavigationThroughIframeButtons', function() {
return; return;
// Return if the iframe hasn't loaded yet. // Return if the iframe hasn't loaded yet.
var iframe = rootNode.find({role: 'iframe'}); var iframe = rootNode.find({role: RoleType.iframe});
var childDoc = iframe.firstChild; var childDoc = iframe.firstChild;
if (!childDoc || childDoc.children.length == 0) if (!childDoc || childDoc.children.length == 0)
return; return;
running = true; running = true;
var doCmd = this.doCmd.bind(this);
var beforeButton = rootNode.find({role: RoleType.button, var beforeButton = rootNode.find({role: RoleType.button,
name: 'Before'}); name: 'Before'});
beforeButton.focus(); beforeButton.focus();
...@@ -675,8 +699,6 @@ TEST_F('BackgroundTest', 'ForwardObjectNavigationThroughIframes', function() { ...@@ -675,8 +699,6 @@ TEST_F('BackgroundTest', 'ForwardObjectNavigationThroughIframes', function() {
return; return;
running = true; running = true;
var doCmd = this.doCmd.bind(this);
var beforeButton = rootNode.find({role: RoleType.button, var beforeButton = rootNode.find({role: RoleType.button,
name: 'Before'}); name: 'Before'});
beforeButton.focus(); beforeButton.focus();
...@@ -716,7 +738,7 @@ TEST_F('BackgroundTest', 'SelectOptionSelected', function() { ...@@ -716,7 +738,7 @@ TEST_F('BackgroundTest', 'SelectOptionSelected', function() {
<option>grapefruit <option>grapefruit
</select> </select>
*/}, function(root) { */}, function(root) {
var select = root.find({role: 'popUpButton'}); var select = root.find({role: RoleType.popUpButton});
var clickSelect = select.doDefault.bind(select); var clickSelect = select.doDefault.bind(select);
var lastOption = select.lastChild.lastChild; var lastOption = select.lastChild.lastChild;
var selectLastOption = lastOption.doDefault.bind(lastOption); var selectLastOption = lastOption.doDefault.bind(lastOption);
...@@ -764,8 +786,8 @@ TEST_F('BackgroundTest', 'EditText', function() { ...@@ -764,8 +786,8 @@ TEST_F('BackgroundTest', 'EditText', function() {
<input type="text"></input> <input type="text"></input>
<input role="combobox" type="text"></input> <input role="combobox" type="text"></input>
*/}, function(root) { */}, function(root) {
var nextEditText = this.doCmd('nextEditText'); var nextEditText = doCmd('nextEditText');
var previousEditText = this.doCmd('previousEditText'); var previousEditText = doCmd('previousEditText');
mockFeedback.call(nextEditText) mockFeedback.call(nextEditText)
.expectSpeech('Combo box') .expectSpeech('Combo box')
.call(previousEditText) .call(previousEditText)
...@@ -803,3 +825,35 @@ TEST_F('BackgroundTest', 'BackwardForwardSync', function() { ...@@ -803,3 +825,35 @@ TEST_F('BackgroundTest', 'BackwardForwardSync', function() {
.replay(); .replay();
}); });
}); });
/** Tests that navigation works when the current object disappears. */
TEST_F('BackgroundTest', 'DisappearingObject', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.disappearingObjectDoc, function(rootNode) {
var deleteButton = rootNode.find({role: RoleType.button,
attributes: { name: 'Delete' }});
var pressDelete = deleteButton.doDefault.bind(deleteButton);
mockFeedback.expectSpeech('start').expectBraille('start');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('Before1')
.call(doCmd('nextObject'))
.expectSpeech('Before2')
.call(doCmd('nextObject'))
.expectSpeech('Before3')
.call(doCmd('nextObject'))
.expectSpeech('Disappearing')
.call(pressDelete)
.expectSpeech('Deleted')
.call(doCmd('nextObject'))
.expectSpeech('After1')
.call(doCmd('nextObject'))
.expectSpeech('After2')
.call(doCmd('previousObject'))
.expectSpeech('After1')
.call(doCmd('previousObject'))
.expectSpeech('Before3');
mockFeedback.replay();
});
});
...@@ -12,6 +12,7 @@ goog.provide('cursors.Movement'); ...@@ -12,6 +12,7 @@ goog.provide('cursors.Movement');
goog.provide('cursors.Range'); goog.provide('cursors.Range');
goog.provide('cursors.Unit'); goog.provide('cursors.Unit');
goog.require('AutomationPredicate');
goog.require('AutomationUtil'); goog.require('AutomationUtil');
goog.require('StringUtil'); goog.require('StringUtil');
goog.require('constants'); goog.require('constants');
...@@ -71,10 +72,17 @@ var Unit = cursors.Unit; ...@@ -71,10 +72,17 @@ var Unit = cursors.Unit;
* is pointed to and covers the case where the accessible text is empty. * is pointed to and covers the case where the accessible text is empty.
*/ */
cursors.Cursor = function(node, index) { cursors.Cursor = function(node, index) {
/** @type {!AutomationNode} @private */
this.node_ = node;
/** @type {number} @private */ /** @type {number} @private */
this.index_ = index; this.index_ = index;
/** @type {Array<AutomationNode>} @private */
this.ancestry_ = [];
var nodeWalker = node;
while (nodeWalker) {
this.ancestry_.push(nodeWalker);
nodeWalker = nodeWalker.parent;
if (nodeWalker && AutomationPredicate.root(nodeWalker))
break;
}
}; };
/** /**
...@@ -93,15 +101,26 @@ cursors.Cursor.prototype = { ...@@ -93,15 +101,26 @@ cursors.Cursor.prototype = {
* @return {boolean} * @return {boolean}
*/ */
equals: function(rhs) { equals: function(rhs) {
return this.node_ === rhs.node && return this.node === rhs.node &&
this.index_ === rhs.index; this.index === rhs.index;
}, },
/** /**
* @return {!AutomationNode} * Returns the node. If the node is invalid since the last time it
* was accessed, moves the cursor to the nearest valid ancestor first.
* @return {AutomationNode}
*/ */
get node() { get node() {
return this.node_; for (var i = 0; i < this.ancestry_.length; i++) {
var firstValidNode = this.ancestry_[i];
if (firstValidNode != null && firstValidNode.role !== undefined &&
firstValidNode.root !== undefined) {
return firstValidNode;
}
// If we have to walk up to an ancestor, reset the index to NODE_INDEX.
this.index_ = cursors.NODE_INDEX;
}
return null;
}, },
/** /**
...@@ -141,7 +160,7 @@ cursors.Cursor.prototype = { ...@@ -141,7 +160,7 @@ cursors.Cursor.prototype = {
* @return {string} * @return {string}
*/ */
getText: function(opt_node) { getText: function(opt_node) {
var node = opt_node || this.node_; var node = opt_node || this.node;
if (node.role === RoleType.textField) if (node.role === RoleType.textField)
return node.value; return node.value;
return node.name || ''; return node.name || '';
...@@ -156,7 +175,11 @@ cursors.Cursor.prototype = { ...@@ -156,7 +175,11 @@ cursors.Cursor.prototype = {
* @return {!cursors.Cursor} The moved cursor. * @return {!cursors.Cursor} The moved cursor.
*/ */
move: function(unit, movement, dir) { move: function(unit, movement, dir) {
var newNode = this.node_; var originalNode = this.node;
if (!originalNode)
return this;
var newNode = originalNode;
var newIndex = this.index_; var newIndex = this.index_;
if ((unit != Unit.NODE || unit != Unit.DOM_NODE) && if ((unit != Unit.NODE || unit != Unit.DOM_NODE) &&
...@@ -257,7 +280,7 @@ cursors.Cursor.prototype = { ...@@ -257,7 +280,7 @@ cursors.Cursor.prototype = {
var pred = unit == Unit.NODE ? var pred = unit == Unit.NODE ?
AutomationPredicate.leaf : AutomationPredicate.object; AutomationPredicate.leaf : AutomationPredicate.object;
newNode = AutomationUtil.findNextNode( newNode = AutomationUtil.findNextNode(
newNode, dir, pred) || this.node_; newNode, dir, pred) || originalNode;
newIndex = cursors.NODE_INDEX; newIndex = cursors.NODE_INDEX;
break; break;
} }
...@@ -268,7 +291,7 @@ cursors.Cursor.prototype = { ...@@ -268,7 +291,7 @@ cursors.Cursor.prototype = {
case Movement.BOUND: case Movement.BOUND:
newNode = AutomationUtil.findNodeUntil(newNode, dir, newNode = AutomationUtil.findNodeUntil(newNode, dir,
AutomationPredicate.linebreak, true); AutomationPredicate.linebreak, true);
newNode = newNode || this.node_; newNode = newNode || originalNode;
newIndex = newIndex =
dir == Dir.FORWARD ? this.getText(newNode).length : 0; dir == Dir.FORWARD ? this.getText(newNode).length : 0;
break; break;
...@@ -281,7 +304,7 @@ cursors.Cursor.prototype = { ...@@ -281,7 +304,7 @@ cursors.Cursor.prototype = {
default: default:
throw Error('Unrecognized unit: ' + unit); throw Error('Unrecognized unit: ' + unit);
} }
newNode = newNode || this.node_; newNode = newNode || originalNode;
newIndex = goog.isDef(newIndex) ? newIndex : this.index_; newIndex = goog.isDef(newIndex) ? newIndex : this.index_;
return new cursors.Cursor(newNode, newIndex); return new cursors.Cursor(newNode, newIndex);
}, },
...@@ -291,7 +314,7 @@ cursors.Cursor.prototype = { ...@@ -291,7 +314,7 @@ cursors.Cursor.prototype = {
* @return {boolean} * @return {boolean}
*/ */
isValid: function() { isValid: function() {
return !!this.node.root; return !!this.node && !!this.node.root;
} }
}; };
...@@ -324,6 +347,8 @@ cursors.WrappingCursor.prototype = { ...@@ -324,6 +347,8 @@ cursors.WrappingCursor.prototype = {
/** @override */ /** @override */
move: function(unit, movement, dir) { move: function(unit, movement, dir) {
var result = this; var result = this;
if (!result.node)
return this;
// Regular movement. // Regular movement.
if (!AutomationPredicate.root(this.node) || dir == Dir.FORWARD) if (!AutomationPredicate.root(this.node) || dir == Dir.FORWARD)
...@@ -340,6 +365,8 @@ cursors.WrappingCursor.prototype = { ...@@ -340,6 +365,8 @@ cursors.WrappingCursor.prototype = {
var pred = unit == Unit.DOM_NODE ? var pred = unit == Unit.DOM_NODE ?
AutomationPredicate.object : AutomationPredicate.leaf; AutomationPredicate.object : AutomationPredicate.leaf;
var endpoint = this.node; var endpoint = this.node;
if (!endpoint)
return this;
// Case 1: forwards (find the root-like node). // Case 1: forwards (find the root-like node).
while (!AutomationPredicate.root(endpoint) && endpoint.parent) while (!AutomationPredicate.root(endpoint) && endpoint.parent)
...@@ -349,7 +376,7 @@ cursors.WrappingCursor.prototype = { ...@@ -349,7 +376,7 @@ cursors.WrappingCursor.prototype = {
var playEarcon = dir == Dir.FORWARD; var playEarcon = dir == Dir.FORWARD;
// Case 2: backward (sync downwards to a leaf), if already on the root. // Case 2: backward (sync downwards to a leaf), if already on the root.
if (dir == Dir.BACKWARD && endpoint == this.node_) { if (dir == Dir.BACKWARD && endpoint == this.node) {
playEarcon = true; playEarcon = true;
endpoint = AutomationUtil.findNodePre(endpoint, endpoint = AutomationUtil.findNodePre(endpoint,
dir, dir,
...@@ -403,6 +430,10 @@ cursors.Range.getDirection = function(rangeA, rangeB) { ...@@ -403,6 +430,10 @@ cursors.Range.getDirection = function(rangeA, rangeB) {
if (!rangeA || !rangeB) if (!rangeA || !rangeB)
return Dir.FORWARD; return Dir.FORWARD;
if (!rangeA.start.node || !rangeA.end.node ||
!rangeB.start.node || !rangeB.end.node)
return Dir.FORWARD;
// They are the same range. // They are the same range.
if (rangeA.start.node === rangeB.start.node && if (rangeA.start.node === rangeB.start.node &&
rangeB.end.node === rangeA.end.node) rangeB.end.node === rangeA.end.node)
...@@ -482,6 +513,9 @@ cursors.Range.prototype = { ...@@ -482,6 +513,9 @@ cursors.Range.prototype = {
*/ */
move: function(unit, dir) { move: function(unit, dir) {
var newStart = this.start_; var newStart = this.start_;
if (!newStart.node)
return this;
var newEnd; var newEnd;
switch (unit) { switch (unit) {
case Unit.CHARACTER: case Unit.CHARACTER:
...@@ -515,6 +549,9 @@ cursors.Range.prototype = { ...@@ -515,6 +549,9 @@ cursors.Range.prototype = {
var start = this.start.node; var start = this.start.node;
var end = this.end.node; var end = this.end.node;
if (!start || !end)
return;
// Find the most common root. // Find the most common root.
var uniqueAncestors = AutomationUtil.getUniqueAncestors(start, end); var uniqueAncestors = AutomationUtil.getUniqueAncestors(start, end);
var mcr = start.root; var mcr = start.root;
......
...@@ -1254,6 +1254,9 @@ Output.prototype = { ...@@ -1254,6 +1254,9 @@ Output.prototype = {
* @private * @private
*/ */
range_: function(range, prevRange, type, rangeBuff) { range_: function(range, prevRange, type, rangeBuff) {
if (!range.start.node || !range.end.node)
return;
if (!prevRange) if (!prevRange)
prevRange = cursors.Range.fromNode(range.start.node.root); prevRange = cursors.Range.fromNode(range.start.node.root);
var cursor = cursors.Cursor.fromNode(range.start.node); var cursor = cursors.Cursor.fromNode(range.start.node);
...@@ -1386,6 +1389,9 @@ Output.prototype = { ...@@ -1386,6 +1389,9 @@ Output.prototype = {
var dir = cursors.Range.getDirection(prevRange, range); var dir = cursors.Range.getDirection(prevRange, range);
var node = range.start.node; var node = range.start.node;
var prevNode = prevRange.getBound(dir).node; var prevNode = prevRange.getBound(dir).node;
if (!node || !prevNode)
return;
var options = {annotation: ['name'], isUnique: true}; var options = {annotation: ['name'], isUnique: true};
var startIndex = range.start.index; var startIndex = range.start.index;
var endIndex = range.end.index; var endIndex = range.end.index;
......
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