tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_range.js (14838B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const UNORDERED_TYPE = 8; // XPathResult.ANY_UNORDERED_NODE_TYPE
      6 
      7 /**
      8 * Determine if the data node has only ignorable white-space.
      9 *
     10 * @return NodeFilter.FILTER_SKIP if it does.
     11 * @return NodeFilter.FILTER_ACCEPT otherwise.
     12 */
     13 function isWhitespace(aNode) {
     14  return /\S/.test(aNode.nodeValue)
     15    ? NodeFilter.FILTER_SKIP
     16    : NodeFilter.FILTER_ACCEPT;
     17 }
     18 
     19 /**
     20 * Create a DocumentFragment with cloned children equaling a node's children.
     21 *
     22 * @param aNode The node to copy from.
     23 *
     24 * @return DocumentFragment node.
     25 */
     26 function getFragment(aNode) {
     27  var frag = aNode.ownerDocument.createDocumentFragment();
     28  for (var i = 0; i < aNode.childNodes.length; i++) {
     29    frag.appendChild(aNode.childNodes.item(i).cloneNode(true));
     30  }
     31  return frag;
     32 }
     33 
     34 // Goodies from head_content.js
     35 const parser = getParser();
     36 
     37 /**
     38 * Translate an XPath to a DOM node. This method uses a document
     39 * fragment as context node.
     40 *
     41 * @param aContextNode The context node to apply the XPath to.
     42 * @param aPath        The XPath to use.
     43 *
     44 * @return Node  The target node retrieved from the XPath.
     45 */
     46 function evalXPathInDocumentFragment(aContextNode, aPath) {
     47  Assert.equal(ChromeUtils.getClassName(aContextNode), "DocumentFragment");
     48  Assert.ok(aContextNode.childNodes.length);
     49  if (aPath == ".") {
     50    return aContextNode;
     51  }
     52 
     53  // Separate the fragment's xpath lookup from the rest.
     54  var firstSlash = aPath.indexOf("/");
     55  if (firstSlash == -1) {
     56    firstSlash = aPath.length;
     57  }
     58  var prefix = aPath.substr(0, firstSlash);
     59  var realPath = aPath.substr(firstSlash + 1);
     60  if (!realPath) {
     61    realPath = ".";
     62  }
     63 
     64  // Set up a special node filter to look among the fragment's child nodes.
     65  var childIndex = 1;
     66  var bracketIndex = prefix.indexOf("[");
     67  if (bracketIndex != -1) {
     68    childIndex = Number(
     69      prefix.substring(bracketIndex + 1, prefix.indexOf("]"))
     70    );
     71    Assert.greater(childIndex, 0);
     72    prefix = prefix.substr(0, bracketIndex);
     73  }
     74 
     75  var targetType = NodeFilter.SHOW_ELEMENT;
     76  var targetNodeName = prefix;
     77  if (prefix.indexOf("processing-instruction(") == 0) {
     78    targetType = NodeFilter.SHOW_PROCESSING_INSTRUCTION;
     79    targetNodeName = prefix.substring(
     80      prefix.indexOf("(") + 2,
     81      prefix.indexOf(")") - 1
     82    );
     83  }
     84  switch (prefix) {
     85    case "text()":
     86      targetType = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION;
     87      targetNodeName = null;
     88      break;
     89    case "comment()":
     90      targetType = NodeFilter.SHOW_COMMENT;
     91      targetNodeName = null;
     92      break;
     93    case "node()":
     94      targetType = NodeFilter.SHOW_ALL;
     95      targetNodeName = null;
     96  }
     97 
     98  var filter = {
     99    count: 0,
    100 
    101    // NodeFilter
    102    acceptNode: function acceptNode(aNode) {
    103      if (aNode.parentNode != aContextNode) {
    104        // Don't bother looking at kids either.
    105        return NodeFilter.FILTER_REJECT;
    106      }
    107 
    108      if (targetNodeName && targetNodeName != aNode.nodeName) {
    109        return NodeFilter.FILTER_SKIP;
    110      }
    111 
    112      this.count++;
    113      if (this.count != childIndex) {
    114        return NodeFilter.FILTER_SKIP;
    115      }
    116 
    117      return NodeFilter.FILTER_ACCEPT;
    118    },
    119  };
    120 
    121  // Look for the node matching the step from the document fragment.
    122  var walker = aContextNode.ownerDocument.createTreeWalker(
    123    aContextNode,
    124    targetType,
    125    filter
    126  );
    127  var targetNode = walker.nextNode();
    128  Assert.notEqual(targetNode, null);
    129 
    130  // Apply our remaining xpath to the found node.
    131  var expr = aContextNode.ownerDocument.createExpression(realPath, null);
    132  var result = expr.evaluate(targetNode, UNORDERED_TYPE, null);
    133  return result.singleNodeValue;
    134 }
    135 
    136 /**
    137 * Get a DOM range corresponding to the test's source node.
    138 *
    139 * @param aSourceNode <source/> element with range information.
    140 * @param aFragment   DocumentFragment generated with getFragment().
    141 *
    142 * @return Range object.
    143 */
    144 function getRange(aSourceNode, aFragment) {
    145  Assert.ok(Element.isInstance(aSourceNode));
    146  Assert.equal(ChromeUtils.getClassName(aFragment), "DocumentFragment");
    147  var doc = aSourceNode.ownerDocument;
    148 
    149  var containerPath = aSourceNode.getAttribute("startContainer");
    150  var startContainer = evalXPathInDocumentFragment(aFragment, containerPath);
    151  var startOffset = Number(aSourceNode.getAttribute("startOffset"));
    152 
    153  containerPath = aSourceNode.getAttribute("endContainer");
    154  var endContainer = evalXPathInDocumentFragment(aFragment, containerPath);
    155  var endOffset = Number(aSourceNode.getAttribute("endOffset"));
    156 
    157  var range = doc.createRange();
    158  range.setStart(startContainer, startOffset);
    159  range.setEnd(endContainer, endOffset);
    160  return range;
    161 }
    162 
    163 /**
    164 * Get the document for a given path, and clean it up for our tests.
    165 *
    166 * @param aPath The path to the local document.
    167 */
    168 function getParsedDocument(aPath) {
    169  return do_parse_document(aPath, "application/xml").then(
    170    processParsedDocument
    171  );
    172 }
    173 
    174 function processParsedDocument(doc) {
    175  Assert.notEqual(doc.documentElement.localName, "parsererror");
    176  Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");
    177 
    178  // Clean out whitespace.
    179  var walker = doc.createTreeWalker(
    180    doc,
    181    NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION,
    182    isWhitespace
    183  );
    184  while (walker.nextNode()) {
    185    var parent = walker.currentNode.parentNode;
    186    parent.removeChild(walker.currentNode);
    187    walker.currentNode = parent;
    188  }
    189 
    190  // Clean out mandatory splits between nodes.
    191  var splits = doc.getElementsByTagName("split");
    192  var i;
    193  for (i = splits.length - 1; i >= 0; i--) {
    194    let node = splits.item(i);
    195    node.remove();
    196  }
    197  splits = null;
    198 
    199  // Replace empty CDATA sections.
    200  var emptyData = doc.getElementsByTagName("empty-cdata");
    201  for (i = emptyData.length - 1; i >= 0; i--) {
    202    let node = emptyData.item(i);
    203    var cdata = doc.createCDATASection("");
    204    node.parentNode.replaceChild(cdata, node);
    205  }
    206 
    207  return doc;
    208 }
    209 
    210 /**
    211 * Run the extraction tests.
    212 */
    213 function run_extract_test() {
    214  var filePath = "test_delete_range.xml";
    215  getParsedDocument(filePath).then(do_extract_test);
    216 }
    217 
    218 function do_extract_test(doc) {
    219  var tests = doc.getElementsByTagName("test");
    220 
    221  // Run our deletion, extraction tests.
    222  for (var i = 0; i < tests.length; i++) {
    223    dump("Configuring for test " + i + "\n");
    224    var currentTest = tests.item(i);
    225 
    226    // Validate the test is properly formatted for what this harness expects.
    227    var baseSource = currentTest.firstChild;
    228    Assert.equal(baseSource.nodeName, "source");
    229    var baseResult = baseSource.nextSibling;
    230    Assert.equal(baseResult.nodeName, "result");
    231    var baseExtract = baseResult.nextSibling;
    232    Assert.equal(baseExtract.nodeName, "extract");
    233    Assert.equal(baseExtract.nextSibling, null);
    234 
    235    /* We do all our tests on DOM document fragments, derived from the test
    236       element's children.  This lets us rip the various fragments to shreds,
    237       while preserving the original elements so we can make more copies of
    238       them.
    239 
    240       After the range's extraction or deletion is done, we use
    241       Node.isEqualNode() between the altered source fragment and the
    242       result fragment.  We also run isEqualNode() between the extracted
    243       fragment and the fragment from the baseExtract node.  If they are not
    244       equal, we have failed a test.
    245 
    246       We also have to ensure the original nodes on the end points of the
    247       range are still in the source fragment.  This is bug 332148.  The nodes
    248       may not be replaced with equal but separate nodes.  The range extraction
    249       may alter these nodes - in the case of text containers, they will - but
    250       the nodes must stay there, to preserve references such as user data,
    251       event listeners, etc.
    252 
    253       First, an extraction test.
    254     */
    255 
    256    var resultFrag = getFragment(baseResult);
    257    var extractFrag = getFragment(baseExtract);
    258 
    259    dump("Extract contents test " + i + "\n\n");
    260    var baseFrag = getFragment(baseSource);
    261    var baseRange = getRange(baseSource, baseFrag);
    262    var startContainer = baseRange.startContainer;
    263    var endContainer = baseRange.endContainer;
    264 
    265    var cutFragment = baseRange.extractContents();
    266    dump("cutFragment: " + cutFragment + "\n");
    267    if (cutFragment) {
    268      Assert.ok(extractFrag.isEqualNode(cutFragment));
    269    } else {
    270      Assert.equal(extractFrag.firstChild, null);
    271    }
    272    Assert.ok(baseFrag.isEqualNode(resultFrag));
    273 
    274    dump("Ensure the original nodes weren't extracted - test " + i + "\n\n");
    275    var walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
    276    var foundStart = false;
    277    var foundEnd = false;
    278    do {
    279      if (walker.currentNode == startContainer) {
    280        foundStart = true;
    281      }
    282 
    283      if (walker.currentNode == endContainer) {
    284        // An end container node should not come before the start container node.
    285        Assert.ok(foundStart);
    286        foundEnd = true;
    287        break;
    288      }
    289    } while (walker.nextNode());
    290    Assert.ok(foundEnd);
    291 
    292    /* Now, we reset our test for the deleteContents case.  This one differs
    293       from the extractContents case only in that there is no extracted document
    294       fragment to compare against.  So we merely compare the starting fragment,
    295       minus the extracted content, against the result fragment.
    296     */
    297    dump("Delete contents test " + i + "\n\n");
    298    baseFrag = getFragment(baseSource);
    299    baseRange = getRange(baseSource, baseFrag);
    300    startContainer = baseRange.startContainer;
    301    endContainer = baseRange.endContainer;
    302    baseRange.deleteContents();
    303    Assert.ok(baseFrag.isEqualNode(resultFrag));
    304 
    305    dump("Ensure the original nodes weren't deleted - test " + i + "\n\n");
    306    walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
    307    foundStart = false;
    308    foundEnd = false;
    309    do {
    310      if (walker.currentNode == startContainer) {
    311        foundStart = true;
    312      }
    313 
    314      if (walker.currentNode == endContainer) {
    315        // An end container node should not come before the start container node.
    316        Assert.ok(foundStart);
    317        foundEnd = true;
    318        break;
    319      }
    320    } while (walker.nextNode());
    321    Assert.ok(foundEnd);
    322 
    323    // Clean up after ourselves.
    324    walker = null;
    325  }
    326 }
    327 
    328 /**
    329 * Miscellaneous tests not covered above.
    330 */
    331 function run_miscellaneous_tests() {
    332  var filePath = "test_delete_range.xml";
    333  getParsedDocument(filePath).then(do_miscellaneous_tests);
    334 }
    335 
    336 function isText(node) {
    337  return (
    338    node.nodeType == node.TEXT_NODE || node.nodeType == node.CDATA_SECTION_NODE
    339  );
    340 }
    341 
    342 function do_miscellaneous_tests(doc) {
    343  var tests = doc.getElementsByTagName("test");
    344 
    345  // Let's try some invalid inputs to our DOM range and see what happens.
    346  var currentTest = tests.item(0);
    347  var baseSource = currentTest.firstChild;
    348 
    349  var baseFrag = getFragment(baseSource);
    350 
    351  var baseRange = getRange(baseSource, baseFrag);
    352  var startContainer = baseRange.startContainer;
    353  var endContainer = baseRange.endContainer;
    354  var startOffset = baseRange.startOffset;
    355  var endOffset = baseRange.endOffset;
    356 
    357  // Text range manipulation.
    358  if (
    359    endOffset > startOffset &&
    360    startContainer == endContainer &&
    361    isText(startContainer)
    362  ) {
    363    // Invalid start node
    364    try {
    365      baseRange.setStart(null, 0);
    366      do_throw("Should have thrown NOT_OBJECT_ERR!");
    367    } catch (e) {
    368      Assert.equal(e.constructor.name, "TypeError");
    369    }
    370 
    371    // Invalid start node
    372    try {
    373      baseRange.setStart({}, 0);
    374      do_throw("Should have thrown SecurityError!");
    375    } catch (e) {
    376      Assert.equal(e.constructor.name, "TypeError");
    377    }
    378 
    379    // Invalid index
    380    try {
    381      baseRange.setStart(startContainer, -1);
    382      do_throw("Should have thrown IndexSizeError!");
    383    } catch (e) {
    384      Assert.equal(e.name, "IndexSizeError");
    385    }
    386 
    387    // Invalid index
    388    var newOffset = isText(startContainer)
    389      ? startContainer.nodeValue.length + 1
    390      : startContainer.childNodes.length + 1;
    391    try {
    392      baseRange.setStart(startContainer, newOffset);
    393      do_throw("Should have thrown IndexSizeError!");
    394    } catch (e) {
    395      Assert.equal(e.name, "IndexSizeError");
    396    }
    397 
    398    newOffset--;
    399    // Valid index
    400    baseRange.setStart(startContainer, newOffset);
    401    Assert.equal(baseRange.startContainer, baseRange.endContainer);
    402    Assert.equal(baseRange.startOffset, newOffset);
    403    Assert.ok(baseRange.collapsed);
    404 
    405    // Valid index
    406    baseRange.setEnd(startContainer, 0);
    407    Assert.equal(baseRange.startContainer, baseRange.endContainer);
    408    Assert.equal(baseRange.startOffset, 0);
    409    Assert.ok(baseRange.collapsed);
    410  } else {
    411    do_throw(
    412      "The first test should be a text-only range test.  Test is invalid."
    413    );
    414  }
    415 
    416  /* See what happens when a range has a startContainer in one fragment, and an
    417     endContainer in another.  According to the DOM spec, section 2.4, the range
    418     should collapse to the new container and offset. */
    419  baseRange = getRange(baseSource, baseFrag);
    420  startContainer = baseRange.startContainer;
    421  startOffset = baseRange.startOffset;
    422  endContainer = baseRange.endContainer;
    423  endOffset = baseRange.endOffset;
    424 
    425  dump("External fragment test\n\n");
    426 
    427  var externalTest = tests.item(1);
    428  var externalSource = externalTest.firstChild;
    429  var externalFrag = getFragment(externalSource);
    430  var externalRange = getRange(externalSource, externalFrag);
    431 
    432  baseRange.setEnd(externalRange.endContainer, 0);
    433  Assert.equal(baseRange.startContainer, externalRange.endContainer);
    434  Assert.equal(baseRange.startOffset, 0);
    435  Assert.ok(baseRange.collapsed);
    436 
    437  /*
    438  // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false?
    439  do_check_false(baseRange.isPointInRange(startContainer, startOffset));
    440  do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1));
    441  do_check_false(baseRange.isPointInRange(endContainer, endOffset));
    442  */
    443 
    444  // Requested by smaug:  A range involving a comment as a document child.
    445  doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml");
    446  Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");
    447  Assert.equal(doc.childNodes.length, 2);
    448  baseRange = doc.createRange();
    449  baseRange.setStart(doc.firstChild, 1);
    450  baseRange.setEnd(doc.firstChild, 2);
    451  var frag = baseRange.extractContents();
    452  Assert.equal(frag.childNodes.length, 1);
    453  Assert.equal(ChromeUtils.getClassName(frag.firstChild), "Comment");
    454  Assert.equal(frag.firstChild.nodeType, frag.COMMENT_NODE);
    455  Assert.equal(frag.firstChild.nodeValue, "f");
    456 
    457  /* smaug also requested attribute tests.  Sadly, those are not yet supported
    458     in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775.
    459   */
    460 }
    461 
    462 function run_test() {
    463  run_extract_test();
    464  run_miscellaneous_tests();
    465 }