text: function removeWhiteSpaceOnlyTextNodes(node)
  {
    for (var i = 0; i < node.childNodes.length; i++) {
      var child = node.childNodes[i];
      if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim().length == 0) {
        node.removeChild(child);
        i--;
      } else if (child.nodeType === Node.ELEMENT_NODE || child.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
        removeWhiteSpaceOnlyTextNodes(child);
      }
    }
    if (node.shadowRoot) {
      removeWhiteSpaceOnlyTextNodes(node.shadowRoot);
    }
  }
  
  function createTestTree(node) {
  
    let ids = {};
  
    function attachShadowFromTemplate(template) {
      let parent = template.parentNode;
      parent.removeChild(template);
      let shadowRoot;
      if (template.getAttribute('data-slot-assignment') === 'manual') {
         shadowRoot =
            parent.attachShadow({mode: template.getAttribute('data-mode'),
                                 slotAssignment: 'manual'});
      } else {
        shadowRoot = parent.attachShadow(
            {mode: template.getAttribute('data-mode')});
      }
      let id = template.id;
      if (id) {
        shadowRoot.id = id;
        ids[id] = shadowRoot;
      }
      shadowRoot.appendChild(document.importNode(template.content, true));
      return shadowRoot;
    }
  
    function walk(root) {
      if (root.id) {
        ids[root.id] = root;
      }
      for (let e of Array.from(root.querySelectorAll('[id]'))) {
        ids[e.id] = e;
      }
      for (let e of Array.from(root.querySelectorAll('template'))) {
        walk(attachShadowFromTemplate(e));
      }
    }
  
    walk(node.cloneNode(true));
    return ids;
  }
  
  // TODO: Refactor this so that only interested results are recorded.
  // Callers of this function would not be interested in every results.
  function dispatchEventWithLog(nodes, target, event, options) {
  
    function labelFor(e) {
      return e.id || e.tagName;
    }
  
    let log = [];
    let attachedNodes = [];
    for (let label in nodes) {
      let startingNode = nodes[label];
      for (let node = startingNode; node; node = node.parentNode) {
        if (attachedNodes.indexOf(node) >= 0)
          continue;
        let id = node.id;
        if (!id)
          continue;
        attachedNodes.push(node);
        if (options && options.capture) {
          // Record [currentTarget, target, relatedTarget, composedPath(), 'capture' | 'non-capture']
          // TODO: Support registering listeners in different orders.
          // e.g. Register a non-capture listener at first, then register a capture listener.
          node.addEventListener(event.type, (e) => {
            log.push([id,
                      labelFor(e.target),
                      e.relatedTarget ? labelFor(e.relatedTarget) : null,
                      e.composedPath().map((n) => {
                        return labelFor(n);
                      }),
                      'capture']);
          }, true);
          node.addEventListener(event.type, (e) => {
            log.push([id,
                      labelFor(e.target),
                      e.relatedTarget ? labelFor(e.relatedTarget) : null,
                      e.composedPath().map((n) => {
                        return labelFor(n);
                      }),
                      'non-capture']);
          });
        } else {
          // Record [currentTarget, target, relatedTarget, composedPath()]
          node.addEventListener(event.type, (e) => {
            log.push([id,
                      labelFor(e.target),
                      e.relatedTarget ? labelFor(e.relatedTarget) : null,
                      e.composedPath().map((n) => {
                        return labelFor(n);
                      })]
                    );
          });
        }
      }
    }
    target.dispatchEvent(event);
    return log;
  }
  
  // TODO(hayato): Merge this into dispatchEventWithLog
  function dispatchUAEventWithLog(nodes, target, eventType, callback) {
  
    function labelFor(e) {
      return e.id || e.tagName;
    }
  
    let log = [];
    let attachedNodes = [];
    for (let label in nodes) {
      let startingNode = nodes[label];
      for (let node = startingNode; node; node = node.parentNode) {
        if (attachedNodes.indexOf(node) >= 0)
          continue;
        let id = node.id;
        if (!id)
          continue;
        attachedNodes.push(node);
        node.addEventListener(eventType, (e) => {
          // Record [currentTarget, target, relatedTarget, composedPath()]
          log.push([id,
                    labelFor(e.target),
                    e.relatedTarget ? labelFor(e.relatedTarget) : null,
                    e.composedPath().map((n) => {
                      return labelFor(n);
                    })]);
        });
      }
    }
    callback(target);
    return log;
  }
  
  // This function assumes that testharness.js is available.
  function assert_event_path_equals(actual, expected) {
    assert_equals(actual.length, expected.length);
    for (let i = 0; i < actual.length; ++i) {
      assert_equals(actual[i].length, expected[i].length);
      assert_equals(actual[i][0], expected[i][0], 'currentTarget at ' + i + ' should be same');
      assert_equals(actual[i][1], expected[i][1], 'target at ' + i + ' should be same');
      assert_equals(actual[i][2], expected[i][2], 'relatedTarget at ' + i + ' should be same');
      assert_array_equals(actual[i][3], expected[i][3], 'composedPath at ' + i + ' should be same');
      if (actual[i][4]) {
        assert_equals(actual[i][4], expected[i][4], 'listener type should be same at ' + i);
      }
    }
  }
  