node_test.js

// 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.nodeTest');
goog.setTestOnly('goog.editor.nodeTest');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.editor.node');
goog.require('goog.style');
goog.require('goog.testing.ExpectedFailures');
goog.require('goog.testing.dom');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');

var expectedFailures;
var parentNode;
var childNode1;
var childNode2;
var childNode3;

var gChildWsNode1 = null;
var gChildTextNode1 = null;
var gChildNbspNode1 = null;
var gChildMixedNode1 = null;
var gChildWsNode2a = null;
var gChildWsNode2b = null;
var gChildTextNode3a = null;
var gChildWsNode3 = null;
var gChildTextNode3b = null;

function setUpPage() {
  expectedFailures = new goog.testing.ExpectedFailures();
  parentNode = document.getElementById('parentNode');
  childNode1 = parentNode.childNodes[0];
  childNode2 = parentNode.childNodes[1];
  childNode3 = parentNode.childNodes[2];
}


function tearDown() {
  expectedFailures.handleTearDown();
}

function setUpDomTree() {
  gChildWsNode1 = document.createTextNode(' \t\r\n');
  gChildTextNode1 = document.createTextNode('Child node');
  gChildNbspNode1 = document.createTextNode('\u00a0');
  gChildMixedNode1 = document.createTextNode('Text\n plus\u00a0');
  gChildWsNode2a = document.createTextNode('');
  gChildWsNode2b = document.createTextNode(' ');
  gChildTextNode3a = document.createTextNode('I am a grand child');
  gChildWsNode3 = document.createTextNode('   \t  \r   \n');
  gChildTextNode3b = document.createTextNode('I am also a grand child');

  childNode3.appendChild(gChildTextNode3a);
  childNode3.appendChild(gChildWsNode3);
  childNode3.appendChild(gChildTextNode3b);

  childNode1.appendChild(gChildMixedNode1);
  childNode1.appendChild(gChildWsNode1);
  childNode1.appendChild(gChildNbspNode1);
  childNode1.appendChild(gChildTextNode1);

  childNode2.appendChild(gChildWsNode2a);
  childNode2.appendChild(gChildWsNode2b);
  document.body.appendChild(parentNode);
}

function tearDownDomTree() {
  childNode1.innerHTML = childNode2.innerHTML = childNode3.innerHTML = '';
  gChildWsNode1 = null;
  gChildTextNode1 = null;
  gChildNbspNode1 = null;
  gChildMixedNode1 = null;
  gChildWsNode2a = null;
  gChildWsNode2b = null;
  gChildTextNode3a = null;
  gChildWsNode3 = null;
  gChildTextNode3b = null;
}

function testGetCompatModeQuirks() {
  var quirksIfr = document.createElement('iframe');
  document.body.appendChild(quirksIfr);
  // Webkit used to default to standards mode, but fixed this in
  // Safari 4/Chrome 2, aka, WebKit 530.
  expectedFailures.expectFailureFor(goog.userAgent.WEBKIT &&
                                    !goog.userAgent.isVersionOrHigher('530'));
  expectedFailures.run(function() {
    assertFalse('Empty sourceless iframe is quirks mode, not standards mode',
        goog.editor.node.isStandardsMode(
            goog.dom.getFrameContentDocument(quirksIfr)));
  });
  document.body.removeChild(quirksIfr);
}

function testGetCompatModeStandards() {
  var standardsIfr = document.createElement('iframe');
  document.body.appendChild(standardsIfr);
  var doc = goog.dom.getFrameContentDocument(standardsIfr);
  doc.open();
  doc.write('<!DOCTYPE HTML><html><head></head><body>&nbsp;</body></html>');
  doc.close();
  assertTrue('Iframe with DOCTYPE written in is standards mode',
      goog.editor.node.isStandardsMode(doc));
  document.body.removeChild(standardsIfr);
}


/**
 * Creates a DOM tree and tests that getLeftMostLeaf returns proper node
 */
