// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.editor.rangeTest'); goog.setTestOnly('goog.editor.rangeTest'); goog.require('goog.dom'); goog.require('goog.dom.Range'); goog.require('goog.dom.TagName'); goog.require('goog.editor.range'); goog.require('goog.editor.range.Point'); goog.require('goog.string'); goog.require('goog.testing.dom'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); var savedHtml; var $; function setUpPage() { $ = goog.dom.getElement; } function setUp() { savedHtml = $('root').innerHTML; } function tearDown() { $('root').innerHTML = savedHtml; } function testNoNarrow() { var def = $('def'); var jkl = $('jkl'); var range = goog.dom.Range.createFromNodes(def.firstChild, 1, jkl.firstChild, 2); range = goog.editor.range.narrow(range, $('parentNode')); goog.testing.dom.assertRangeEquals( def.firstChild, 1, jkl.firstChild, 2, range); } function testNarrowAtEndEdge() { var def = $('def'); var jkl = $('jkl'); var range = goog.dom.Range.createFromNodes(def.firstChild, 1, jkl.firstChild, 2); range = goog.editor.range.narrow(range, def); goog.testing.dom.assertRangeEquals( def.firstChild, 1, def.firstChild, 3, range); } function testNarrowAtStartEdge() { var def = $('def'); var jkl = $('jkl'); var range = goog.dom.Range.createFromNodes(def.firstChild, 1, jkl.firstChild, 2); range = goog.editor.range.narrow(range, jkl); goog.testing.dom.assertRangeEquals( jkl.firstChild, 0, jkl.firstChild, 2, range); } function testNarrowOutsideElement() { var def = $('def'); var jkl = $('jkl'); var range = goog.dom.Range.createFromNodes(def.firstChild, 1, jkl.firstChild, 2); range = goog.editor.range.narrow(range, $('pqr')); assertNull(range); } function testNoExpand() { var div = $('parentNode'); div.innerHTML = '
bbb';
var children = div.childNodes;
assertEquals(3, children.length);
var imgNode = children[1];
var range = goog.editor.range.placeCursorNextTo(imgNode, false);
assertEquals(
'range node should be the right sibling of img tag', children[2],
range.getStartNode());
assertEquals(0, range.getStartOffset());
}
function testPlaceCursorNextTo_rightOfImgAtEnd() {
var div = $('parentNode');
div.innerHTML =
'aaa
';
var children = div.childNodes;
assertEquals(2, children.length);
var imgNode = children[1];
var range = goog.editor.range.placeCursorNextTo(imgNode, false);
assertEquals(
'range node should be the parent of img', div, range.getStartNode());
assertEquals(
'offset should be right after the img tag', 2, range.getStartOffset());
}
function testPlaceCursorNextTo_leftOfImg() {
var div = $('parentNode');
div.innerHTML =
'
xxx';
var children = div.childNodes;
assertEquals(2, children.length);
var imgNode = children[0];
var range = goog.editor.range.placeCursorNextTo(imgNode, true);
assertEquals(
'range node should be the parent of img', div, range.getStartNode());
assertEquals('offset should point to the img tag', 0, range.getStartOffset());
}
function testPlaceCursorNextTo_rightOfFirstOfTwoImgTags() {
var div = $('parentNode');
div.innerHTML =
'aaa
' +
'
';
var children = div.childNodes;
assertEquals(3, children.length);
var imgNode = children[1]; // First of two IMG nodes
var range = goog.editor.range.placeCursorNextTo(imgNode, false);
assertEquals(
'range node should be the parent of img instead of ' +
'node with innerHTML=' + range.getStartNode().innerHTML,
div, range.getStartNode());
assertEquals(
'offset should be right after the img tag', 2, range.getStartOffset());
}
function testGetDeepEndPoint() {
var div = $('parentNode');
var def = $('def');
var jkl = $('jkl');
assertPointEquals(
div.firstChild, 0, goog.editor.range.getDeepEndPoint(
goog.dom.Range.createFromNodeContents(div), true));
assertPointEquals(
div.lastChild, div.lastChild.length,
goog.editor.range.getDeepEndPoint(
goog.dom.Range.createFromNodeContents(div), false));
assertPointEquals(
def.firstChild, 0, goog.editor.range.getDeepEndPoint(
goog.dom.Range.createCaret(div, 1), true));
assertPointEquals(
def.nextSibling, 0, goog.editor.range.getDeepEndPoint(
goog.dom.Range.createCaret(div, 2), true));
}
function testNormalizeOnNormalizedDom() {
var defText = $('def').firstChild;
var jklText = $('jkl').firstChild;
var range = goog.dom.Range.createFromNodes(defText, 1, jklText, 2);
var newRange = normalizeBody(range);
goog.testing.dom.assertRangeEquals(defText, 1, jklText, 2, newRange);
}
function testDeepPointFindingOnNormalizedDom() {
var def = $('def');
var jkl = $('jkl');
var range = goog.dom.Range.createFromNodes(def, 0, jkl, 1);
var newRange = normalizeBody(range);
// Make sure that newRange is measured relative to the text nodes,
// not the DIV elements.
goog.testing.dom.assertRangeEquals(
def.firstChild, 0, jkl.firstChild, 3, newRange);
}
function testNormalizeOnVeryFragmentedDom() {
var defText = $('def').firstChild;
var jklText = $('jkl').firstChild;
var range = goog.dom.Range.createFromNodes(defText, 1, jklText, 2);
// Fragment the DOM a bunch.
fragmentText(defText);
fragmentText(jklText);
var newRange = normalizeBody(range);
// our old text nodes may not be valid anymore. find new ones.
defText = $('def').firstChild;
jklText = $('jkl').firstChild;
goog.testing.dom.assertRangeEquals(defText, 1, jklText, 2, newRange);
}
function testNormalizeOnDivWithEmptyTextNodes() {
var emptyDiv = $('normalizeTest-with-empty-text-nodes');
// Append empty text nodes to the emptyDiv.
var tnode1 = goog.dom.createTextNode('');
var tnode2 = goog.dom.createTextNode('');
var tnode3 = goog.dom.createTextNode('');
goog.dom.appendChild(emptyDiv, tnode1);
goog.dom.appendChild(emptyDiv, tnode2);
goog.dom.appendChild(emptyDiv, tnode3);
var range = goog.dom.Range.createFromNodes(emptyDiv, 1, emptyDiv, 2);
// Cannot use document.body.normalize() as it fails to normalize the div
// (in IE) if it has nothing but empty text nodes.
var newRange = goog.editor.range.rangePreservingNormalize(emptyDiv, range);
if (goog.userAgent.GECKO &&
goog.string.compareVersions(goog.userAgent.VERSION, '1.9') == -1) {
// In FF2, node.normalize() leaves an empty textNode in the div, unlike
// other browsers where the div is left with no children.
goog.testing.dom.assertRangeEquals(
emptyDiv.firstChild, 0, emptyDiv.firstChild, 0, newRange);
} else {
goog.testing.dom.assertRangeEquals(emptyDiv, 0, emptyDiv, 0, newRange);
}
}
function testRangeCreatedInVeryFragmentedDom() {
var def = $('def');
var defText = def.firstChild;
var jkl = $('jkl');
var jklText = jkl.firstChild;
// Fragment the DOM a bunch.
fragmentText(defText);
fragmentText(jklText);
// Notice that there are two empty text nodes at the beginning of each
// fragmented node.
var range = goog.dom.Range.createFromNodes(def, 3, jkl, 4);
var newRange = normalizeBody(range);
// our old text nodes may not be valid anymore. find new ones.
defText = $('def').firstChild;
jklText = $('jkl').firstChild;
goog.testing.dom.assertRangeEquals(defText, 1, jklText, 2, newRange);
}
function testNormalizeInFragmentedDomWithPreviousSiblings() {
var ghiText = $('def').nextSibling;
var mnoText = $('jkl').nextSibling;
var range = goog.dom.Range.createFromNodes(ghiText, 1, mnoText, 2);
// Fragment the DOM a bunch.
fragmentText($('def').previousSibling); // fragment abc
fragmentText(ghiText);
fragmentText(mnoText);
var newRange = normalizeBody(range);
// our old text nodes may not be valid anymore. find new ones.
ghiText = $('def').nextSibling;
mnoText = $('jkl').nextSibling;
goog.testing.dom.assertRangeEquals(ghiText, 1, mnoText, 2, newRange);
}
function testRangeCreatedInFragmentedDomWithPreviousSiblings() {
var def = $('def');
var ghiText = $('def').nextSibling;
var jkl = $('jkl');
var mnoText = $('jkl').nextSibling;
// Fragment the DOM a bunch.
fragmentText($('def').previousSibling); // fragment abc
fragmentText(ghiText);
fragmentText(mnoText);
// Notice that there are two empty text nodes at the beginning of each
// fragmented node.
var root = $('parentNode');
var range = goog.dom.Range.createFromNodes(root, 9, root, 16);
var newRange = normalizeBody(range);
// our old text nodes may not be valid anymore. find new ones.
ghiText = $('def').nextSibling;
mnoText = $('jkl').nextSibling;
goog.testing.dom.assertRangeEquals(ghiText, 1, mnoText, 2, newRange);
}
/**
* Branched from the tests for goog.dom.SavedCaretRange.
*/
function testSavedCaretRange() {
var def = $('def-1');
var jkl = $('jkl-1');
var range =
goog.dom.Range.createFromNodes(def.firstChild, 1, jkl.firstChild, 2);
range.select();
var saved = goog.editor.range.saveUsingNormalizedCarets(range);
assertHTMLEquals(
'def', def.innerHTML);
assertHTMLEquals(
'jkl', jkl.innerHTML);
clearSelectionAndRestoreSaved(saved);
var selection = goog.dom.Range.createFromWindow(window);
def = $('def-1');
jkl = $('jkl-1');
assertHTMLEquals('def', def.innerHTML);
assertHTMLEquals('jkl', jkl.innerHTML);
// Check that everything was normalized ok.
assertEquals(1, def.childNodes.length);
assertEquals(1, jkl.childNodes.length);
goog.testing.dom.assertRangeEquals(
def.firstChild, 1, jkl.firstChild, 2, selection);
}
function testRangePreservingNormalize() {
var parent = $('normalizeTest-4');
var def = $('def-4');
var jkl = $('jkl-4');
fragmentText(def.firstChild);
fragmentText(jkl.firstChild);
var range = goog.dom.Range.createFromNodes(def, 3, jkl, 4);
var oldRangeDescription = goog.testing.dom.exposeRange(range);
range = goog.editor.range.rangePreservingNormalize(parent, range);
// Check that everything was normalized ok.
assertEquals(
'def should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, def.childNodes.length);
assertEquals(
'jkl should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, jkl.childNodes.length);
goog.testing.dom.assertRangeEquals(
def.firstChild, 1, jkl.firstChild, 2, range);
}
function testRangePreservingNormalizeWhereEndNodePreviousSiblingIsSplit() {
var parent = $('normalizeTest-with-br');
var br = parent.childNodes[1];
fragmentText(parent.firstChild);
var range = goog.dom.Range.createFromNodes(parent, 3, br, 0);
range = goog.editor.range.rangePreservingNormalize(parent, range);
// Code used to throw an error here.
assertEquals('parent should have 3 children', 3, parent.childNodes.length);
goog.testing.dom.assertRangeEquals(parent.firstChild, 1, parent, 1, range);
}
function testRangePreservingNormalizeWhereStartNodePreviousSiblingIsSplit() {
var parent = $('normalizeTest-with-br');
var br = parent.childNodes[1];
fragmentText(parent.firstChild);
fragmentText(parent.lastChild);
var range = goog.dom.Range.createFromNodes(br, 0, parent, 9);
range = goog.editor.range.rangePreservingNormalize(parent, range);
// Code used to throw an error here.
assertEquals('parent should have 3 children', 3, parent.childNodes.length);
goog.testing.dom.assertRangeEquals(parent, 1, parent.lastChild, 1, range);
}
function testSelectionPreservingNormalize1() {
var parent = $('normalizeTest-2');
var def = $('def-2');
var jkl = $('jkl-2');
fragmentText(def.firstChild);
fragmentText(jkl.firstChild);
goog.dom.Range.createFromNodes(def, 3, jkl, 4).select();
assertFalse(goog.dom.Range.createFromWindow(window).isReversed());
var oldRangeDescription =
goog.testing.dom.exposeRange(goog.dom.Range.createFromWindow(window));
goog.editor.range.selectionPreservingNormalize(parent);
// Check that everything was normalized ok.
var range = goog.dom.Range.createFromWindow(window);
assertFalse(range.isReversed());
assertEquals(
'def should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, def.childNodes.length);
assertEquals(
'jkl should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, jkl.childNodes.length);
goog.testing.dom.assertRangeEquals(
def.firstChild, 1, jkl.firstChild, 2, range);
}
/**
* Make sure that selectionPreservingNormalize doesn't explode with no
* selection in the document.
*/
function testSelectionPreservingNormalize2() {
var parent = $('normalizeTest-3');
var def = $('def-3');
var jkl = $('jkl-3');
def.firstChild.splitText(1);
jkl.firstChild.splitText(2);
goog.dom.Range.clearSelection(window);
goog.editor.range.selectionPreservingNormalize(parent);
// Check that everything was normalized ok.
assertEquals(1, def.childNodes.length);
assertEquals(1, jkl.childNodes.length);
assertFalse(goog.dom.Range.hasSelection(window));
}
function testSelectionPreservingNormalize3() {
if (goog.userAgent.EDGE_OR_IE) {
return;
}
var parent = $('normalizeTest-2');
var def = $('def-2');
var jkl = $('jkl-2');
fragmentText(def.firstChild);
fragmentText(jkl.firstChild);
goog.dom.Range.createFromNodes(jkl, 4, def, 3).select();
assertTrue(goog.dom.Range.createFromWindow(window).isReversed());
var oldRangeDescription =
goog.testing.dom.exposeRange(goog.dom.Range.createFromWindow(window));
goog.editor.range.selectionPreservingNormalize(parent);
// Check that everything was normalized ok.
var range = goog.dom.Range.createFromWindow(window);
assertTrue(range.isReversed());
assertEquals(
'def should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, def.childNodes.length);
assertEquals(
'jkl should have 1 child; range is ' +
goog.testing.dom.exposeRange(range) + ', range was ' +
oldRangeDescription,
1, jkl.childNodes.length);
goog.testing.dom.assertRangeEquals(
def.firstChild, 1, jkl.firstChild, 2, range);
}
function testSelectionPreservingNormalizeAfterPlaceCursorNextTo() {
var parent = $('normalizeTest-with-div');
goog.editor.range.placeCursorNextTo(parent.firstChild);
goog.editor.range.selectionPreservingNormalize(parent);
// Code used to throw an exception here.
}
/** Normalize the body and return the normalized range. */
function normalizeBody(range) {
var rangeFactory = goog.editor.range.normalize(range);
document.body.normalize();
return rangeFactory();
}
/** Break a text node up into lots of little fragments. */
function fragmentText(text) {
// NOTE(nicksantos): For some reason, splitText makes IE deeply
// unhappy to the point where normalize and other normal DOM operations
// start failing. It's a useful test for Firefox though, because different
// versions of FireFox handle empty text nodes differently.
// See goog.editor.BrowserFeature.
if (goog.userAgent.IE) {
manualSplitText(text, 2);
manualSplitText(text, 1);
manualSplitText(text, 0);
manualSplitText(text, 0);
} else {
text.splitText(2);
text.splitText(1);
text.splitText(0);
text.splitText(0);
}
}
/**
* Clear the selection by re-parsing the DOM. Then restore the saved
* selection.
* @param {goog.dom.SavedRange} saved The saved range.
*/
function clearSelectionAndRestoreSaved(saved) {
goog.dom.Range.clearSelection(window);
assertFalse(goog.dom.Range.hasSelection(window));
saved.restore();
assertTrue(goog.dom.Range.hasSelection(window));
}
function manualSplitText(node, pos) {
var newNodeString = node.nodeValue.substr(pos);
node.nodeValue = node.nodeValue.substr(0, pos);
goog.dom.insertSiblingAfter(document.createTextNode(newNodeString), node);
}
function testSelectNodeStartSimple() {
var div = $('parentNode');
div.innerHTML = 'Cursor should go in here
'; goog.editor.range.selectNodeStart(div); var range = goog.dom.Range.createFromWindow(window); // Gotta love browsers and their inconsistencies with selection // representations. What we are trying to achieve is that when we type // the text will go into the P node. In Gecko, the selection is at the start // of the text node, as you'd expect, but in pre-530 Webkit, it has been // normalized to the visible position of P:0. if (goog.userAgent.GECKO || goog.userAgent.IE || goog.userAgent.EDGE || (goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('530'))) { goog.testing.dom.assertRangeEquals( div.firstChild.firstChild, 0, div.firstChild.firstChild, 0, range); } else { goog.testing.dom.assertRangeEquals( div.firstChild, 0, div.firstChild, 0, range); } } function testSelectNodeStartBr() { var div = $('parentNode'); div.innerHTML = '
Cursor should go in here
x
y
Italic'; // Select the whole thing. var range = goog.dom.Range.createFromNodeContents(root); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.DIV)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.B)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.I)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.CODE)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.U)); // Just select italic. range = goog.dom.Range.createFromNodes(root, 3, root, 4); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.DIV)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.B)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.I)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.CODE)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.U)); // Select "ld x y". range = goog.dom.Range.createFromNodes( root.firstChild.firstChild, 2, root.childNodes[2], 1); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.DIV)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.B)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.I)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.CODE)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.U)); // Select ol. range = goog.dom.Range.createFromNodes( root.firstChild.firstChild, 1, root.firstChild.firstChild, 3); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.DIV)); assertTrue(goog.editor.range.intersectsTag(range, goog.dom.TagName.B)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.I)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.CODE)); assertFalse(goog.editor.range.intersectsTag(range, goog.dom.TagName.U)); } function testNormalizeNode() { var div = goog.dom.createDom(goog.dom.TagName.DIV, null, 'a', 'b', 'c'); assertEquals(3, div.childNodes.length); goog.editor.range.normalizeNode(div); assertEquals(1, div.childNodes.length); assertEquals('abc', div.firstChild.nodeValue); div = goog.dom.createDom( goog.dom.TagName.DIV, null, goog.dom.createDom(goog.dom.TagName.SPAN, null, '1', '2'), goog.dom.createTextNode(''), goog.dom.createDom(goog.dom.TagName.BR), 'b', 'c'); assertEquals(5, div.childNodes.length); assertEquals(2, div.firstChild.childNodes.length); goog.editor.range.normalizeNode(div); if (goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher(1.9) || goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher(526)) { // Old Gecko and Webkit versions don't delete the empty node. assertEquals(4, div.childNodes.length); } else { assertEquals(3, div.childNodes.length); } assertEquals(1, div.firstChild.childNodes.length); assertEquals('12', div.firstChild.firstChild.nodeValue); assertEquals('bc', div.lastChild.nodeValue); assertEquals( String(goog.dom.TagName.BR), div.lastChild.previousSibling.tagName); } function testDeepestPoint() { var parent = $('parentNode'); var def = $('def'); assertEquals(def, parent.childNodes[1]); var deepestPoint = goog.editor.range.Point.createDeepestPoint; var defStartLeft = deepestPoint(parent, 1, true); assertPointEquals( def.previousSibling, def.previousSibling.nodeValue.length, defStartLeft); var defStartRight = deepestPoint(parent, 1, false); assertPointEquals(def.firstChild, 0, defStartRight); var defEndLeft = deepestPoint(parent, 2, true); assertPointEquals( def.firstChild, def.firstChild.nodeValue.length, defEndLeft); var defEndRight = deepestPoint(parent, 2, false); assertPointEquals(def.nextSibling, 0, defEndRight); } function assertPointEquals(node, offset, actualPoint) { assertEquals('Point has wrong node', node, actualPoint.node); assertEquals('Point has wrong offset', offset, actualPoint.offset); }