// Copyright 2007 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.dom.RangeTest'); goog.setTestOnly('goog.dom.RangeTest'); goog.require('goog.dom'); goog.require('goog.dom.NodeType'); goog.require('goog.dom.Range'); goog.require('goog.dom.RangeType'); goog.require('goog.dom.TagName'); goog.require('goog.dom.TextRange'); goog.require('goog.dom.browserrange'); goog.require('goog.testing.dom'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); var assertRangeEquals = goog.testing.dom.assertRangeEquals; function setUp() { // Reset the focus; some tests may invalidate the focus to exercise various // browser bugs. var focusableElement = goog.dom.getElement('focusableElement'); focusableElement.focus(); focusableElement.blur(); } function normalizeHtml(str) { return str.toLowerCase() .replace(/[\n\r\f"]/g, '') .replace(/<\/li>/g, ''); // " for emacs } function testCreate() { assertNotNull( 'Browser range object can be created for node', goog.dom.Range.createFromNodeContents(goog.dom.getElement('test1'))); } function testTableRange() { var tr = goog.dom.getElement('cell').parentNode; var range = goog.dom.Range.createFromNodeContents(tr); assertEquals('Selection should have correct text', '12', range.getText()); assertEquals( 'Selection should have correct html fragment', '12', normalizeHtml(range.getHtmlFragment())); // TODO(robbyw): On IE the TR is included, on FF it is not. // assertEquals('Selection should have correct valid html', // '12', // normalizeHtml(range.getValidHtml())); assertEquals( 'Selection should have correct pastable html', '
12
', normalizeHtml(range.getPastableHtml())); } function testUnorderedListRange() { var ul = goog.dom.getElement('ulTest').firstChild; var range = goog.dom.Range.createFromNodeContents(ul); assertEquals( 'Selection should have correct html fragment', '1
  • 2', normalizeHtml(range.getHtmlFragment())); // TODO(robbyw): On IE the UL is included, on FF it is not. // assertEquals('Selection should have correct valid html', // '
  • 1
  • 2
  • ', normalizeHtml(range.getValidHtml())); assertEquals( 'Selection should have correct pastable html', '', normalizeHtml(range.getPastableHtml())); } function testOrderedListRange() { var ol = goog.dom.getElement('olTest').firstChild; var range = goog.dom.Range.createFromNodeContents(ol); assertEquals( 'Selection should have correct html fragment', '1
  • 2', normalizeHtml(range.getHtmlFragment())); // TODO(robbyw): On IE the OL is included, on FF it is not. // assertEquals('Selection should have correct valid html', // '
  • 1
  • 2
  • ', normalizeHtml(range.getValidHtml())); assertEquals( 'Selection should have correct pastable html', '
    1. 1
    2. 2
    ', normalizeHtml(range.getPastableHtml())); } function testCreateFromNodes() { var start = goog.dom.getElement('test1').firstChild; var end = goog.dom.getElement('br'); var range = goog.dom.Range.createFromNodes(start, 2, end, 0); assertNotNull( 'Browser range object can be created for W3C node range', range); assertEquals( 'Start node should be selected at start endpoint', start, range.getStartNode()); assertEquals('Selection should start at offset 2', 2, range.getStartOffset()); assertEquals( 'Start node should be selected at anchor endpoint', start, range.getAnchorNode()); assertEquals( 'Selection should be anchored at offset 2', 2, range.getAnchorOffset()); var div = goog.dom.getElement('test2'); assertEquals( 'DIV node should be selected at end endpoint', div, range.getEndNode()); assertEquals('Selection should end at offset 1', 1, range.getEndOffset()); assertEquals( 'DIV node should be selected at focus endpoint', div, range.getFocusNode()); assertEquals( 'Selection should be focused at offset 1', 1, range.getFocusOffset()); assertTrue( 'Text content should be "xt\\s*abc"', /xt\s*abc/.test(range.getText())); assertFalse('Nodes range is not collapsed', range.isCollapsed()); } function testCreateControlRange() { if (!goog.userAgent.IE) { return; } var cr = document.body.createControlRange(); cr.addElement(goog.dom.getElement('logo')); var range = goog.dom.Range.createFromBrowserRange(cr); assertNotNull( 'Control range object can be created from browser range', range); assertEquals( 'Created range is a control range', goog.dom.RangeType.CONTROL, range.getType()); } function testTextNode() { var range = goog.dom.Range.createFromNodeContents( goog.dom.getElement('test1').firstChild); assertEquals( 'Created range is a text range', goog.dom.RangeType.TEXT, range.getType()); assertEquals( 'Text node should be selected at start endpoint', 'Text', range.getStartNode().nodeValue); assertEquals('Selection should start at offset 0', 0, range.getStartOffset()); assertEquals( 'Text node should be selected at end endpoint', 'Text', range.getEndNode().nodeValue); assertEquals( 'Selection should end at offset 4', 'Text'.length, range.getEndOffset()); assertEquals( 'Container should be text node', goog.dom.NodeType.TEXT, range.getContainer().nodeType); assertEquals('Text content should be "Text"', 'Text', range.getText()); assertFalse('Text range is not collapsed', range.isCollapsed()); } function testDiv() { var range = goog.dom.Range.createFromNodeContents(goog.dom.getElement('test2')); assertEquals( 'Text node "abc" should be selected at start endpoint', 'abc', range.getStartNode().nodeValue); assertEquals('Selection should start at offset 0', 0, range.getStartOffset()); assertEquals( 'Text node "def" should be selected at end endpoint', 'def', range.getEndNode().nodeValue); assertEquals( 'Selection should end at offset 3', 'def'.length, range.getEndOffset()); assertEquals( 'Container should be DIV', goog.dom.getElement('test2'), range.getContainer()); assertTrue( 'Div text content should be "abc\\s*def"', /abc\s*def/.test(range.getText())); assertFalse('Div range is not collapsed', range.isCollapsed()); } function testEmptyNode() { var range = goog.dom.Range.createFromNodeContents(goog.dom.getElement('empty')); assertEquals( 'DIV be selected at start endpoint', goog.dom.getElement('empty'), range.getStartNode()); assertEquals('Selection should start at offset 0', 0, range.getStartOffset()); assertEquals( 'DIV should be selected at end endpoint', goog.dom.getElement('empty'), range.getEndNode()); assertEquals('Selection should end at offset 0', 0, range.getEndOffset()); assertEquals( 'Container should be DIV', goog.dom.getElement('empty'), range.getContainer()); assertEquals('Empty text content should be ""', '', range.getText()); assertTrue('Empty range is collapsed', range.isCollapsed()); } function testCollapse() { var range = goog.dom.Range.createFromNodeContents(goog.dom.getElement('test2')); assertFalse('Div range is not collapsed', range.isCollapsed()); range.collapse(); assertTrue( 'Div range is collapsed after call to empty()', range.isCollapsed()); range = goog.dom.Range.createFromNodeContents(goog.dom.getElement('empty')); assertTrue('Empty range is collapsed', range.isCollapsed()); range.collapse(); assertTrue('Empty range is still collapsed', range.isCollapsed()); } // TODO(robbyw): Test iteration over a strange document fragment. function testIterator() { goog.testing.dom.assertNodesMatch( goog.dom.Range.createFromNodeContents(goog.dom.getElement('test2')), ['abc', '#br', '#br', 'def']); } function testReversedNodes() { var node = goog.dom.getElement('test1').firstChild; var range = goog.dom.Range.createFromNodes(node, 4, node, 0); assertTrue('Range is reversed', range.isReversed()); node = goog.dom.getElement('test3'); range = goog.dom.Range.createFromNodes(node, 0, node, 1); assertFalse('Range is not reversed', range.isReversed()); } function testReversedContents() { var range = goog.dom.Range.createFromNodeContents(goog.dom.getElement('test1'), true); assertTrue('Range is reversed', range.isReversed()); assertEquals('Range should select "Text"', 'Text', range.getText()); assertEquals('Range start offset should be 0', 0, range.getStartOffset()); assertEquals('Range end offset should be 4', 4, range.getEndOffset()); assertEquals('Range anchor offset should be 4', 4, range.getAnchorOffset()); assertEquals('Range focus offset should be 0', 0, range.getFocusOffset()); var range2 = range.clone(); range.collapse(true); assertTrue('Range is collapsed', range.isCollapsed()); assertFalse('Collapsed range is not reversed', range.isReversed()); assertEquals( 'Post collapse start offset should be 4', 4, range.getStartOffset()); range2.collapse(false); assertTrue('Range 2 is collapsed', range2.isCollapsed()); assertFalse('Collapsed range 2 is not reversed', range2.isReversed()); assertEquals( 'Post collapse start offset 2 should be 0', 0, range2.getStartOffset()); } function testRemoveContents() { var outer = goog.dom.getElement('removeTest'); var range = goog.dom.Range.createFromNodeContents(outer.firstChild); range.removeContents(); assertEquals('Removed range content should be ""', '', range.getText()); assertTrue('Removed range should be collapsed', range.isCollapsed()); assertEquals('Outer div should have 1 child now', 1, outer.childNodes.length); assertEquals( 'Inner div should be empty', 0, outer.firstChild.childNodes.length); } function testRemovePartialContents() { var outer = goog.dom.getElement('removePartialTest'); var originalText = goog.dom.getTextContent(outer); try { var range = goog.dom.Range.createFromNodes( outer.firstChild, 2, outer.firstChild, 4); removeHelper(1, range, outer, 1, '0145'); range = goog.dom.Range.createFromNodes( outer.firstChild, 0, outer.firstChild, 1); removeHelper(2, range, outer, 1, '145'); range = goog.dom.Range.createFromNodes( outer.firstChild, 2, outer.firstChild, 3); removeHelper(3, range, outer, 1, '14'); var br = goog.dom.createDom(goog.dom.TagName.BR); outer.appendChild(br); range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer, 1); removeHelper(4, range, outer, 2, '1
    '); outer.innerHTML = '
    123'; range = goog.dom.Range.createFromNodes(outer, 0, outer.lastChild, 2); removeHelper(5, range, outer, 1, '3'); outer.innerHTML = '123
    456'; range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.lastChild, 2); removeHelper(6, range, outer, 2, '16'); outer.innerHTML = '123
    456'; range = goog.dom.Range.createFromNodes(outer.firstChild, 0, outer.lastChild, 2); removeHelper(7, range, outer, 1, '6'); outer.innerHTML = '
    '; range = goog.dom.Range.createFromNodeContents(outer.firstChild); removeHelper(8, range, outer, 1, '
    '); } finally { // Restore the original text state for repeated runs. goog.dom.setTextContent(outer, originalText); } // TODO(robbyw): Fix the following edge cases: // * Selecting contents of a node containing multiply empty divs // * Selecting via createFromNodes(x, 0, x, x.childNodes.length) // * Consistent handling of nodeContents(
    ).remove } function removeHelper( testNumber, range, outer, expectedChildCount, expectedContent) { range.removeContents(); assertTrue( testNumber + ': Removed range should now be collapsed', range.isCollapsed()); assertEquals( testNumber + ': Removed range content should be ""', '', range.getText()); assertEquals( testNumber + ': Outer div should contain correct text', expectedContent, outer.innerHTML.toLowerCase()); assertEquals( testNumber + ': Outer div should have ' + expectedChildCount + ' children now', expectedChildCount, outer.childNodes.length); assertNotNull( testNumber + ': Empty node should still exist', goog.dom.getElement('empty')); } function testSurroundContents() { var outer = goog.dom.getElement('surroundTest'); outer.innerHTML = '---Text that
    will be surrounded---'; var range = goog.dom.Range.createFromNodes( outer.firstChild, 3, outer.lastChild, outer.lastChild.nodeValue.length - 3); var div = goog.dom.createDom(goog.dom.TagName.DIV, {'style': 'color: red'}); var output = range.surroundContents(div); assertEquals( 'Outer element should contain new element', outer, output.parentNode); assertFalse('New element should have no id', !!output.id); assertEquals('New element should be red', 'red', output.style.color); assertEquals( 'Outer element should have three children', 3, outer.childNodes.length); assertEquals( 'New element should have three children', 3, output.childNodes.length); // TODO(robbyw): Ensure the range stays in a reasonable state. } /** * Given two offsets into the 'foobar' node, make sure that inserting * nodes at those offsets doesn't change a selection of 'oba'. * @bug 1480638 */ function assertSurroundDoesntChangeSelectionWithOffsets( offset1, offset2, expectedHtml) { var div = goog.dom.getElement('bug1480638'); goog.dom.setTextContent(div, 'foobar'); var rangeToSelect = goog.dom.Range.createFromNodes(div.firstChild, 2, div.firstChild, 5); rangeToSelect.select(); var rangeToSurround = goog.dom.Range.createFromNodes( div.firstChild, offset1, div.firstChild, offset2); rangeToSurround.surroundWithNodes( goog.dom.createDom(goog.dom.TagName.SPAN), goog.dom.createDom(goog.dom.TagName.SPAN)); // Make sure that the selection didn't change. assertHTMLEquals( 'Selection must not change when contents are surrounded.', expectedHtml, goog.dom.Range.createFromWindow().getHtmlFragment()); } function testSurroundWithNodesDoesntChangeSelection1() { assertSurroundDoesntChangeSelectionWithOffsets( 3, 4, 'oba'); } function testSurroundWithNodesDoesntChangeSelection2() { assertSurroundDoesntChangeSelectionWithOffsets(3, 6, 'oba'); } function testSurroundWithNodesDoesntChangeSelection3() { assertSurroundDoesntChangeSelectionWithOffsets(1, 3, 'oba'); } function testSurroundWithNodesDoesntChangeSelection4() { assertSurroundDoesntChangeSelectionWithOffsets(1, 6, 'oba'); } function testInsertNode() { var outer = goog.dom.getElement('insertTest'); goog.dom.setTextContent(outer, 'ACD'); var range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.firstChild, 2); range.insertNode(goog.dom.createTextNode('B'), true); assertEquals( 'Element should have correct innerHTML', 'ABCD', outer.innerHTML); goog.dom.setTextContent(outer, '12'); range = goog.dom.Range.createFromNodes(outer.firstChild, 0, outer.firstChild, 1); var br = range.insertNode(goog.dom.createDom(goog.dom.TagName.BR), false); assertEquals( 'New element should have correct innerHTML', '1
    2', outer.innerHTML.toLowerCase()); assertEquals('BR should be in outer', outer, br.parentNode); } function testReplaceContentsWithNode() { var outer = goog.dom.getElement('insertTest'); goog.dom.setTextContent(outer, 'AXC'); var range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.firstChild, 2); range.replaceContentsWithNode(goog.dom.createTextNode('B')); assertEquals('Element should have correct innerHTML', 'ABC', outer.innerHTML); goog.dom.setTextContent(outer, 'ABC'); range = goog.dom.Range.createFromNodes(outer.firstChild, 3, outer.firstChild, 3); range.replaceContentsWithNode(goog.dom.createTextNode('D')); assertEquals( 'Element should have correct innerHTML after collapsed replace', 'ABCD', outer.innerHTML); outer.innerHTML = 'AXXXC'; range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.lastChild, 1); range.replaceContentsWithNode(goog.dom.createTextNode('B')); goog.testing.dom.assertHtmlContentsMatch('ABC', outer); } function testSurroundWithNodes() { var outer = goog.dom.getElement('insertTest'); goog.dom.setTextContent(outer, 'ACE'); var range = goog.dom.Range.createFromNodes(outer.firstChild, 1, outer.firstChild, 2); range.surroundWithNodes( goog.dom.createTextNode('B'), goog.dom.createTextNode('D')); assertEquals( 'New element should have correct innerHTML', 'ABCDE', outer.innerHTML); } function testIsRangeInDocument() { var outer = goog.dom.getElement('insertTest'); outer.innerHTML = '
    ABC'; var range = goog.dom.Range.createCaret(outer.lastChild, 1); assertEquals( 'Should get correct start element', 'ABC', range.getStartNode().nodeValue); assertTrue('Should be considered in document', range.isRangeInDocument()); goog.dom.setTextContent(outer, 'DEF'); assertFalse('Should be marked as out of document', range.isRangeInDocument()); } function testRemovedNode() { var node = goog.dom.getElement('removeNodeTest'); var range = goog.dom.browserrange.createRangeFromNodeContents(node); range.select(); goog.dom.removeNode(node); var newRange = goog.dom.Range.createFromWindow(window); // In Chrome 14 and below (<= Webkit 535.1), newRange will be null. // In Chrome 16 and above (>= Webkit 535.7), newRange will be collapsed // like on other browsers. // We didn't bother testing in between. if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('535.7')) { assertNull('Webkit supports rangeCount == 0', newRange); } else { assertTrue( 'The other browsers will just have an empty range.', newRange.isCollapsed()); } } function testReversedRange() { if (goog.userAgent.EDGE_OR_IE) return; // IE doesn't make this distinction. goog.dom.Range .createFromNodes( goog.dom.getElement('test2'), 0, goog.dom.getElement('test1'), 0) .select(); var range = goog.dom.Range.createFromWindow(window); assertTrue('Range should be reversed', range.isReversed()); } function testUnreversedRange() { goog.dom.Range .createFromNodes( goog.dom.getElement('test1'), 0, goog.dom.getElement('test2'), 0) .select(); var range = goog.dom.Range.createFromWindow(window); assertFalse('Range should not be reversed', range.isReversed()); } function testReversedThenUnreversedRange() { // This tests a workaround for a webkit bug where webkit caches selections // incorrectly. goog.dom.Range .createFromNodes( goog.dom.getElement('test2'), 0, goog.dom.getElement('test1'), 0) .select(); goog.dom.Range .createFromNodes( goog.dom.getElement('test1'), 0, goog.dom.getElement('test2'), 0) .select(); var range = goog.dom.Range.createFromWindow(window); assertFalse('Range should not be reversed', range.isReversed()); } function testHasAndClearSelection() { goog.dom.Range.createFromNodeContents(goog.dom.getElement('test1')).select(); assertTrue('Selection should exist', goog.dom.Range.hasSelection()); goog.dom.Range.clearSelection(); assertFalse('Selection should not exist', goog.dom.Range.hasSelection()); } function assertForward(string, startNode, startOffset, endNode, endOffset) { var root = goog.dom.getElement('test2'); var originalInnerHtml = root.innerHTML; assertFalse( string, goog.dom.Range.isReversed(startNode, startOffset, endNode, endOffset)); assertTrue( string, goog.dom.Range.isReversed(endNode, endOffset, startNode, startOffset)); assertEquals( 'Contents should be unaffected after: ' + string, root.innerHTML, originalInnerHtml); } function testIsReversed() { var root = goog.dom.getElement('test2'); var text1 = root.firstChild; // Text content: 'abc'. var br = root.childNodes[1]; var text2 = root.lastChild; // Text content: 'def'. assertFalse( 'Same element position gives false', goog.dom.Range.isReversed(root, 0, root, 0)); assertFalse( 'Same text position gives false', goog.dom.Range.isReversed(text1, 0, text2, 0)); assertForward( 'Element offsets should compare against each other', root, 0, root, 2); assertForward( 'Text node offsets should compare against each other', text1, 0, text2, 2); assertForward('Text nodes should compare correctly', text1, 0, text2, 0); assertForward('Text nodes should compare to later elements', text1, 0, br, 0); assertForward( 'Text nodes should compare to earlier elements', br, 0, text2, 0); assertForward('Parent is before element child', root, 0, br, 0); assertForward('Parent is before text child', root, 0, text1, 0); assertFalse( 'Equivalent position gives false', goog.dom.Range.isReversed(root, 0, text1, 0)); assertFalse( 'Equivalent position gives false', goog.dom.Range.isReversed(root, 1, br, 0)); assertForward('End of element is after children', text1, 0, root, 3); assertForward('End of element is after children', br, 0, root, 3); assertForward('End of element is after children', text2, 0, root, 3); assertForward('End of element is after end of last child', text2, 3, root, 3); } function testSelectAroundSpaces() { // set the selection var textNode = goog.dom.getElement('textWithSpaces').firstChild; goog.dom.TextRange.createFromNodes(textNode, 5, textNode, 12).select(); // get the selection and check that it matches what we set it to var range = goog.dom.Range.createFromWindow(); assertEquals(' world ', range.getText()); assertEquals(5, range.getStartOffset()); assertEquals(12, range.getEndOffset()); assertEquals(textNode, range.getContainer()); // Check the contents again, because there used to be a bug where // it changed after calling getContainer(). assertEquals(' world ', range.getText()); } function testSelectInsideSpaces() { // set the selection var textNode = goog.dom.getElement('textWithSpaces').firstChild; goog.dom.TextRange.createFromNodes(textNode, 6, textNode, 11).select(); // get the selection and check that it matches what we set it to var range = goog.dom.Range.createFromWindow(); assertEquals('world', range.getText()); assertEquals(6, range.getStartOffset()); assertEquals(11, range.getEndOffset()); assertEquals(textNode, range.getContainer()); // Check the contents again, because there used to be a bug where // it changed after calling getContainer(). assertEquals('world', range.getText()); } function testRangeBeforeBreak() { var container = goog.dom.getElement('rangeAroundBreaks'); var text = container.firstChild; var offset = text.length; assertEquals(4, offset); var br = container.childNodes[1]; var caret = goog.dom.Range.createCaret(text, offset); caret.select(); assertEquals(offset, caret.getStartOffset()); var range = goog.dom.Range.createFromWindow(); assertFalse('Should not contain whole
    ', range.containsNode(br, false)); if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) { assertTrue( 'Range over
    is adjacent to the immediate range before it', range.containsNode(br, true)); } else { assertFalse( 'Should not contain partial
    ', range.containsNode(br, true)); } assertEquals(offset, range.getStartOffset()); assertEquals(text, range.getStartNode()); } function testRangeAfterBreak() { var container = goog.dom.getElement('rangeAroundBreaks'); var br = container.childNodes[1]; var caret = goog.dom.Range.createCaret(container.lastChild, 0); caret.select(); assertEquals(0, caret.getStartOffset()); var range = goog.dom.Range.createFromWindow(); assertFalse('Should not contain whole
    ', range.containsNode(br, false)); var isSafari3 = goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('528'); if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) || isSafari3) { assertTrue( 'Range over
    is adjacent to the immediate range after it', range.containsNode(br, true)); } else { assertFalse( 'Should not contain partial
    ', range.containsNode(br, true)); } if (isSafari3) { assertEquals(2, range.getStartOffset()); assertEquals(container, range.getStartNode()); } else { assertEquals(0, range.getStartOffset()); assertEquals(container.lastChild, range.getStartNode()); } } function testRangeAtBreakAtStart() { var container = goog.dom.getElement('breaksAroundNode'); var br = container.firstChild; var caret = goog.dom.Range.createCaret(container.firstChild, 0); caret.select(); assertEquals(0, caret.getStartOffset()); var range = goog.dom.Range.createFromWindow(); assertTrue( 'Range over
    is adjacent to the immediate range before it', range.containsNode(br, true)); assertFalse('Should not contain whole
    ', range.containsNode(br, false)); assertRangeEquals(container, 0, container, 0, range); } function testFocusedElementDisappears() { // This reproduces a failure case specific to Gecko, where an element is // created, contentEditable is set, is focused, and removed. After that // happens, calling selection.collapse fails. // https://bugzilla.mozilla.org/show_bug.cgi?id=773137 var disappearingElement = goog.dom.createDom(goog.dom.TagName.DIV); document.body.appendChild(disappearingElement); disappearingElement.contentEditable = true; disappearingElement.focus(); document.body.removeChild(disappearingElement); var container = goog.dom.getElement('empty'); var caret = goog.dom.Range.createCaret(container, 0); // This should not throw. caret.select(); assertEquals(0, caret.getStartOffset()); } function assertNodeEquals(expected, actual) { assertEquals( 'Expected: ' + goog.testing.dom.exposeNode(expected) + '\nActual: ' + goog.testing.dom.exposeNode(actual), expected, actual); }