function testGetLeftMostLeaf() {
  setUpDomTree();

  assertEquals('Should skip ws node', gChildMixedNode1,
               goog.editor.node.getLeftMostLeaf(parentNode));
  assertEquals('Should skip ws node', gChildMixedNode1,
               goog.editor.node.getLeftMostLeaf(childNode1));
  assertEquals('Has no non ws leaves', childNode2,
               goog.editor.node.getLeftMostLeaf(childNode2));
  assertEquals('Should return first child', gChildTextNode3a,
               goog.editor.node.getLeftMostLeaf(childNode3));
  assertEquals('Has no children', gChildTextNode1,
               goog.editor.node.getLeftMostLeaf(gChildTextNode1));

  tearDownDomTree();
}


/**
 * Creates a DOM tree and tests that getRightMostLeaf returns proper node
 */
function testGetRightMostLeaf() {
  setUpDomTree();

  assertEquals("Should return child3's rightmost child", gChildTextNode3b,
               goog.editor.node.getRightMostLeaf(parentNode));
  assertEquals('Should skip ws node', gChildTextNode1,
               goog.editor.node.getRightMostLeaf(childNode1));
  assertEquals('Has no non ws leaves', childNode2,
               goog.editor.node.getRightMostLeaf(childNode2));
  assertEquals('Should return last child', gChildTextNode3b,
               goog.editor.node.getRightMostLeaf(childNode3));
  assertEquals('Has no children', gChildTextNode1,
               goog.editor.node.getRightMostLeaf(gChildTextNode1));

  tearDownDomTree();
}


/**
 * Creates a DOM tree and tests that getFirstChild properly ignores
 * ignorable nodes
 */
function testGetFirstChild() {
  setUpDomTree();

  assertNull('Has no none ws children',
      goog.editor.node.getFirstChild(childNode2));
  assertEquals('Should skip first child, as it is ws', gChildMixedNode1,
      goog.editor.node.getFirstChild(childNode1));
  assertEquals('Should just return first child', gChildTextNode3a,
      goog.editor.node.getFirstChild(childNode3));
  assertEquals('Should return first child', childNode1,
      goog.editor.node.getFirstChild(parentNode));

  assertNull('First child of a text node should return null',
      goog.editor.node.getFirstChild(gChildTextNode1));
  assertNull('First child of null should return null',
      goog.editor.node.getFirstChild(null));

  tearDownDomTree();
}


/**
 * Create a DOM tree and test that getLastChild properly ignores
 * ignorable nodes
 */
function testGetLastChild() {
  setUpDomTree();

  assertNull('Has no none ws children',
      goog.editor.node.getLastChild(childNode2));
  assertEquals('Should skip last child, as it is ws', gChildTextNode1,
               goog.editor.node.getLastChild(childNode1));
  assertEquals('Should just return last child', gChildTextNode3b,
               goog.editor.node.getLastChild(childNode3));
  assertEquals('Should return last child', childNode3,
               goog.editor.node.getLastChild(parentNode));

  assertNull('Last child of a text node should return null',
      goog.editor.node.getLastChild(gChildTextNode1));
  assertNull('Last child of null should return null',
      goog.editor.node.getLastChild(gChildTextNode1));

  tearDownDomTree();
}


/**
 * Test if nodes that should be ignorable return false and nodes that should
 * not be ignored return true.
 */
function testIsImportant() {
  var wsNode = document.createTextNode(' \t\r\n');
  assertFalse('White space node is ignorable',
      goog.editor.node.isImportant(wsNode));
  var textNode = document.createTextNode('Hello');
  assertTrue('Text node is important', goog.editor.node.isImportant(textNode));
  var nbspNode = document.createTextNode('\u00a0');
  assertTrue('Node with nbsp is important',
      goog.editor.node.isImportant(nbspNode));
  var imageNode = document.createElement('img');
  assertTrue('Image node is important',
      goog.editor.node.isImportant(imageNode));
}


