From 76ba117f5ae7b73558c84ab80ac3fe612181585c Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 5 Jan 2015 21:57:10 +1100 Subject: [PATCH 1/2] Fixing issue #126 and adding unit tests for it. --- src/js/AuditRule.js | 76 +++++++++++++++----------------- test/index.html | 1 + test/js/audit-rule-test.js | 90 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 test/js/audit-rule-test.js diff --git a/src/js/AuditRule.js b/src/js/AuditRule.js index 19c3e5e3..50870334 100644 --- a/src/js/AuditRule.js +++ b/src/js/AuditRule.js @@ -134,55 +134,49 @@ axs.AuditRule.collectMatchingElements = function(node, matcher, collection, if (element && matcher.call(null, element)) collection.push(element); - // Descend into node: - // If it has a ShadowRoot, ignore all child elements - these will be picked - // up by the or elements. Descend straight into the - // ShadowRoot. + // Descend into node if (element) { - var shadowRoot = element.shadowRoot || element.webkitShadowRoot; - if (shadowRoot) { - axs.AuditRule.collectMatchingElements(shadowRoot, - matcher, - collection, - shadowRoot); + if (element.localName == 'content') { + // If it is a element, descend into distributed elements - descend + // into distributed elements - these are elements from outside the shadow + // root which are rendered inside the shadow DOM. + var content = /** @type {HTMLContentElement} */ (element); + var distributedNodes = content.getDistributedNodes(); + for (var i = 0; i < distributedNodes.length; i++) { + axs.AuditRule.collectMatchingElements(distributedNodes[i], + matcher, + collection, + opt_shadowRoot); + } + return; + } else if (element.localName == 'shadow') { + // If it is a element, descend into the olderShadowRoot of the + // current ShadowRoot. + var shadow = /** @type {HTMLShadowElement} */ (element); + if (!opt_shadowRoot) { + console.warn('ShadowRoot not provided for', element); + } else { + var olderShadowRoot = opt_shadowRoot.olderShadowRoot || + shadow.olderShadowRoot; + if (olderShadowRoot) { + axs.AuditRule.collectMatchingElements(olderShadowRoot, + matcher, + collection, + olderShadowRoot); + } + } return; - } - } - - // If it is a element, descend into distributed elements - descend - // into distributed elements - these are elements from outside the shadow - // root which are rendered inside the shadow DOM. - if (element && element.localName == 'content') { - var content = /** @type {HTMLContentElement} */ (element); - var distributedNodes = content.getDistributedNodes(); - for (var i = 0; i < distributedNodes.length; i++) { - axs.AuditRule.collectMatchingElements(distributedNodes[i], - matcher, - collection, - opt_shadowRoot); - } - return; - } - - // If it is a element, descend into the olderShadowRoot of the - // current ShadowRoot. - if (element && element.localName == 'shadow') { - var shadow = /** @type {HTMLShadowElement} */ (element); - if (!opt_shadowRoot) { - console.warn('ShadowRoot not provided for', element); } else { - var olderShadowRoot = opt_shadowRoot.olderShadowRoot || - shadow.olderShadowRoot; - if (olderShadowRoot) { - axs.AuditRule.collectMatchingElements(olderShadowRoot, + var shadowRoot = element.shadowRoot || element.webkitShadowRoot; + if (shadowRoot) { + axs.AuditRule.collectMatchingElements(shadowRoot, matcher, collection, - olderShadowRoot); + shadowRoot); } } - return; + } - // If it is neither the parent of a ShadowRoot, a element, nor // a element recurse normally. var child = node.firstChild; diff --git a/test/index.html b/test/index.html index d226e5a4..00025b52 100644 --- a/test/index.html +++ b/test/index.html @@ -40,6 +40,7 @@ + diff --git a/test/js/audit-rule-test.js b/test/js/audit-rule-test.js new file mode 100644 index 00000000..f2106c39 --- /dev/null +++ b/test/js/audit-rule-test.js @@ -0,0 +1,90 @@ +// Copyright 2014 Google Inc. +// +// 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. +(function(){ + module("collectMatchingElements"); + + var DIV_COUNT = 10; + function matcher(element) { + var tagName = element.tagName; + if (!tagName) + return false; + return (tagName.toLowerCase() === "div" && element.classList.contains("test")); + } + + function buildTestDom() { + var result = document.createDocumentFragment(); + result = result.appendChild(document.createElement("div")); + for (var i = 0; i < DIV_COUNT; i++) { + var element = document.createElement("div"); + element.className = "test"; + result.appendChild(element); + } + return result; + } + + test("Simple DOM", function () { + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var matched = []; + axs.AuditRule.collectMatchingElements(container, matcher, matched); + equal(matched.length, DIV_COUNT); + }); + + test("With shadow DOM", function () { + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var wrapper = container.firstElementChild; + if (wrapper.createShadowRoot) { + var matched = []; + wrapper.createShadowRoot(); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + equal(matched.length, DIV_COUNT); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("Nodes within shadow DOM", function () { + var container = document.getElementById('qunit-fixture'); + var wrapper = container.appendChild(document.createElement("div")); + if (wrapper.createShadowRoot) { + var root = wrapper.createShadowRoot(); + root.appendChild(buildTestDom()); + var matched = []; + axs.AuditRule.collectMatchingElements(container, matcher, matched); + equal(matched.length, DIV_COUNT); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("Nodes within DOM and shadow DOM", function () { + var container = document.getElementById('qunit-fixture'); + var wrapper = container.appendChild(document.createElement("div")); + if (wrapper.createShadowRoot) { + var root = wrapper.createShadowRoot(); + root.appendChild(buildTestDom()); + var matched = []; + wrapper.appendChild(buildTestDom()); + axs.AuditRule.collectMatchingElements(container, matcher, matched); + equal(matched.length, (DIV_COUNT * 2)); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + +})(); From a868f5da9a81a607104a7e1826ace43d6cba2425 Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Tue, 6 Jan 2015 16:01:38 +1100 Subject: [PATCH 2/2] Rework tests and fix actual issue --- src/js/AuditRule.js | 73 +++++++++-------- src/js/externs/externs.js | 5 +- test/js/audit-rule-test.js | 155 +++++++++++++++++++++++++++++++++++-- 3 files changed, 190 insertions(+), 43 deletions(-) diff --git a/src/js/AuditRule.js b/src/js/AuditRule.js index 50870334..af0f7da6 100644 --- a/src/js/AuditRule.js +++ b/src/js/AuditRule.js @@ -134,48 +134,53 @@ axs.AuditRule.collectMatchingElements = function(node, matcher, collection, if (element && matcher.call(null, element)) collection.push(element); - // Descend into node + // Descend into node: + // If it has a ShadowRoot, ignore all child elements - these will be picked + // up by the or elements. Descend straight into the + // ShadowRoot. if (element) { - if (element.localName == 'content') { - // If it is a element, descend into distributed elements - descend - // into distributed elements - these are elements from outside the shadow - // root which are rendered inside the shadow DOM. - var content = /** @type {HTMLContentElement} */ (element); - var distributedNodes = content.getDistributedNodes(); + // NOTE: grunt qunit DOES NOT support Shadow DOM, so if changing this + // code, be sure to run the tests in the browser before committing. + var shadowRoot = element.shadowRoot || element.webkitShadowRoot; + if (shadowRoot) { + axs.AuditRule.collectMatchingElements(shadowRoot, + matcher, + collection, + shadowRoot); + return; + } + } + + // If it is a element, descend into distributed elements - descend + // into distributed elements - these are elements from outside the shadow + // root which are rendered inside the shadow DOM. + if (element && element.localName == 'content') { + var content = /** @type {HTMLContentElement} */ (element); + var distributedNodes = content.getDistributedNodes(); + for (var i = 0; i < distributedNodes.length; i++) { + axs.AuditRule.collectMatchingElements(distributedNodes[i], + matcher, + collection, + opt_shadowRoot); + } + return; + } + + // If it is a element, descend into the olderShadowRoot of the + // current ShadowRoot. + if (element && element.localName == 'shadow') { + var shadow = /** @type {HTMLShadowElement} */ (element); + if (!opt_shadowRoot) { + console.warn('ShadowRoot not provided for', element); + } else { + var distributedNodes = shadow.getDistributedNodes(); for (var i = 0; i < distributedNodes.length; i++) { axs.AuditRule.collectMatchingElements(distributedNodes[i], matcher, collection, opt_shadowRoot); } - return; - } else if (element.localName == 'shadow') { - // If it is a element, descend into the olderShadowRoot of the - // current ShadowRoot. - var shadow = /** @type {HTMLShadowElement} */ (element); - if (!opt_shadowRoot) { - console.warn('ShadowRoot not provided for', element); - } else { - var olderShadowRoot = opt_shadowRoot.olderShadowRoot || - shadow.olderShadowRoot; - if (olderShadowRoot) { - axs.AuditRule.collectMatchingElements(olderShadowRoot, - matcher, - collection, - olderShadowRoot); - } - } - return; - } else { - var shadowRoot = element.shadowRoot || element.webkitShadowRoot; - if (shadowRoot) { - axs.AuditRule.collectMatchingElements(shadowRoot, - matcher, - collection, - shadowRoot); - } } - } // If it is neither the parent of a ShadowRoot, a element, nor // a element recurse normally. diff --git a/src/js/externs/externs.js b/src/js/externs/externs.js index acb409b1..00b680e4 100644 --- a/src/js/externs/externs.js +++ b/src/js/externs/externs.js @@ -43,10 +43,9 @@ HTMLContentElement.prototype.getDistributedNodes = function() {}; function HTMLShadowElement() {} /** - * Note: this is an out of date model, but still used in practice sometimes. - * @type {ShadowRoot} + * @return {Array.} */ -HTMLShadowElement.prototype.olderShadowRoot; +HTMLShadowElement.prototype.getDistributedNodes = function() {}; /** * Note: will be deprecated at some point; prefer shadowRoot if it exists. diff --git a/test/js/audit-rule-test.js b/test/js/audit-rule-test.js index f2106c39..63ed5a68 100644 --- a/test/js/audit-rule-test.js +++ b/test/js/audit-rule-test.js @@ -41,14 +41,51 @@ equal(matched.length, DIV_COUNT); }); - test("With shadow DOM", function () { + test("With shadow DOM with no content insertion point", function () { var container = document.getElementById('qunit-fixture'); container.appendChild(buildTestDom()); var wrapper = container.firstElementChild; if (wrapper.createShadowRoot) { var matched = []; - wrapper.createShadowRoot(); + var root = wrapper.createShadowRoot(); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + equal(matched.length, 0); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("With shadow DOM with content element", function () { + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var wrapper = container.firstElementChild; + if (wrapper.createShadowRoot) { + var matched = []; + var root = wrapper.createShadowRoot(); + var content = document.createElement('content'); + root.appendChild(content); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // picks up content + equal(matched.length, DIV_COUNT); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("With shadow DOM with shadow element", function () { + console.log("With shadow DOM with shadow element"); + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var wrapper = container.firstElementChild; + if (wrapper.createShadowRoot) { + var matched = []; + var root = wrapper.createShadowRoot(); + var shadow = document.createElement('shadow'); + root.appendChild(shadow); axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // picks up content equal(matched.length, DIV_COUNT); } else { console.warn("Test platform does not support shadow DOM"); @@ -64,6 +101,7 @@ root.appendChild(buildTestDom()); var matched = []; axs.AuditRule.collectMatchingElements(container, matcher, matched); + // Nodes in shadows are found equal(matched.length, DIV_COUNT); } else { console.warn("Test platform does not support shadow DOM"); @@ -71,20 +109,125 @@ } }); - test("Nodes within DOM and shadow DOM", function () { + test("With shadow DOM with olderShadowRoot but no shadow element", function () { var container = document.getElementById('qunit-fixture'); - var wrapper = container.appendChild(document.createElement("div")); + var wrapper = container.appendChild(document.createElement('div')); if (wrapper.createShadowRoot) { + var matched = []; + var olderShadowRoot = wrapper.createShadowRoot(); + olderShadowRoot.appendChild(buildTestDom()); var root = wrapper.createShadowRoot(); - root.appendChild(buildTestDom()); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // New shadow root hides older shadow root + equal(matched.length, 0); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("With shadow DOM with olderShadowRoot and shadow element", function () { + var container = document.getElementById('qunit-fixture'); + var wrapper = container.appendChild(document.createElement('div')); + if (wrapper.createShadowRoot) { var matched = []; + var olderShadowRoot = wrapper.createShadowRoot(); + olderShadowRoot.appendChild(buildTestDom()); + var root = wrapper.createShadowRoot(); + var shadow = document.createElement('shadow'); + root.appendChild(shadow); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // element picks up content from olderShadowRoot + equal(matched.length, DIV_COUNT); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("With shadow DOM with olderShadowRoot, shadow element and child nodes but no content element", function () { + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var wrapper = container.firstElementChild; + if (wrapper.createShadowRoot) { + var matched = []; + var olderShadowRoot = wrapper.createShadowRoot(); + var olderShadowContent = document.createElement('div'); + olderShadowContent.className = 'test'; + olderShadowRoot.appendChild(olderShadowContent); + var root = wrapper.createShadowRoot(); + var shadow = document.createElement('shadow'); + root.appendChild(shadow); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // element picks up content from olderShadowRoot only + equal(matched.length, 1); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("With shadow DOM with olderShadowRoot, shadow element and child nodes and content element", function () { + var container = document.getElementById('qunit-fixture'); + container.appendChild(buildTestDom()); + var wrapper = container.firstElementChild; + if (wrapper.createShadowRoot) { + var matched = []; + var olderShadowRoot = wrapper.createShadowRoot(); + var olderShadowContent = document.createElement('div'); + olderShadowContent.className = 'test'; + olderShadowRoot.appendChild(olderShadowContent); + var root = wrapper.createShadowRoot(); + var shadow = document.createElement('shadow'); + root.appendChild(shadow); + var content = document.createElement('content'); + root.appendChild(content); + axs.AuditRule.collectMatchingElements(wrapper, matcher, matched); + // element picks up content from olderShadowRoot and child nodes + equal(matched.length, DIV_COUNT + 1); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); + + test("Nodes within DOM and shadow DOM - no content distribution point", function () { + var container = document.getElementById('qunit-fixture'); + var wrapper = container.appendChild(document.createElement("div")); + if (wrapper.createShadowRoot) { + var root = wrapper.createShadowRoot(); + var rootContent = document.createElement('div'); + rootContent.className = 'test'; + root.appendChild(rootContent); wrapper.appendChild(buildTestDom()); + var matched = []; axs.AuditRule.collectMatchingElements(container, matcher, matched); - equal(matched.length, (DIV_COUNT * 2)); + // Nodes in light dom are not distributed + equal(matched.length, 1); } else { console.warn("Test platform does not support shadow DOM"); ok(true); } }); + test("Nodes within DOM and shadow DOM with content element", function () { + var container = document.getElementById('qunit-fixture'); + var wrapper = container.appendChild(document.createElement("div")); + wrapper.appendChild(buildTestDom()); + if (wrapper.createShadowRoot) { + var root = wrapper.createShadowRoot(); + var rootContent = document.createElement('div'); + rootContent.className = 'test'; + root.appendChild(rootContent); + var content = document.createElement('content'); + root.appendChild(content); + var matched = []; + axs.AuditRule.collectMatchingElements(container, matcher, matched); + // Nodes in light dom are distributed into content element. + equal(matched.length, (DIV_COUNT + 1)); + } else { + console.warn("Test platform does not support shadow DOM"); + ok(true); + } + }); })();