/**
 * Test that isAllNonNbspWhiteSpace returns true if node contains only
 * whitespace that is not nbsp and false otherwise
 */
function testIsAllNonNbspWhiteSpace() {
  var wsNode = document.createTextNode(' \t\r\n');
  assertTrue('String is all non nbsp',
      goog.editor.node.isAllNonNbspWhiteSpace(wsNode));
  var textNode = document.createTextNode('Hello');
  assertFalse('String should not be whitespace',
              goog.editor.node.isAllNonNbspWhiteSpace(textNode));
  var nbspNode = document.createTextNode('\u00a0');
  assertFalse('String has nbsp',
      goog.editor.node.isAllNonNbspWhiteSpace(nbspNode));
}


/**
 * Creates a DOM tree and Test that getPreviousSibling properly ignores
 * ignorable nodes
 */
function testGetPreviousSibling() {
  setUpDomTree();

  assertNull('No previous sibling',
             goog.editor.node.getPreviousSibling(gChildTextNode3a));
  assertEquals('Should have text sibling', gChildTextNode3a,
               goog.editor.node.getPreviousSibling(gChildWsNode3));
  assertEquals('Should skip over white space sibling', gChildTextNode3a,
               goog.editor.node.getPreviousSibling(gChildTextNode3b));
  assertNull('No previous sibling',
             goog.editor.node.getPreviousSibling(gChildMixedNode1));
  assertEquals('Should have mixed text sibling', gChildMixedNode1,
               goog.editor.node.getPreviousSibling(gChildWsNode1));
  assertEquals('Should skip over white space sibling', gChildMixedNode1,
               goog.editor.node.getPreviousSibling(gChildNbspNode1));
  assertNotEquals('Should not move past ws and nbsp', gChildMixedNode1,
                  goog.editor.node.getPreviousSibling(gChildTextNode1));
  assertEquals('Should go to child 2', childNode2,
               goog.editor.node.getPreviousSibling(childNode3));
  assertEquals('Should go to child 1', childNode1,
               goog.editor.node.getPreviousSibling(childNode2));
  assertNull('Only has white space siblings',
             goog.editor.node.getPreviousSibling(gChildWsNode2b));

  tearDownDomTree();
}


/**
 * Creates a DOM tree and tests that getNextSibling properly ignores igrnorable
 * nodes when determining the next sibling
 */
function testGetNextSibling() {
  setUpDomTree();

  assertEquals('Child 1 should have Child 2', childNode2,
               goog.editor.node.getNextSibling(childNode1));
  assertEquals('Child 2 should have child 3', childNode3,
               goog.editor.node.getNextSibling(childNode2));
  assertNull('Child 3 has no next sibling',
      goog.editor.node.getNextSibling(childNode3));
  assertNotEquals('Should not skip ws and nbsp nodes', gChildTextNode1,
                  goog.editor.node.getNextSibling(gChildMixedNode1));
  assertNotEquals('Should not skip nbsp node', gChildTextNode1,
                  goog.editor.node.getNextSibling(gChildWsNode1));
  assertEquals('Should have sibling', gChildTextNode1,
               goog.editor.node.getNextSibling(gChildNbspNode1));
  assertNull('Should have no next sibling',
             goog.editor.node.getNextSibling(gChildTextNode1));
  assertNull('Only has ws sibling',
      goog.editor.node.getNextSibling(gChildWsNode2a));
  assertNull('Has no next sibling',
      goog.editor.node.getNextSibling(gChildWsNode2b));
  assertEquals('Should skip ws node', gChildTextNode3b,
               goog.editor.node.getNextSibling(gChildTextNode3a));

  tearDownDomTree();
}


function testIsEmpty() {
  var textNode = document.createTextNode('');
  assertTrue('Text node with no content should be empty',
      goog.editor.node.isEmpty(textNode));
  textNode.data = '\xa0';
  assertTrue('Text node with nbsp should be empty',
      goog.editor.node.isEmpty(textNode));
  assertFalse('Text node with nbsp should not be empty when prohibited',
      goog.editor.node.isEmpty(textNode, true));

  textNode.data = '     ';
  assertTrue('Text node with whitespace should be empty',
      goog.editor.node.isEmpty(textNode));
  textNode.data = 'notEmpty';
  assertFalse('Text node with text should not be empty',
      goog.editor.node.isEmpty(textNode));

  var div = document.createElement('div');
  assertTrue('Empty div should be empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<iframe></iframe>';
  assertFalse('Div containing an iframe is not empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<img></img>';
  assertFalse('Div containing an image is not empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<embed></embed>';
  assertFalse('Div containing an embed is not empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<div><span></span></div>';
  assertTrue('Div containing other empty tags is empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<div><span>  </span></div>';
  assertTrue('Div containing other empty tags and whitespace is empty',
      goog.editor.node.isEmpty(div));
  div.innerHTML = '<div><span>Not empty</span></div>';
  assertFalse('Div containing tags and text is not empty',
      goog.editor.node.isEmpty(div));

  var img = document.createElement(goog.dom.TagName.IMG);
  assertFalse('Empty img should not be empty',
      goog.editor.node.isEmpty(img));

  var iframe = document.createElement(goog.dom.TagName.IFRAME);
  assertFalse('Empty iframe should not be empty',
      goog.editor.node.isEmpty(iframe));

  var embed = document.createElement('embed');
  assertFalse('Empty embed should not be empty',
      goog.editor.node.isEmpty(embed));
}


/**
 * Test that getLength returns 0 if the node has no length and no children,
 * the # of children if the node has no length but does have children,
 * and the length of the node if the node does have length
 */
function testGetLength() {
  var parentNode = document.createElement('p');

  assertEquals('Length 0 and no children', 0,
      goog.editor.node.getLength(parentNode));

  var childNode1 = document.createTextNode('node 1');
  var childNode2 = document.createTextNode('node number 2');
  var childNode3 = document.createTextNode('');
  parentNode.appendChild(childNode1);
  parentNode.appendChild(childNode2);
  parentNode.appendChild(childNode3);
  assertEquals('Length 0 and 3 children', 3,
      goog.editor.node.getLength(parentNode));
  assertEquals('Text node, length 6', 6,
      goog.editor.node.getLength(childNode1));
  assertEquals('Text node, length 0', 0,
      goog.editor.node.getLength(childNode3));
}

function testFindInChildrenSuccess() {
  var parentNode = document.createElement('div');
  parentNode.innerHTML = '<div>foo</div><b>foo2</b>';

  var index = goog.editor.node.findInChildren(parentNode,
      function(node) {
        return node.tagName == 'B';
      });
  assertEquals('Should find second child', index, 1);
}

function testFindInChildrenFailure() {
  var parentNode = document.createElement('div');
  parentNode.innerHTML = '<div>foo</div><b>foo2</b>';

  var index = goog.editor.node.findInChildren(parentNode,
      function(node) {
        return false;
      });
  assertNull("Shouldn't find a child", index);
}

function testFindHighestMatchingAncestor() {
  setUpDomTree();
  var predicateFunc = function(node) {
    return node.tagName == 'DIV';
  };
  var node = goog.editor.node.findHighestMatchingAncestor(
      gChildTextNode3a, predicateFunc);
  assertNotNull('Should return an ancestor', node);
  assertEquals('Should have found "parentNode" as the last ' +
               'ancestor matching the predicate',
               parentNode,
               node);

  predicateFunc = function(node) {
    return node.childNodes.length == 1;
  };
  node = goog.editor.node.findHighestMatchingAncestor(gChildTextNode3a,
                                                      predicateFunc);
  assertNull("Shouldn't return an ancestor", node);

  tearDownDomTree();
}

function testIsBlock() {
  var blockDisplays = ['block', 'list-item', 'table', 'table-caption',
    'table-cell', 'table-column', 'table-column-group', 'table-footer',
    'table-footer-group', 'table-header-group', 'table-row',
    'table-row-group'];

  var structuralTags = [
    goog.dom.TagName.BODY,
    goog.dom.TagName.FRAME,
    goog.dom.TagName.FRAMESET,
    goog.dom.TagName.HEAD,
    goog.dom.TagName.HTML
  ];

  // The following tags are considered inline in IE, except LEGEND which is
  // only a block element in WEBKIT.
  var ambiguousTags = [
    goog.dom.TagName.DETAILS,
    goog.dom.TagName.HR,
    goog.dom.TagName.ISINDEX,
    goog.dom.TagName.LEGEND,
    goog.dom.TagName.MAP,
    goog.dom.TagName.NOFRAMES,
    goog.dom.TagName.OPTGROUP,
    goog.dom.TagName.OPTION,
    goog.dom.TagName.SUMMARY
  ];

  // Older versions of IE and Gecko consider the following elements to be
  // inline, but IE9+ and Gecko 2.0+ recognize the new elements.
  var legacyAmbiguousTags = [
    goog.dom.TagName.ARTICLE,
    goog.dom.TagName.ASIDE,
    goog.dom.TagName.FIGCAPTION,
    goog.dom.TagName.FIGURE,
    goog.dom.TagName.FOOTER,
    goog.dom.TagName.HEADER,
    goog.dom.TagName.HGROUP,
    goog.dom.TagName.NAV,
    goog.dom.TagName.SECTION
  ];

  var tagsToIgnore = goog.array.flatten(structuralTags, ambiguousTags);

  if ((goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) ||
      (goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('2'))) {
    goog.array.extend(tagsToIgnore, legacyAmbiguousTags);
  }

  // Appending an applet tag can cause the test to hang if Java is blocked on
  // the system.
  tagsToIgnore.push(goog.dom.TagName.APPLET);

  // Appending an embed tag to the page in IE brings up a warning dialog about
  // loading Java content.
  if (goog.userAgent.IE) {
    tagsToIgnore.push(goog.dom.TagName.EMBED);
  }

  for (var tag in goog.dom.TagName) {
    if (goog.array.contains(tagsToIgnore, tag)) {
      continue;
    }

    var el = goog.dom.createElement(tag);
    document.body.appendChild(el);
    var display = goog.style.getCascadedStyle(el, 'display') ||
                  goog.style.getComputedStyle(el, 'display');
    goog.dom.removeNode(el);

    if (goog.editor.node.isBlockTag(el)) {
      assertContains('Display for ' + tag + ' should be block-like',
          display, blockDisplays);
    } else {
      assertNotContains('Display for ' + tag + ' should not be block-like',
          display, blockDisplays);
    }
  }
}

function createDivWithTextNodes(var_args) {
  var dom = goog.dom.createDom('div');
  for (var i = 0; i < arguments.length; i++) {
    goog.dom.appendChild(dom, goog.dom.createTextNode(arguments[i]));
  }
  return dom;
}

function testSkipEmptyTextNodes() {
  assertNull('skipEmptyTextNodes should gracefully handle null',
      goog.editor.node.skipEmptyTextNodes(null));

  var dom1 = createDivWithTextNodes('abc', '', 'xyz', '', '');
  assertEquals('expected not to skip first child', dom1.firstChild,
      goog.editor.node.skipEmptyTextNodes(dom1.firstChild));
  assertEquals('expected to skip second child', dom1.childNodes[2],
      goog.editor.node.skipEmptyTextNodes(dom1.childNodes[1]));
  assertNull('expected to skip all the rest of the children',
      goog.editor.node.skipEmptyTextNodes(dom1.childNodes[3]));
}

function testIsEditableContainer() {
  var editableContainerElement = document.getElementById('editableTest');
  assertTrue('Container element should be considered editable container',
      goog.editor.node.isEditableContainer(editableContainerElement));

  var nonEditableContainerElement = document.getElementById('parentNode');
  assertFalse('Other element should not be considered editable container',
      goog.editor.node.isEditableContainer(nonEditableContainerElement));
}

function testIsEditable() {
  var editableContainerElement = document.getElementById('editableTest');
  var childNode = editableContainerElement.firstChild;
  var childElement = editableContainerElement.getElementsByTagName('span')[0];

  assertFalse('Container element should not be considered editable',
      goog.editor.node.isEditable(editableContainerElement));
  assertTrue('Child text node should be considered editable',
      goog.editor.node.isEditable(childNode));
  assertTrue('Child element should be considered editable',
      goog.editor.node.isEditable(childElement));
  assertTrue('Grandchild node should be considered editable',
      goog.editor.node.isEditable(childElement.firstChild));
  assertFalse('Other element should not be considered editable',
      goog.editor.node.isEditable(document.getElementById('parentNode')));
}

function testFindTopMostEditableAncestor() {
  var root = document.getElementById('editableTest');
  var span = root.getElementsByTagName(goog.dom.TagName.SPAN)[0];
  var textNode = span.firstChild;

  assertEquals('Should return self if self is matched.',
      textNode, goog.editor.node.findTopMostEditableAncestor(textNode,
      function(node) {
        return node.nodeType == goog.dom.NodeType.TEXT;
      }));
  assertEquals('Should not walk out of editable node.',
      null, goog.editor.node.findTopMostEditableAncestor(textNode,
      function(node) {
        return node.tagName == goog.dom.TagName.BODY;
      }));
  assertEquals('Should not match editable container.',
      null, goog.editor.node.findTopMostEditableAncestor(textNode,
      function(node) {
        return node.tagName == goog.dom.TagName.DIV;
      }));
  assertEquals('Should find node in editable container.',
      span, goog.editor.node.findTopMostEditableAncestor(textNode,
      function(node) {
        return node.tagName == goog.dom.TagName.SPAN;
      }));
}

function testSplitDomTreeAt() {
  var innerHTML = '<p>1<b>2</b>3</p>';
  var root = goog.dom.createElement(goog.dom.TagName.DIV);

  root.innerHTML = innerHTML;
  var result = goog.editor.node.splitDomTreeAt(
      root.getElementsByTagName(goog.dom.TagName.B)[0], null, root);
  goog.testing.dom.assertHtmlContentsMatch('<p>1<b>2</b></p>', root);
  goog.testing.dom.assertHtmlContentsMatch('<p>3</p>', result);

  root.innerHTML = innerHTML;
  result = goog.editor.node.splitDomTreeAt(
      root.getElementsByTagName(goog.dom.TagName.B)[0],
      goog.dom.createTextNode('and'),
      root);
  goog.testing.dom.assertHtmlContentsMatch('<p>1<b>2</b></p>', root);
  goog.testing.dom.assertHtmlContentsMatch('<p>and3</p>', result);
}

function testTransferChildren() {
  var prefix = '<b>Bold 1</b>';
  var innerHTML = '<b>Bold</b><ul><li>Item 1</li><li>Item 2</li></ul>';

  var root1 = goog.dom.createElement(goog.dom.TagName.DIV);
  root1.innerHTML = innerHTML;

  var root2 = goog.dom.createElement(goog.dom.TagName.P);
  root2.innerHTML = prefix;

  var b = root1.getElementsByTagName(goog.dom.TagName.B)[0];

  // Transfer the children.
  goog.editor.node.transferChildren(root2, root1);
  assertEquals(0, root1.childNodes.length);
  goog.testing.dom.assertHtmlContentsMatch(prefix + innerHTML, root2);
  assertEquals(b, root2.getElementsByTagName(goog.dom.TagName.B)[1]);

  // Transfer them back.
  goog.editor.node.transferChildren(root1, root2);
  assertEquals(0, root2.childNodes.length);
  goog.testing.dom.assertHtmlContentsMatch(prefix + innerHTML, root1);
  assertEquals(b, root1.getElementsByTagName(goog.dom.TagName.B)[1]);
}