diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/shadow-dom | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/shadow-dom')
235 files changed, 18127 insertions, 0 deletions
diff --git a/testing/web-platform/tests/shadow-dom/Document-prototype-adoptNode.html b/testing/web-platform/tests/shadow-dom/Document-prototype-adoptNode.html new file mode 100644 index 0000000000..6afd603618 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Document-prototype-adoptNode.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<title>DOM and Shadow DOM: Document.prototype.adoptNode</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="The adoptNode(node) method must throw a HierarchyRequestError exception if node is a shadow root."> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-document-adoptnode"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +function testAdoptNode(mode) { + test(function () { + var newDocument = document.implementation.createHTMLDocument(); + assert_throws_dom('HierarchyRequestError', function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: mode}); + newDocument.adoptNode(shadowRoot); + }); + }, 'adoptNode on a shadow root in ' + mode + ' mode must throw a HierarchyRequestError'); +} + +testAdoptNode('open'); +testAdoptNode('closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Document-prototype-currentScript.html b/testing/web-platform/tests/shadow-dom/Document-prototype-currentScript.html new file mode 100644 index 0000000000..176ef348fc --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Document-prototype-currentScript.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset=utf-8> +<title>HTML: Document.prototype.currentScript</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="If the script element is in a document, then set the script element's node document's currentScript attribute to the script element."> +<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-block"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script id="outerScriptElement"> + +function assert_shadowdom_supported() { + assert_true('attachShadow' in document.createElement('div'), 'Shadow DOM is not supported'); +} + +var outerScriptElement = document.currentScript; + +function testInlineScript(mode) +{ + test(function () { + assert_shadowdom_supported(); + var host = document.createElement('div'); + var shadowRoot = host.attachShadow({mode: mode}); + var scriptElement = document.createElement('script'); + scriptElement.textContent = 'assert_equals(document.currentScript, null)'; + shadowRoot.appendChild(scriptElement); + + assert_equals(document.currentScript, outerScriptElement, + 'document.currentScript must be set to the currently excusing script element in a document tree before executing a script in a shadow tree'); + document.body.appendChild(host); + assert_equals(document.currentScript, outerScriptElement, + 'document.currentScript must be set to the currently excusing script element in a document tree after executing a script in a shadow tree'); + + }, 'document.currentScript must not to be set to a script element in a shadow tree in ' + mode + ' mode'); +} + +testInlineScript('open'); +testInlineScript('closed'); + +var executeExternalScript = null; +var testedScriptElement = null; +function executeNextTest() +{ + var testCase = asyncScriptTests.shift(); + if (!testCase) + return; + + var mode = testCase.mode; + + testCase.test.add_cleanup(() => { + setTimeout(executeNextTest, 1); + }); + testCase.test.step(function () { + assert_shadowdom_supported(); + testedScriptElement = document.createElement('script'); + testedScriptElement.src = 'resources/Document-prototype-currentScript-helper.js'; + + if (mode !== null) { + var host = document.createElement('div'); + var shadowRoot = host.attachShadow({mode: mode}); + shadowRoot.appendChild(testedScriptElement); + document.body.appendChild(host); + } else { + document.body.appendChild(testedScriptElement); + } + + if (testCase.remove) + testedScriptElement.parentNode.removeChild(testedScriptElement); + }); + + executeExternalScript = function () { + testCase.test.step(function () { + assert_equals(document.currentScript, testCase.expected()); + }); + testCase.test.done(); + } +} + +var asyncScriptTests = [ + { + test: async_test('document.currentScript must be set to a script element that loads an external script in a document tree'), + mode: null, remove: false, expected: function () { return testedScriptElement; }}, + { + test: async_test('document.currentScript must be set to a script element that loads an external script in a document tree (2)'), + mode: null, remove: true, expected: function () { return testedScriptElement; }}, + { + test: async_test('document.currentScript must not be set to a script element that loads an external script in an open shadow tree'), + mode: 'open', remove: false, expected: function () { return null; }}, + { + test: async_test('document.currentScript must not be set to a script element that loads an external script in a closed shadow tree'), + mode: 'closed', remove: false, expected: function () { return null; }}, + { + test: async_test('document.currentScript must be set to a script element that loads an external script that was in an open shadow tree and then removed'), + mode: 'open', remove: true, expected: function () { return testedScriptElement; }}, + { + test: async_test('document.currentScript must be set to a script element that loads an external script that was in a closed shadow tree and then removed'), + mode: 'closed', remove: true, expected: function () { return testedScriptElement; }}, +]; + +executeNextTest(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Document-prototype-importNode.html b/testing/web-platform/tests/shadow-dom/Document-prototype-importNode.html new file mode 100644 index 0000000000..f71f8dc6c5 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Document-prototype-importNode.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<title>DOM and Shadow DOM: Document.prototype.importNode</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="The importNode(node, deep) method must throw a NotSupportedError exception if node is a shadow root."> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-document-importnode"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +function testImportNode(mode) { + test(function () { + var newDocument = document.implementation.createHTMLDocument(); + assert_throws_dom('NotSupportedError', function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: mode}); + newDocument.importNode(shadowRoot); + }); + }, 'importNode on a shadow root in ' + mode + ' mode must throw a NotSupportedError'); +} + +testImportNode('open'); +testImportNode('closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/DocumentOrShadowRoot-prototype-elementFromPoint.html b/testing/web-platform/tests/shadow-dom/DocumentOrShadowRoot-prototype-elementFromPoint.html new file mode 100644 index 0000000000..6893ad394b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/DocumentOrShadowRoot-prototype-elementFromPoint.html @@ -0,0 +1,279 @@ +<!DOCTYPE html> +<html> + <head> + <title>Shadow DOM and CSSOM View: Document.prototype.elementFromPoint</title> + <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> + <meta name="assert" content="DocumentOrShadowRoot must have elementFromPoint and must return retarget the result against the context object."> + <link rel="help" href="https://www.w3.org/TR/cssom-view-1/#dom-document-elementfrompoint"> + <link rel="help" href="https://www.w3.org/TR/shadow-dom/#extensions-to-the-documentorshadowroot-mixin"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <div id="container"></div> + <style> +test-element { display: block; width: 100px; height: 100px; } + </style> + <script> + +function pointInElement(node) { + let x = 5; + let y = 5; + do { + x += node.offsetLeft; + y += node.offsetTop; + node = node.offsetParent; + } while (node); + return [x, y]; +} + +const displayValues = ['inline', 'block', 'inline-block']; +var container = document.getElementById('container'); +customElements.define('test-element', class extends HTMLElement { + constructor() { + super(); + } +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = 'hello'; + container.appendChild(host); + assert_equals(document.elementFromPoint(...pointInElement(host)), host); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), host); + }, 'document.elementFromPoint and shadow.ElementFromPoint must return the shadow host of the hit-tested text node when the hit-tested text node is a direct child of the root and the host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = 'text'; + container.appendChild(host); + assert_equals(document.elementFromPoint(...pointInElement(host)), host); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), host); + }, 'document.elementFromPoint and shadowRoot.elementFromPoint must return the shadow host when the hit-tested text node is assigned to a slot and the host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = '<span>text</span>'; + container.appendChild(host); + assert_equals(document.elementFromPoint(...pointInElement(host)), host.querySelector('span')); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), host.querySelector('span')); + }, 'document.elementFromPoint and shadowRoot.elementFromPoint must return the element assigned to a slot when hit-tested text node under an element is assigned to a slot in the shadow tree and the shadow host of the slot has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<span>text</span>'; + container.appendChild(host); + assert_equals(document.elementFromPoint(...pointInElement(host)), host); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), shadow.querySelector('span')); + }, 'document.elementFromPoint must return the shadow host of the hit-tested element under a shadow root and shadowRoot.elementFromPoint must return the element parent of the hit-tested text node under the point when the shadow host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot>fallback</slot>'; + container.appendChild(host); + assert_equals(document.elementFromPoint(...pointInElement(host)), host); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), shadow.querySelector('slot')); + }, 'document.elementFromPoint must return the shadow host and shadowRoot.elementFromPoint must return the slot parent of the fallback text when the hit-tested text node is a fallback content and the host has display: ' + displayValue); +}); + + + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = '<inner-host>hello</inner-host>'; + container.appendChild(host); + + let innerHost = host.querySelector('inner-host'); + let innerShadow = innerHost.attachShadow({mode: 'closed'}); + innerShadow.innerHTML = '<slot></slot>'; + assert_equals(document.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(innerShadow.elementFromPoint(...pointInElement(host)), innerHost); + }, 'document.elementFromPoint, shadowRoot.elementFromPoint, innerShadow.elementFromPoint must return a child element assigned to a slot' + + ' when the hit-tested text node is assigned to a slot in the shadow tree of the child element and the outer shadow host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = '<inner-host></inner-host>'; + container.appendChild(host); + + let innerHost = host.querySelector('inner-host'); + let innerShadow = innerHost.attachShadow({mode: 'closed'}); + innerShadow.innerHTML = 'hello'; + assert_equals(document.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(innerShadow.elementFromPoint(...pointInElement(host)), innerHost); + }, 'document.elementFromPoint, shadowRoot.elementFromPoint, innerShadow.elementFromPoint must return a child element with its own shadow tree assigned to a slot' + + ' when the hit-tested text node is its direct child and the outer shadow host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = '<inner-host></inner-host>'; + container.appendChild(host); + + let innerHost = host.querySelector('inner-host'); + let innerShadow = innerHost.attachShadow({mode: 'closed'}); + innerShadow.innerHTML = '<span>hello</span>'; + + assert_equals(document.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(shadow.elementFromPoint(...pointInElement(host)), innerHost); + assert_equals(innerShadow.elementFromPoint(...pointInElement(host)), innerShadow.querySelector('span')); + }, 'document.elementFromPoint, shadowRoot.elementFromPoint must return a child element with its own shadow tree assigned to a slot' + + ' when the hit-tested text node is a child of another element and innerShadow.elementFromPoint must return the parent element of the hit-tested text node under it when the outer shadow host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = 'hello'; + container.appendChild(host); + assert_array_equals(document.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + }, 'document.elementsFromPoint and shadow.elementsFromPoint must return the shadow host and its ancestors of the hit-tested text node when the hit-tested text node is a direct child of the root and the host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<slot></slot>'; + host.innerHTML = 'text'; + container.appendChild(host); + assert_array_equals(document.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + },'document.elementsFromPoint and shadowRoot.elementsFromPoint must return the shadow host and its ancestors when the hit-tested text node is assigned to a slot and the host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<div><slot></slot></div>'; + host.innerHTML = '<span>text</span>'; + container.appendChild(host); + assert_array_equals(document.elementsFromPoint(...pointInElement(host)), [host.querySelector('span'), host, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(...pointInElement(host)), [host.querySelector('span'), shadow.querySelector('div'), host, container, document.body, document.documentElement]); + }, 'document.elementsFromPoint and shadowRoot.elementsFromPoint must return the element assigned to a slot and its non-shadow ancestors when hit-tested text node under an element is assigned to a slot in the shadow tree and the shadow host of the slot has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<span>text</span>'; + container.appendChild(host); + assert_array_equals(document.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(...pointInElement(host)), [shadow.querySelector('span'), host, container, document.body, document.documentElement]); + }, 'document.elementsFromPoint must return the shadow host and its ancestors of the hit-tested element under a shadow root and' + + 'shadowRoot.elementsFromPoint must return the element parent and its non-shadow ancestors of the hit-tested text node under the point when the shadow host has display: ' + displayValue); +}); + +displayValues.forEach(function (displayValue) { + test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = displayValue; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<div><slot>fallback</slot></div>'; + container.appendChild(host); + assert_array_equals(document.elementsFromPoint(...pointInElement(host)), [host, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(...pointInElement(host)), [shadow.querySelector('slot'), shadow.querySelector('div'), host, container, document.body, document.documentElement]); + }, 'document.elementsFromPoint must return the shadow host and its ancestors and shadowRoot.elementsFromPoint must return the slot parent of the fallback text and its non-shadow ancestors when the hit-tested text node is a fallback content and the host has display: ' + displayValue); +}); + +test(function () { + container.innerHTML = ''; + let host = document.createElement('test-element'); + host.style.display = 'block'; + let shadow = host.attachShadow({mode: 'closed'}); + shadow.innerHTML = '<div style="margin: 2px;">not hit</div>'; + let aboveHost = document.createElement("div"); + aboveHost.appendChild(host); + container.appendChild(aboveHost); + document.documentElement.style = 'background-attachment: scroll; height: 2px;'; + let boundingRect = host.getBoundingClientRect(); + assert_array_equals(document.elementsFromPoint(boundingRect.x, boundingRect.y), [host, aboveHost, container, document.body, document.documentElement]); + assert_array_equals(shadow.elementsFromPoint(boundingRect.x, boundingRect.y), [host, aboveHost, container, document.body, document.documentElement]); +}, 'shadowRoot.elementsFromPoint must behave the same with document.elementsFromPoint regarding HTML element'); + +container.innerHTML = ''; + +test(function(t) { + const container = document.createElement("div"); + const span = document.createElement("span"); + span.innerText = "foo"; + document.body.appendChild(container); + container.appendChild(span); + + let elements = document.elementsFromPoint(...pointInElement(span)); + assert_equals(elements.length, 4); + assert_equals(elements[0], span); + assert_equals(elements[1], container); + assert_equals(elements[2].nodeName, 'BODY'); + assert_equals(elements[3].nodeName, 'HTML'); + + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + elements = shadowRoot.elementsFromPoint(...pointInElement(span)); + assert_equals(elements.length, 4); + assert_equals(elements[0], span); + assert_equals(elements[1], container); + assert_equals(elements[2].nodeName, 'BODY'); + assert_equals(elements[3].nodeName, 'HTML'); +}, "elementsFromPoint should return all elements under a point, even when context object is not connected"); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow-custom-element.html b/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow-custom-element.html new file mode 100644 index 0000000000..11ae277c41 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow-custom-element.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<title>Shadow DOM: Attaching a ShadowRoot for custom elements</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@chromium.org"> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-element-attachshadow"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +class MyAutonomousCustomElement extends HTMLElement { +} + +customElements.define('my-custom', MyAutonomousCustomElement); + +test(() => { + assert_true(document.createElement('my-custom').attachShadow({mode: "open"}) instanceof ShadowRoot); +}, 'Element.attachShadow must create an instance of ShadowRoot for autonomous custom elements'); + +test(() => { + assert_true(document.createElement('undefined-custom').attachShadow({mode: 'open'}) instanceof ShadowRoot); +}, 'Element.attachShadow must create an instance of ShadowRoot for undefined autonomous custom elements'); + +test(() => { + class ShadowDisabledElement extends HTMLElement { + static get disabledFeatures() { return ['shadow']; } + } + + // No definition. attachShadow() should succeed. + let element = document.createElement('shadow-disabled-element'); + element.attachShadow({mode: 'closed'}); + + // No definition and it's already a host. + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: 'closed'}); + }, 'No definition, host'); + + // The element has a definition, and it's already a host. + customElements.define('shadow-disabled-element', ShadowDisabledElement); + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: 'closed'}); + }, 'Definition, host'); + + // The element has a definition, and it's not a host. + assert_throws_dom('NotSupportedError', () => { + document.createElement('shadow-disabled-element').attachShadow({mode: 'closed'}); + }, 'Definition, not a host'); +}, 'Element.attachShadow for an autonomous custom element with ' + + 'disabledFeatures=["shadow"] should throw a NotSupportedError'); + +test(() => { + class ShadowDisabledHeadingElement extends HTMLHeadingElement { + static get disabledFeatures() { return ['shadow']; } + } + + // No definition. attachShadow() should succeed. + let element = document.createElement('h2', + {is: 'shadow-disabled-heading-element'}); + element.attachShadow({mode: 'closed'}); + + // No definition and it's already a host. + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: 'closed'}); + }, 'No definition, host.'); + + // The element has a definition, and it's already a host. + customElements.define('shadow-disabled-heading-element', + ShadowDisabledHeadingElement, {extends: 'h2'}); + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: 'closed'}); + }, 'Definition, host'); + + // The element has a definition, and it's not a host. + let h2 = document.createElement('h2', {is: 'shadow-disabled-heading-element'}); + assert_throws_dom('NotSupportedError', () => { + h2.attachShadow({mode: 'closed'}); + }, 'Definition, not a host'); +}, 'Element.attachShadow for a customized built-in element with ' + + 'disabledFeatures=["shadow"] should throw a NotSupportedError'); + +test(() => { + class CapitalShadowDisabledElement extends HTMLElement { + static get disabledFeatures() { return ['SHADOW']; } + } + + customElements.define('capital-shadow-disabled-element', CapitalShadowDisabledElement); + + // Test fails if this throws + document.createElement('capital-shadow-disabled-element').attachShadow({mode: 'open'}); +}, 'Element.attachShadow for a custom element with disabledFeatures=["SHADOW"] should not throw a NotSupportedError'); + +class MyCustomizedBuiltinElement extends HTMLInputElement { +} + +customElements.define('my-input', MyCustomizedBuiltinElement, { extends: 'input' }); + +test(() => { + assert_throws_dom('NotSupportedError', () => { + document.createElement('input', {is: 'my-input'}).attachShadow({mode: "open"}); + }); +}, 'Element.attachShadow must throw a NotSupportedError for customized built-in elements'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow.html b/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow.html new file mode 100644 index 0000000000..187ac4c408 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Element-interface-attachShadow.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Attaching a ShadowRoot</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Element.prototype.attachShadow should create an instance of ShadowRoot"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#widl-Element-attachShadow-ShadowRoot-ShadowRootInit-shadowRootInitDict"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src='../html/resources/common.js'></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + assert_true('attachShadow' in Element.prototype, 'Element.prototype.attachShadow must exist'); + assert_equals(typeof(document.createElement('div').attachShadow), 'function', 'An instance of div must have attachShadow which is a function'); +}, 'Check the existence of Element.attachShadow'); + +test(function () { + assert_false('attachShadow' in Node.prototype, 'Node.prototype.attachShadow must not exist'); + assert_false('attachShadow' in CharacterData.prototype, 'CharacterData.prototype.attachShadow must not exist'); + assert_false('attachShadow' in Comment.prototype, 'Comment.prototype.attachShadow must not exist'); + assert_equals(typeof(document.createComment('').attachShadow), 'undefined', 'An instance of comment must not have attachShadow'); + assert_false('attachShadow' in Document.prototype, 'Document.prototype.attachShadow must not exist'); + assert_equals(typeof(document.attachShadow), 'undefined', 'An instance of document must not have attachShadow which is a function'); + assert_false('attachShadow' in DocumentFragment.prototype, 'DocumentFragment.prototype.attachShadow must not exist'); + assert_equals(typeof((new DOMParser()).parseFromString('', 'text/html').attachShadow), 'undefined', 'An instance of document must not have attachShadow which is a function'); + assert_false('attachShadow' in Text.prototype, 'Text.prototype.attachShadow must not exist'); + assert_equals(typeof(document.createTextNode('').attachShadow), 'undefined', 'An instance of text node must not have attachShadow'); +}, 'Nodes other than Element should not have attachShadow'); + +test(function () { + assert_throws_js(TypeError, function () { + document.createElement('div').attachShadow({}) + }, 'attachShadow must throw a TypeError when mode is omitted'); + + assert_throws_js(TypeError, function () { + document.createElement('div').attachShadow({mode: true}) + }, 'attachShadow must throw a TypeError when mode is a boolean'); + + assert_throws_js(TypeError, function () { + document.createElement('div').attachShadow({mode: 1}) + }, 'attachShadow must throw a TypeError when mode is 1'); +}, 'Element.attachShadow must throw a TypeError if mode is not "open" or "closed"'); + +test(function () { + assert_true(document.createElement('div').attachShadow({mode: "open"}) instanceof ShadowRoot, + 'attachShadow({mode: "open"}) should create an instance of ShadowRoot'); + assert_true(document.createElement('div').attachShadow({mode: "closed"}) instanceof ShadowRoot, + 'attachShadow({mode: "closed"}) should create an instance of ShadowRoot'); +}, 'Element.attachShadow must create an instance of ShadowRoot'); + +test(function () { + assert_throws_dom('NotSupportedError', function () { + var div = document.createElement('div'); + div.attachShadow({mode: "open"}); + div.attachShadow({mode: "open"}); + }, 'Calling attachShadow({mode: "open"}) twice on the same element must throw'); + + assert_throws_dom('NotSupportedError', function () { + var div = document.createElement('div'); + div.attachShadow({mode: "closed"}); + div.attachShadow({mode: "closed"}); + }, 'Calling attachShadow({mode: "closed"}) twice on the same element must throw'); + + assert_throws_dom('NotSupportedError', function () { + var div = document.createElement('div'); + div.attachShadow({mode: "open"}); + div.attachShadow({mode: "closed"}); + }, 'Calling attachShadow({mode: "closed"}) after attachShadow({mode: "open"}) on the same element must throw'); + + assert_throws_dom('NotSupportedError', function () { + var div = document.createElement('div'); + div.attachShadow({mode: "closed"}); + div.attachShadow({mode: "open"}); + }, 'Calling attachShadow({mode: "open"}) after attachShadow({mode: "closed"}) on the same element must throw'); +}, 'Element.attachShadow must throw a NotSupportedError if the context object already hosts a shadow tree'); + +test(function () { + for (var elementName of HTML5_SHADOW_DISALLOWED_ELEMENTS) { + assert_throws_dom('NotSupportedError', function () { + document.createElement(elementName).attachShadow({mode: "open"}); + }, 'Calling attachShadow({mode: "open"}) on ' + elementName + ' element must throw'); + + assert_throws_dom('NotSupportedError', function () { + document.createElement(elementName).attachShadow({mode: "closed"}); + }, 'Calling attachShadow({mode: "closed"}) on ' + elementName + ' element must throw'); + } +}, 'Element.attachShadow must throw a NotSupportedError for non-safelisted elements'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Element-interface-shadowRoot-attribute.html b/testing/web-platform/tests/shadow-dom/Element-interface-shadowRoot-attribute.html new file mode 100644 index 0000000000..02d8050997 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Element-interface-shadowRoot-attribute.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Element interface shadowRoot attribute</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="shadowRoot attribute on Element interface must return the associated open shadow tree if there is one"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#the-shadowroot-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + assert_true('shadowRoot' in Element.prototype, 'shadowRoot must be defined on Element prototype'); + assert_true('shadowRoot' in document.createElement('div'), 'shadowRoot must be defined on an instance of div element'); + assert_false('shadowRoot' in Node.prototype, 'shadowRoot must not be defined on Node prototype'); + assert_false('shadowRoot' in Text.prototype, 'shadowRoot must not be defined on Text prototype'); + assert_false('shadowRoot' in document.createTextNode(''), 'shadowRoot must not be defined on an instance of Text node'); + assert_false('shadowRoot' in Comment.prototype, 'shadowRoot must not be defined on Comment prototype'); + assert_false('shadowRoot' in document.createComment(''), 'shadowRoot must not be defined on an instance of Comment node'); + assert_false('shadowRoot' in Document.prototype, 'shadowRoot must not be defined on Document prototype'); + assert_false('shadowRoot' in document, 'shadowRoot must not be defined on an instance of Document'); + assert_false('shadowRoot' in DocumentFragment.prototype, 'shadowRoot must not be defined on DocumentFragment prototype'); + assert_false('shadowRoot' in (new DOMParser).parseFromString('', 'text/html'), 'shadowRoot must not be defined on an instance of DocumentFragment node'); +}, 'shadowRoot must be defined on Element prototype'); + +test(function () { + var host = document.createElement('div'); + assert_equals(host.shadowRoot, null, 'shadowRoot must return null when the host does not have a shadow tree attached to it'); + + var openShadowRoot = host.attachShadow({mode: 'open'}); + assert_equals(host.shadowRoot, openShadowRoot, 'shadowRoot must return the open shadow root attachShadow attached'); +}, 'shadowRoot attribute must return the open shadow root associated with the element'); + +test(function () { + var host = document.createElement('div'); + host.attachShadow({mode: 'closed'}); + assert_equals(host.shadowRoot, null); +}, 'shadowRoot attribute must return null if the shadow root attached to the element is closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Extensions-to-Event-Interface.html b/testing/web-platform/tests/shadow-dom/Extensions-to-Event-Interface.html new file mode 100644 index 0000000000..806d539cad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Extensions-to-Event-Interface.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Extensions to Event Interface</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Event interface must have composedPath() as a method"> +<link rel="help" href="http://w3c.github.io/webcomponents/spec/shadow/#extensions-to-event-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/event-path-test-helpers.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + var event = new Event('my-event'); + assert_array_equals(event.composedPath(), []); +}, 'composedPath() must return an empty array when the event has not been dispatched'); + +test(function () { + var event = new Event('my-event'); + document.body.dispatchEvent(event); + assert_array_equals(event.composedPath(), []); +}, 'composedPath() must return an empty array when the event is no longer dispatched'); + +test(function () { + var event = new Event('my-event'); + assert_false(event.composed); +}, 'composed on EventInit must default to false'); + +test(function () { + var event = new Event('my-event', {composed: true}); + assert_true(event.composed); + + event = new Event('my-event', {composed: false}); + assert_false(event.composed); +}, 'composed on EventInit must set the composed flag'); + +/* +-SR: ShadowRoot -S: Slot target: (~) *: indicates start digit: event path order +A (4) --------------------------- A-SR (3) ++ B ------------ B-SR + A1 (2) --- A1-SR (1) + + C + B1 --- B1-SR + A2-S + A1a (*; 0) + + D --- D-SR + B1a + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 +*/ + +function testComposedEvent(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + var log = dispatchEventWithEventLog(nodes, nodes.A1a, new Event('my-event', {composed: true, bubbles: true})); + + var expectedPath = ['A1a', 'A1-SR', 'A1', 'A-SR', 'A']; + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], expectedPath); + assert_array_equals(log.pathAtTargets[1], expectedPath); + assert_array_equals(log.pathAtTargets[2], mode == 'open' ? expectedPath : ['A1', 'A-SR', 'A'], + 'composedPath must only contain unclosed nodes of the current target.'); + }, 'The event must propagate out of ' + mode + ' mode shadow boundaries when the composed flag is set'); +} + +testComposedEvent('open'); +testComposedEvent('closed'); + +/* +-SR: ShadowRoot -S: Slot target: (~) *: indicates start digit: event path order +A ------------------------------- A-SR ++ B ------------ B-SR + A1 --- A1-SR (1) + + C + B1 --- B1-SR + A2-S + A1a (*; 0) + + D --- D-SR + B1a + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 +*/ + +function testNonComposedEvent(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + var log = dispatchEventWithEventLog(nodes, nodes.A1a, new Event('my-event', {composed: false, bubbles: true})); + + var expectedPath = ['A1a', 'A1-SR']; + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], expectedPath); + assert_array_equals(log.pathAtTargets[1], expectedPath); + }, 'The event must not propagate out of ' + mode + ' mode shadow boundaries when the composed flag is unset'); +} + +testNonComposedEvent('open'); +testNonComposedEvent('closed'); + +/* +-SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order +A ------------------------------- A-SR ++ B ------------ B-SR + A1 ----------- A1-SR (1) + + C + B1 --- B1-SR + A2-S [*; 0-1] + A1a (*; 0) + + D --- D-SR + B1a + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 +*/ + +function testNonComposedEventWithRelatedTarget(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + var log = dispatchEventWithEventLog(nodes, nodes.A1a, new MouseEvent('foo', {composed: false, bubbles: true, relatedTarget: nodes['A2-S']})); + + var expectedPath = ['A1a', 'A1-SR']; + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], expectedPath); + assert_array_equals(log.pathAtTargets[1], expectedPath); + assert_array_equals(log.relatedTargets, ['A2-S', 'A2-S']); + }, 'The event must not propagate out of ' + mode + ' mode shadow boundaries when the composed flag is unset on an event with relatedTarget'); +} + +testNonComposedEventWithRelatedTarget('open'); +testNonComposedEventWithRelatedTarget('closed'); + +/* +-SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order +A ------------------------------------------------ A-SR ++ B ------------ B-SR (4) + A1 --- A1-SR + + C + B1 (3) [0,3-4] --- B1-SR (2) + A2-S + A1a + + D --- D-SR + B1a (*; 0) + B1b [1-2] --- B1b-SR + + D1 + B1c-S (1) + B1b1 + + B1b2 [*] +*/ + +function testScopedEventWithUnscopedRelatedTargetThroughSlot(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + var log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {scoped: true, relatedTargetScoped: false, bubbles: true, relatedTarget: nodes['B1b2']})); + + var expectedPath = ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR']; + var pathExposedToB1a = ['B1a', 'B1', 'B-SR']; + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.pathAtTargets[1], expectedPath); + assert_array_equals(log.pathAtTargets[2], expectedPath); + assert_array_equals(log.pathAtTargets[3], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.pathAtTargets[4], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.relatedTargets, ['B1', 'B1b', 'B1b', 'B1', 'B1']); + }, 'The event must not propagate out of ' + mode + ' mode shadow tree of the target but must propagate out of inner shadow trees when the scoped flag is set'); +} + +testScopedEventWithUnscopedRelatedTargetThroughSlot('open'); +testScopedEventWithUnscopedRelatedTargetThroughSlot('closed'); + +/* +-SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order +A ------------------------------- A-SR (3) ++ B ------------ B-SR + A1 (2) ------- A1-SR (1) + + C + B1 --- B1-SR + A2-S [*; 0-3] + A1a (*; 0) + + D --- D-SR + B1a + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 +*/ + +function testComposedEventWithRelatedTarget(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + log = dispatchEventWithEventLog(nodes, nodes.A1a, new MouseEvent('foo', {composed: true, bubbles: true, relatedTarget: nodes['A2-S']})); + + var expectedPath = ['A1a', 'A1-SR', 'A1', 'A-SR']; + var pathExposedToA1 = ['A1', 'A-SR']; + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], expectedPath); + assert_array_equals(log.pathAtTargets[1], expectedPath); + assert_array_equals(log.pathAtTargets[2], mode == 'open' ? expectedPath : pathExposedToA1); + assert_array_equals(log.pathAtTargets[3], mode == 'open' ? expectedPath : pathExposedToA1); + assert_array_equals(log.relatedTargets, ['A2-S', 'A2-S', 'A2-S', 'A2-S']); + }, 'The event must propagate out of ' + mode + ' mode shadow tree in which the relative target and the relative related target are the same'); +} + +testComposedEventWithRelatedTarget('open'); +testComposedEventWithRelatedTarget('closed'); + +/* +-SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order +A (8) [0-5,8] ---------------------------------------- A-SR (7) ++ B (5) ------- B-SR (4) + A1 [6,7] --- A1-SR + + C + B1 (3) ------- B1-SR (2) + A2-S (6) + A1a [*] + + D --- D-SR + B1a (*; 0) + B1b ------- B1b-SR + + D1 + B1c-S (1) + B1b1 + + B1b2 +*/ + +function testComposedEventThroughSlot(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {composed: true, bubbles: true, relatedTarget: nodes.A1a})); + + var expectedPath = ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A']; + var expectedRelatedTarget = ['A', 'A', 'A', 'A', 'A', 'A', 'A1', 'A1', 'A']; + var pathExposedToB1a = ['B1a', 'B1', 'B-SR', 'B', 'A']; + var pathExposedToB1cS = ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A']; + var pathExposedToB = [ 'B', 'A']; + var pathExposedToA1 = [ 'B', 'A2-S', 'A-SR', 'A']; + + assert_array_equals(log.eventPath, expectedPath); + assert_equals(log.eventPath.length, log.pathAtTargets.length); + assert_array_equals(log.pathAtTargets[0], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.pathAtTargets[1], mode == 'open' ? expectedPath : pathExposedToB1cS); + assert_array_equals(log.pathAtTargets[2], mode == 'open' ? expectedPath : pathExposedToB1cS); + assert_array_equals(log.pathAtTargets[3], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.pathAtTargets[4], mode == 'open' ? expectedPath : pathExposedToB1a); + assert_array_equals(log.pathAtTargets[5], mode == 'open' ? expectedPath : pathExposedToB); + assert_array_equals(log.pathAtTargets[6], mode == 'open' ? expectedPath : pathExposedToA1); + assert_array_equals(log.pathAtTargets[7], mode == 'open' ? expectedPath : pathExposedToA1); + assert_array_equals(log.pathAtTargets[8], mode == 'open' ? expectedPath : pathExposedToB); + assert_array_equals(log.relatedTargets, expectedRelatedTarget); + }, 'composedPath() must contain and only contain the unclosed nodes of target in ' + mode + ' mode shadow trees'); +} + +testComposedEventThroughSlot('open'); +testComposedEventThroughSlot('closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/HTMLSlotElement-interface.html b/testing/web-platform/tests/shadow-dom/HTMLSlotElement-interface.html new file mode 100644 index 0000000000..e22de32167 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/HTMLSlotElement-interface.html @@ -0,0 +1,269 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: HTMLSlotElement interface</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="HTMLSlotElement must exist on window with name attribute and getAssignedNode() method"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#the-slot-element"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + assert_true('HTMLSlotElement' in window, 'HTMLSlotElement must be defined on window'); + assert_equals(Object.getPrototypeOf(HTMLSlotElement.prototype), HTMLElement.prototype, 'HTMLSlotElement should inherit from HTMLElement'); + assert_true(document.createElement('slot') instanceof HTMLSlotElement, 'slot element should be an instance of HTMLSlotElement'); + assert_true(document.createElement('slot') instanceof HTMLElement, 'slot element should be an instance of HTMLElement'); +}, 'HTMLSlotElement must be defined on window'); + +test(function () { + assert_true('name' in HTMLSlotElement.prototype, '"name" attribute must be defined on HTMLSlotElement.prototype'); + + var slotElement = document.createElement('slot'); + assert_equals(slotElement.name, '', '"name" attribute must return the empty string when "name" content attribute is not set'); + + slotElement.setAttribute('name', 'foo'); + assert_equals(slotElement.name, 'foo', '"name" attribute must return the value of the "name" content attribute'); + + slotElement.name = 'bar'; + assert_equals(slotElement.name, 'bar', '"name" attribute must return the assigned value'); + assert_equals(slotElement.getAttribute('name'), 'bar', '"name" attribute must update the "name" content attribute'); +}, '"name" attribute on HTMLSlotElement must reflect "name" attribute'); + +function testSlotOutsideShadowTree(options) +{ + test(function () { + assert_true('assignedNodes' in HTMLSlotElement.prototype, '"assignedNodes" method must be defined on HTMLSlotElement.prototype'); + + var slotElement = document.createElement('slot'); + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes() must return an empty array when the slot element is not in any tree'); + + document.body.appendChild(slotElement); + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes() must return an empty array when the slot element is in a document tree'); + + }, 'assignedNodes(' + (options ? JSON.stringify(options) : '') + + ') on a HTMLSlotElement must return an empty array when the slot element is not in a tree or in a document tree'); +} + +testSlotOutsideShadowTree(null); +testSlotOutsideShadowTree({flattened: false}); +testSlotOutsideShadowTree({flattened: true}); + +function testSingleLevelOfSlotting(options) +{ + test(function () { + assert_true('assignedNodes' in HTMLSlotElement.prototype, '"assignedNodes" method must be defined on HTMLSlotElement.prototype'); + + var shadowHost = document.createElement('div'); + var child = document.createElement('p'); + + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + var slotElement = document.createElement('slot'); + shadowRoot.appendChild(slotElement); + + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes() must return an empty array when there are no nodes in the shadow tree'); + + shadowHost.appendChild(child); + assert_array_equals(slotElement.assignedNodes(options), [child], 'assignedNodes() on a default slot must return an element without slot element'); + + child.setAttribute('slot', 'foo'); + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes() on a default slot must not return an element with non-empty slot attribute'); + + child.setAttribute('slot', ''); + assert_array_equals(slotElement.assignedNodes(options), [child], 'assignedNodes() on a default slot must return an element with empty slot attribute'); + + slotElement.setAttribute('name', 'bar'); + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes() on a named slot must not return an element with empty slot attribute'); + + slotElement.setAttribute('name', ''); + assert_array_equals(slotElement.assignedNodes(options), [child], 'assignedNodes() on an empty name slot must return an element with empty slot attribute'); + + }, 'assignedNodes(' + (options ? JSON.stringify(options) : '') + ') must return the list of assigned nodes when none of the assigned nodes themselves are slots'); +} + +testSingleLevelOfSlotting(null); +testSingleLevelOfSlotting({flattened: false}); +testSingleLevelOfSlotting({flattened: true}); + +function testMutatingSlottedContents(options) +{ + test(function () { + var shadowHost = document.createElement('div'); + var p = document.createElement('p'); + var b = document.createElement('b'); + shadowHost.appendChild(p); + shadowHost.appendChild(b); + + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + var slotElement = document.createElement('slot'); + shadowRoot.appendChild(slotElement); + + assert_array_equals(slotElement.assignedNodes(options), [p, b], 'assignedNodes must return the distributed nodes'); + + slotElement.name = 'foo'; + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes must be empty when there are no matching elements for the slot name'); + + b.slot = 'foo'; + assert_array_equals(slotElement.assignedNodes(options), [b], 'assignedNodes must return the nodes with the matching slot name'); + + p.slot = 'foo'; + assert_array_equals(slotElement.assignedNodes(options), [p, b], 'assignedNodes must return the nodes with the matching slot name in the tree order'); + + slotElement.removeAttribute('name'); + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes must be empty for a default slot when all elements have "slot" attributes specified'); + + }, 'assignedNodes(' + (options ? JSON.stringify(options) : '') + ') must update when slot and name attributes are modified'); +} + +testMutatingSlottedContents(null); +testMutatingSlottedContents({flattened: false}); +testMutatingSlottedContents({flattened: true}); + +function testMutatingSlotName(options) +{ + test(function () { + var shadowHost = document.createElement('div'); + var child = document.createElement('span'); + shadowHost.appendChild(child); + + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + var slotElement = document.createElement('slot'); + slotElement.name = 'foo'; + shadowRoot.appendChild(slotElement); + + assert_array_equals(slotElement.assignedNodes(options), [], 'assignedNodes must be empty when there are no matching elements for the slot name'); + + slotElement.removeAttribute('name'); + assert_array_equals(slotElement.assignedNodes(options), [child], 'assignedNodes must be empty when there are no matching elements for the slot name'); + + }, 'assignedNodes(' + (options ? JSON.stringify(options) : '') + ') must update when a default slot is introduced dynamically by a slot rename'); +} + +testMutatingSlotName(null); +testMutatingSlotName({flattened: false}); +testMutatingSlotName({flattened: true}); + +function testInsertingAndRemovingSlots(options) +{ + test(function () { + var shadowHost = document.createElement('div'); + var p = document.createElement('p'); + var text = document.createTextNode(''); + var comment = document.createComment(''); + var processingInstruction = document.createProcessingInstruction('target', 'data'); + var b = document.createElement('b'); + shadowHost.appendChild(p); + shadowHost.appendChild(text); + shadowHost.appendChild(comment); + shadowHost.appendChild(processingInstruction); + shadowHost.appendChild(b); + + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + + var firstSlotElement = document.createElement('slot'); + shadowRoot.appendChild(firstSlotElement); + + var secondSlotElement = document.createElement('slot'); + shadowRoot.appendChild(secondSlotElement); + + assert_array_equals(firstSlotElement.assignedNodes(options), [p, text, b], + 'assignedNodes on a default slot must return the elements without slot attributes and text nodes'); + assert_array_equals(secondSlotElement.assignedNodes(options), [], + 'assignedNodes on the second unnamed slot element must return an empty array'); + + shadowRoot.removeChild(firstSlotElement); + assert_array_equals(firstSlotElement.assignedNodes(options), [], + 'assignedNodes on a detached formerly-default slot must return an empty array'); + assert_array_equals(secondSlotElement.assignedNodes(options), [p, text, b], + 'assignedNodes on the second unnamed slot element after removing the first must return the elements without slot attributes and text nodes'); + + shadowRoot.removeChild(secondSlotElement); + shadowRoot.appendChild(secondSlotElement); + assert_array_equals(firstSlotElement.assignedNodes(options), [], + 'Removing and re-inserting a default slot must not change the result of assignedNodes on a detached slot'); + assert_array_equals(secondSlotElement.assignedNodes(options), [p, text, b], + 'Removing and re-inserting a default slot must not change the result of assignedNodes'); + + shadowRoot.insertBefore(firstSlotElement, secondSlotElement); + assert_array_equals(firstSlotElement.assignedNodes(options), [p, text, b], + 'assignedNodes on a newly inserted unnamed slot element must return the elements without slot attributes and text nodes'); + assert_array_equals(secondSlotElement.assignedNodes(options), [], + 'assignedNodes on formerly-first but now second unnamed slot element must return an empty array'); + + }, 'assignedNodes(' + (options ? JSON.stringify(options) : '') + ') must update when slot elements are inserted or removed'); +} + +testInsertingAndRemovingSlots(null); +testInsertingAndRemovingSlots({flattened: false}); +testInsertingAndRemovingSlots({flattened: true}); + +test(function () { + var outerHost = document.createElement('div'); + var outerChild = document.createElement('span'); + outerHost.appendChild(outerChild); + + var outerShadow = outerHost.attachShadow({mode: 'closed'}); + var innerHost = document.createElement('div'); + var outerSlot = document.createElement('slot'); + var innerChild = document.createElement('b'); + outerShadow.appendChild(innerHost); + innerHost.appendChild(outerSlot); + innerHost.appendChild(innerChild); + + var innerShadow = innerHost.attachShadow({mode: 'closed'}); + var innerSlot = document.createElement('slot'); + innerShadow.appendChild(innerSlot); + + assert_array_equals(outerSlot.assignedNodes(), [outerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(outerSlot.assignedNodes({flatten: false}), [outerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(outerSlot.assignedNodes({flatten: true}), [outerChild], 'assignedNodes({flatten: true}) on a default slot must return the assigned nodes if they are not themselves slots'); + + assert_array_equals(innerSlot.assignedNodes(), [outerSlot, innerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: false}), [outerSlot, innerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: true}), [outerChild, innerChild], 'assignedNodes({flatten: true}) on a default slot must return the distributed nodes'); + + outerSlot.name = 'foo'; + assert_array_equals(outerSlot.assignedNodes(), [], 'assignedNodes() on a named slot must return an empty array if there are no matching elements'); + assert_array_equals(outerSlot.assignedNodes({flatten: false}), [], 'assignedNodes({flatten: false}) on a named slot must return an empty array if there are no matching elements'); + assert_array_equals(outerSlot.assignedNodes({flatten: true}), [], 'assignedNodes({flatten: true}) on a named slot must return an empty array if there are no matching elements'); + + assert_array_equals(innerSlot.assignedNodes(), [outerSlot, innerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: false}), [outerSlot, innerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: true}), [innerChild], 'assignedNodes({flatten: true}) on a default slot must return the distributed nodes'); + + outerChild.slot = 'foo'; + assert_array_equals(outerSlot.assignedNodes(), [outerChild], 'assignedNodes() on a named slot must return matching elements'); + assert_array_equals(outerSlot.assignedNodes({flatten: false}), [outerChild], 'assignedNodes({flatten: false}) on a named slot must return matching elements'); + assert_array_equals(outerSlot.assignedNodes({flatten: true}), [outerChild], 'assignedNodes({flatten: true}) on a named slot must return matching elements'); + + assert_array_equals(innerSlot.assignedNodes(), [outerSlot, innerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: false}), [outerSlot, innerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: true}), [outerChild, innerChild], 'assignedNodes({flatten: true}) on a default slot must return the distributed nodes'); + + var newInnerSlot = document.createElement('slot'); + innerShadow.insertBefore(newInnerSlot, innerSlot); + assert_array_equals(newInnerSlot.assignedNodes(), [outerSlot, innerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(newInnerSlot.assignedNodes({flatten: false}), [outerSlot, innerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(newInnerSlot.assignedNodes({flatten: true}), [outerChild, innerChild], 'assignedNodes({flatten: true}) on a default slot must return the distributed nodes'); + + assert_array_equals(innerSlot.assignedNodes(), [], 'assignedNodes() on a nameless slot element which appears after a default slot must return an empty array'); + assert_array_equals(innerSlot.assignedNodes({flatten: false}), [], 'assignedNodes({flatten: false}) on a nameless slot element which appears after a default slot must return an empty array'); + assert_array_equals(innerSlot.assignedNodes({flatten: true}), [], 'assignedNodes({flatten: true}) on a nameless slot element which appears after a default slot must return an empty array'); + + innerShadow.removeChild(newInnerSlot); + assert_array_equals(newInnerSlot.assignedNodes(), [], 'assignedNodes() must return an empty array when the slot element is not in any tree'); + assert_array_equals(newInnerSlot.assignedNodes({flatten: false}), [], 'assignedNodes({flatten: false}) must return an empty array when the slot element is not in any tree'); + assert_array_equals(newInnerSlot.assignedNodes({flatten: true}), [], 'assignedNodes({flatten: true}) must return an empty array when the slot element is not in any tree'); + + assert_array_equals(innerSlot.assignedNodes(), [outerSlot, innerChild], 'assignedNodes() on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: false}), [outerSlot, innerChild], 'assignedNodes({flatten: false}) on a default slot must return the assigned nodes'); + assert_array_equals(innerSlot.assignedNodes({flatten: true}), [outerChild, innerChild], 'assignedNodes({flatten: true}) on a default slot must return the distributed nodes'); + +}, 'assignedNodes({flatten: true}) must return the distributed nodes, and assignedNodes() and assignedNodes({flatten: false}) must returned the assigned nodes'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/META.yml b/testing/web-platform/tests/shadow-dom/META.yml new file mode 100644 index 0000000000..8fb0a9b995 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/META.yml @@ -0,0 +1,6 @@ +spec: https://dom.spec.whatwg.org/#shadow-trees +suggested_reviewers: + - kojiishi + - rniwa + - takayoshikochi + - hayatoito diff --git a/testing/web-platform/tests/shadow-dom/MouseEvent-prototype-offsetX-offsetY.html b/testing/web-platform/tests/shadow-dom/MouseEvent-prototype-offsetX-offsetY.html new file mode 100644 index 0000000000..643736b85c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/MouseEvent-prototype-offsetX-offsetY.html @@ -0,0 +1,154 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: MouseEvent's offsetX and offsetY attributes must be relative to the relative target.</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="The MouseEvent offsetX and offsetY attributes must return the coordinates relative to the origin of the padding edge of the relative target"> +<link rel="help" href="http://w3c.github.io/webcomponents/spec/shadow/#event-dispatch"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/event-path-test-helpers.js"></script> +</head> +<body> +<style> +html, body { padding: 0; margin: 0; } +my-host { display: block; width: 180px; height: 80px; margin: 10px 20px; padding: 10px; background: #ccc; } +#log { margin: 1rem; } +</style> +<my-host></my-host> +<div id="log"></div> +<script> + +var shadowStyle = '#container { width: 160px; height: 60px; padding: 10px; background: yellow; } #target { margin-left: 5px; background: orange; }'; +var host = document.querySelector('my-host'); + +function attachLoggers(targets) +{ + var eventLogs = []; + for (var i = 0; i < targets.length; i++) { + targets[i].addEventListener('mousedown', function (event) { + eventLogs.push({current: this, target: event.target, offsetX: event.offsetX, offsetY: event.offsetY}); + }); + } + return eventLogs; +} + +test(function () { + host.innerHTML = '<style>' + shadowStyle + '</style><div id="container"><span id="target">Click here</div></div>'; + + var target = host.querySelector('#target'); + var container = host.querySelector('#container'); + + var eventLogs = attachLoggers([target, container, host, document.body]); + var mouseEvent = new MouseEvent('mousedown', {clientX: 51, clientY: 37, composed: true, bubbles: true}); + target.dispatchEvent(mouseEvent); + + assert_equals(host.offsetLeft, 20, 'The host must be at (20px, 10px)'); + assert_equals(host.offsetTop, 10, 'The host must be at (20px, 10px)'); + assert_equals(target.offsetLeft, 45, 'The target must be at (45px, 30px)'); + assert_equals(target.offsetTop, 30, 'The target must be at (45px, 30px)'); + + assert_equals(eventLogs[0].current, target); + assert_equals(eventLogs[0].target, target); + assert_equals(eventLogs[0].offsetX, 21); // Padding edge of target is at (30px, 20px) + assert_equals(eventLogs[0].offsetY, 17); + + assert_equals(eventLogs[1].current, container); + assert_equals(eventLogs[1].target, target); + assert_equals(eventLogs[1].offsetX, 21); + assert_equals(eventLogs[1].offsetY, 17); + + assert_equals(eventLogs[2].current, host); + assert_equals(eventLogs[2].target, target); + assert_equals(eventLogs[2].offsetX, 21); + assert_equals(eventLogs[2].offsetY, 17); + + assert_equals(eventLogs[3].current, document.body); + assert_equals(eventLogs[3].target, target); + assert_equals(eventLogs[3].offsetX, 21); + assert_equals(eventLogs[3].offsetY, 17); +}, 'MouseEvent\'s offsetX and offsetY attributes must be relative to the target.'); + +var shadowRoot = host.attachShadow({mode: 'closed'}); +test(function () { + shadowRoot.innerHTML = '<style>' + shadowStyle + '</style><div id="container"><span id="target">Click here</div></div>'; + + var target = shadowRoot.querySelector('#target'); + var container = shadowRoot.querySelector('#container'); + + var eventLogs = attachLoggers([target, container, shadowRoot, host, document.body]); + var mouseEvent = new MouseEvent('mousedown', {clientX: 51, clientY: 37, composed: true, bubbles: true}); + target.dispatchEvent(mouseEvent); + + assert_equals(host.offsetLeft, 20, 'The host must be at (20px, 10px)'); + assert_equals(host.offsetTop, 10, 'The host must be at (20px, 10px)'); + assert_equals(target.offsetLeft, 45, 'The target must be at (45px, 30px)'); + assert_equals(target.offsetTop, 30, 'The target must be at (45px, 30px)'); + + assert_equals(eventLogs[0].current, target); + assert_equals(eventLogs[0].target, target); + assert_equals(eventLogs[0].offsetX, 21); // Padding edge of target is at (30px, 20px) + assert_equals(eventLogs[0].offsetY, 17); + + assert_equals(eventLogs[1].current, container); + assert_equals(eventLogs[1].target, target); + assert_equals(eventLogs[1].offsetX, 21); + assert_equals(eventLogs[1].offsetY, 17); + + assert_equals(eventLogs[3].current, host); + assert_equals(eventLogs[3].target, host); + assert_equals(eventLogs[3].offsetX, 31); // Padding edge of host is at (20px, 10px) + assert_equals(eventLogs[3].offsetY, 27); + + assert_equals(eventLogs[4].current, document.body); + assert_equals(eventLogs[4].target, host); + assert_equals(eventLogs[4].offsetX, 31); + assert_equals(eventLogs[4].offsetY, 27); +}, 'MouseEvent\'s offsetX and offsetY attributes must be relative to the shadow host when an event is dispatched inside its shadow tree.'); + +test(function () { + shadowRoot.innerHTML = '<style>' + shadowStyle + '</style><div id="container"><slot></slot></div>'; + host.innerHTML = '<style>' + shadowStyle + '</style><div id="target">Click here</div>'; + + var target = host.querySelector('#target'); + var container = shadowRoot.querySelector('#container'); + + var eventLogs = attachLoggers([target, container, shadowRoot, host, document.body]); + var mouseEvent = new MouseEvent('mousedown', {clientX: 51, clientY: 37, composed: true, bubbles: true}); + target.dispatchEvent(mouseEvent); + + assert_equals(host.offsetLeft, 20, 'The host must be at (20px, 10px)'); + assert_equals(host.offsetTop, 10, 'The host must be at (20px, 10px)'); + assert_equals(target.offsetLeft, 45, 'The target must be at (45px, 30px)'); + assert_equals(target.offsetTop, 30, 'The target must be at (45px, 30px)'); + + assert_equals(eventLogs[0].current, target); + assert_equals(eventLogs[0].target, target); + assert_equals(eventLogs[0].target.offsetParent, document.body); + assert_equals(eventLogs[0].offsetX, 6); // Padding edge of target is at (45px, 30px) + assert_equals(eventLogs[0].offsetY, 7); + + assert_equals(eventLogs[1].current, container); + assert_equals(eventLogs[1].target, target); + assert_equals(eventLogs[1].offsetX, 6); + assert_equals(eventLogs[1].offsetY, 7); + + assert_equals(eventLogs[2].current, shadowRoot); + assert_equals(eventLogs[2].target, target); + assert_equals(eventLogs[2].offsetX, 6); + assert_equals(eventLogs[2].offsetY, 7); + + assert_equals(eventLogs[3].current, host); + assert_equals(eventLogs[3].target, target); + assert_equals(eventLogs[3].offsetX, 6); + assert_equals(eventLogs[3].offsetY, 7); + + assert_equals(eventLogs[4].current, document.body); + assert_equals(eventLogs[4].target, target); + assert_equals(eventLogs[4].offsetX, 6); + assert_equals(eventLogs[4].offsetY, 7); +}, 'MouseEvent\'s offsetX and offsetY attributes must be relative to the target when an event is dispatched on a slotted content.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Node-prototype-cloneNode.html b/testing/web-platform/tests/shadow-dom/Node-prototype-cloneNode.html new file mode 100644 index 0000000000..0ee47d492e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Node-prototype-cloneNode.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> +<title>DOM: cloneNode(deep)</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="If context object is a shadow root, then it must throw a NotSupportedError."> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-node-clonenode"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +function testCloneNode(mode) { + test(function () { + assert_throws_dom('NotSupportedError', function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: mode}); + shadowRoot.cloneNode(false); + }, 'cloneNode(false) on a shadow root in ' + mode + ' mode must throw a NotSupportedError'); + + assert_throws_dom('NotSupportedError', function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: mode}); + shadowRoot.cloneNode(false); + }, 'cloneNode(true) on a closed shadow root must throw a NotSupportedError'); + + }, 'cloneNode on a shadow root in ' + mode + ' mode must throw a NotSupportedError'); +} + +testCloneNode('open'); +testCloneNode('closed'); + +test(function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: 'open'}); + + assert_equals(element.cloneNode(false).shadowRoot, null, 'cloneNode(false) on an element with an open shadow root should not clone its shadow root'); + assert_equals(element.cloneNode(true).shadowRoot, null, 'cloneNode(true) on an element with an open shadow root should not clone its shadow root'); +}, 'cloneNode on an element with an open shadow root should not clone its shadow root'); + +test(function () { + var element = document.createElement('div'); + var shadowRoot = element.attachShadow({mode: 'closed'}); + + assert_true(element.cloneNode(false).attachShadow({mode: 'closed'}) instanceof ShadowRoot, + 'An element returned by cloneNode(false) on an element with a closed shadow root should allow attachShadow'); + + assert_true(element.cloneNode(true).attachShadow({mode: 'closed'}) instanceof ShadowRoot, + 'An element returned by cloneNode(true) on an element with a closed shadow root should allow attachShadow'); + +}, 'cloneNode on an element with a closed shadow root should not clone its shadow root'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Range-prototype-insertNode.html b/testing/web-platform/tests/shadow-dom/Range-prototype-insertNode.html new file mode 100644 index 0000000000..b55ef68dcb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Range-prototype-insertNode.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(() => { + var range = document.createRange(); + var rangeContainer = document.createElement('div'); + range.setStart(rangeContainer, 0); + range.setEnd(rangeContainer, 0); + var shadowRoot = document.createElement('span').attachShadow({mode: "open"}); + var h1 = shadowRoot.appendChild(document.createElement('h1')); + + range.insertNode(shadowRoot); + assert_equals(rangeContainer.firstChild, h1); +}, 'Test if Range.prototype.insertNode() should accept ShadowRoot input.'); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/ShadowRoot-interface.html b/testing/web-platform/tests/shadow-dom/ShadowRoot-interface.html new file mode 100644 index 0000000000..de7ac91f25 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/ShadowRoot-interface.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: ShadowRoot interface</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="ShadowRoot interface and its attributes must be defined"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#the-shadowroot-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + assert_true('ShadowRoot' in window, '"ShadowRoot" exists on window'); +}, 'Check the existence of ShadowRoot interface'); + +test(function () { + assert_equals(Object.getPrototypeOf(ShadowRoot.prototype), DocumentFragment.prototype, 'ShadowRoot must inherit from DocumentFragment'); +}, 'ShadowRoot must inherit from DocumentFragment'); + +test(function () { + assert_throws_js(TypeError, function () { new ShadowRoot(); }, 'new ShadowRoot() must throw a TypeError'); +}, 'ShadowRoot must not be a constructor'); + +function testActiveElement(mode) { + test(function () { + var host = document.createElement('div'); + document.body.appendChild(host); + var shadowRoot = host.attachShadow({'mode': mode}); + shadowRoot.appendChild(document.createElement('input')); + assert_equals(shadowRoot.activeElement, null, 'ShadowRoot.host must return null if an ' + mode + ' shadow tree does not have a focused element'); + shadowRoot.firstChild.focus(); + assert_equals(shadowRoot.activeElement, shadowRoot.firstChild, 'ShadowRoot.host must return the focused element of an ' + mode + ' shadow tree'); + host.remove(); + assert_equals(shadowRoot.activeElement, null, 'ShadowRoot.host must return null if an ' + mode + ' shadow tree lost focus'); + }, 'ShadowRoot.activeElement must return the focused element of the context object when shadow root is ' + mode + '.'); +} + +testActiveElement('open'); +testActiveElement('closed'); + +test(function () { + var host1 = document.createElement('div'); + assert_equals(host1.attachShadow({'mode': 'open'}).host, host1, 'ShadowRoot.host must return the shadow host of an open shadow tree') + + var host2 = document.createElement('div'); + assert_equals(host2.attachShadow({'mode': 'closed'}).host, host2, 'ShadowRoot.host must return the shadow host of a closed shadow tree'); +}, 'ShadowRoot.host must return the shadow host of the context object.'); + +function testInnerHTML(mode) { + test(function () { + var host = document.createElement('div'); + var shadowRoot = host.attachShadow({'mode': mode}); + assert_equals(shadowRoot.innerHTML, '', 'ShadowRoot.innerHTML must be an empty string when the shadow root does not have any children'); + + shadowRoot.appendChild(document.createTextNode('hello')); + assert_equals(shadowRoot.innerHTML, 'hello', 'ShadowRoot.innerHTML must serialize a text node child'); + + shadowRoot.appendChild(document.createElement('span')); + assert_equals(shadowRoot.innerHTML, 'hello<span></span>', 'ShadowRoot.innerHTML must serialize a HTML element child'); + }, 'ShadowRoot.innerHTML must return the result of the HTML fragment serialization algorithm when shadow root is ' + mode + '.'); +} + +testInnerHTML('open'); +testInnerHTML('closed'); + +function testSetInnerHTML(mode) { + test(function () { + var host = document.createElement('div'); + var shadowRoot = host.attachShadow({'mode': mode}); + shadowRoot.innerHTML = 'hello'; + assert_equals(shadowRoot.childNodes.length, 1, 'ShadowRoot.innerHTML = "hello" must insert a single child (text node)'); + assert_true(shadowRoot.firstChild instanceof Text, 'The first child of the shadow root after ShadowRoot.innerHTML = "hello" must be a Text node'); + assert_equals(shadowRoot.firstChild.data, 'hello', 'The first Text node should contain the string "hello" after ShadowRoot.innerHTML = "hello"'); + + shadowRoot.innerHTML = '<b>hello</b>'; + assert_equals(shadowRoot.childNodes.length, 1, 'ShadowRoot.innerHTML = "<b>hello</b>" must insert a single child (b)'); + assert_true(shadowRoot.firstChild instanceof HTMLElement, 'The first child of the shadow root after ShadowRoot.innerHTML = "<b>hello</b>" must be a HTML element'); + assert_equals(shadowRoot.firstChild.localName, 'b', 'The local name of the shadow root\'s first child after ShadowRoot.innerHTML = "<b>hello</b>" must be "b"'); + assert_equals(shadowRoot.innerHTML, '<b>hello</b>', 'ShadowRoot.innerHTML must be "<b>hello</b>" after ShadowRoot.innerHTML = "<b>hello</b>"'); + + shadowRoot.innerHTML = ''; + assert_equals(shadowRoot.childNodes.length, 0, 'ShadowRoot.innerHTML = "" must remove all its children'); + }, 'ShadowRoot.innerHTML must replace all with the result of invoking the fragment parsing algorithm when shadow root is ' + mode + '.'); +} + +testSetInnerHTML('open'); +testSetInnerHTML('closed'); + +function testStyleSheets(mode) { + test(function () { + var host = document.createElement('div'); + var shadowRoot = host.attachShadow({'mode': mode}); + + shadowRoot.innerHTML = '<span></span><style> a.rule {} </style><style> b.rule {} </style>'; + assert_equals(shadowRoot.styleSheets.length, 0, 'shadowRoot.styleSheets must be empty when the shadow root is not connected'); + var styles = shadowRoot.querySelectorAll('style'); + assert_equals(styles[0].sheet, null, "Sheet should be null in a disconnected tree"); + assert_equals(styles[1].sheet, null, "Sheet should be null in a disconnected tree"); + + document.body.appendChild(host); + assert_equals(shadowRoot.styleSheets.length, 2, 'shadowRoot.styleSheets must contain two items when the shadow root has two style elements'); + assert_equals(shadowRoot.styleSheets[0], styles[0].sheet, 'shadowRoot.styleSheets[0] must be the first style element in the shadow root'); + assert_equals(shadowRoot.styleSheets[1], styles[1].sheet, 'shadowRoot.styleSheets[1] must be the second style element in the shadow root'); + + host.remove(); + assert_equals(shadowRoot.styleSheets.length, 0, 'shadowRoot.styleSheets must be empty when the shadow root is not connected'); + assert_equals(styles[0].sheet, null, "Sheet should be null in a disconnected tree"); + assert_equals(styles[1].sheet, null, "Sheet should be null in a disconnected tree"); + }, 'ShadowRoot.styleSheets must return a StyleSheetList sequence containing the shadow root style sheets when shadow root is ' + mode + '.'); +} + +testStyleSheets('open'); +testStyleSheets('closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/Slottable-mixin.html b/testing/web-platform/tests/shadow-dom/Slottable-mixin.html new file mode 100644 index 0000000000..84c7fce445 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/Slottable-mixin.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Slottable mixin</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Element and Text interfaces must implement Slottable mixin"> +<link rel="help" href="https://dom.spec.whatwg.org/#slotable"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +test(function () { + assert_true('assignedSlot' in Element.prototype, 'assignedSlot must be defined on Element.prototype'); + assert_true('assignedSlot' in document.createElement('div'), 'assignedSlot must be defined on a div element'); + + assert_true('assignedSlot' in Text.prototype, 'assignedSlot must be defined on Text.prototype'); + assert_true('assignedSlot' in document.createTextNode(''), 'assignedSlot must be defined on a text node'); + assert_false('assignedSlot' in document.createComment(''), 'assignedSlot must not be defined on a comment node'); + assert_false('assignedSlot' in document.createProcessingInstruction('target', 'data'), 'assignedSlot must not be defined on a processing instruction node'); + +}, 'assignedSlot attribute must be defined on Element and Text interfaces'); + +test(function () { + assert_equals(document.createElement('div').assignedSlot, null, 'assignedSlot must be null when the element is not in any tree'); + + var shadowHost = document.createElement('div'); + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + + var childElement = document.createElement('b'); + shadowHost.appendChild(childElement); + assert_equals(childElement.assignedSlot, null, 'assignedSlot on an element must be null when a node is not assigned of any slot'); + + var childTextNode = document.createTextNode(''); + shadowHost.appendChild(childTextNode); + assert_equals(childTextNode.assignedSlot, null, 'assignedSlot on a text node must be null when a node is not assigned of any slot'); + + var slot = document.createElement('slot'); + slot.name = 'foo'; + shadowRoot.appendChild(slot); + assert_equals(childElement.assignedSlot, null, 'assignedSlot on an element must be null when a node does not match any slot'); + assert_equals(childTextNode.assignedSlot, null, 'assignedSlot on a text node must be null when a node does not match any slot'); + +}, 'assignedSlot must return null when the node does not have an assigned node'); + +test(function () { + var shadowHost = document.createElement('div'); + var childElement = document.createElement('b'); + shadowHost.appendChild(childElement); + + var childTextNode = document.createTextNode(''); + shadowHost.appendChild(childTextNode); + + var shadowRoot = shadowHost.attachShadow({mode: 'open'}); + var slot = document.createElement('slot'); + shadowRoot.appendChild(slot); + + assert_equals(childElement.assignedSlot, slot, 'assignedSlot on an element must return the assigned default slot element'); + assert_equals(childTextNode.assignedSlot, slot, 'assignedSlot on a text node must return the assigned default slot element'); + + slot.name = 'foo'; + assert_equals(childElement.assignedSlot, null, 'assignedSlot on an element must null when the element is unassigned from a slot element'); + assert_equals(childTextNode.assignedSlot, null, 'assignedSlot on a text node must null when the node is unassigned from a slot element'); + + childElement.slot = 'foo'; + assert_equals(childElement.assignedSlot, slot, 'assignedSlot on an element must return the re-assigned slot element'); + + slot.removeAttribute('name'); + assert_equals(childTextNode.assignedSlot, slot, 'assignedSlot on a text node must return the re-assigned slot element'); + +}, 'assignedSlot must return the assigned slot'); + +test(function () { + var shadowHost = document.createElement('div'); + var childElement = document.createElement('b'); + shadowHost.appendChild(childElement); + + var childTextNode = document.createTextNode(''); + shadowHost.appendChild(childTextNode); + + var shadowRoot = shadowHost.attachShadow({mode: 'closed'}); + var slot = document.createElement('slot'); + shadowRoot.appendChild(slot); + + assert_equals(childElement.assignedSlot, null, 'assignedSlot on an element must return null if the slot is inside a closed shadow tree.'); + assert_equals(childTextNode.assignedSlot, null, 'assignedSlot on a text node must return null if the slot is inside a closed shadow tree.'); + +}, 'assignedSlot must return null when the assigned slot element is inside a closed shadow tree'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/WEB_FEATURES.yml b/testing/web-platform/tests/shadow-dom/WEB_FEATURES.yml new file mode 100644 index 0000000000..171923c067 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: shadow-dom + files: "**" diff --git a/testing/web-platform/tests/shadow-dom/accesskey.tentative.html b/testing/web-platform/tests/shadow-dom/accesskey.tentative.html new file mode 100644 index 0000000000..93eb912be0 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/accesskey.tentative.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: accesskey</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/accesskey.js"></script> +</head> +<body> +<div id="log"></div> +<div id="container" style="position: relative"></div> +<script> + +const container = document.getElementById('container'); + +function testAccesskeyInShadowTree(mode) { + promise_test(async t => { + const host = document.createElement('div'); + container.appendChild(host); + t.add_cleanup(() => host.remove()); + + const shadowRoot = host.attachShadow({mode}); + shadowRoot.innerHTML = '<button id="button" accesskey="g">Click Me with Shift+Alt+g or on Mac with Control+Option+g</button>'; + + let el = shadowRoot.getElementById("button"); + let eventWatcher = new EventWatcher(t, el, ['click']); + let waitForClick = eventWatcher.wait_for('click'); + + await pressAccessKey("g"); + await waitForClick; + }, `button element with accesskey in the shadow tree of ${mode} mode`); +} + +testAccesskeyInShadowTree("open"); +testAccesskeyInShadowTree("closed"); + +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html b/testing/web-platform/tests/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html new file mode 100644 index 0000000000..bcb4ee62e0 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html @@ -0,0 +1,191 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Capturing event listeners should be invoked before bubbling event listeners</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./resources/shadow-dom.js"></script> +<script> + +function attachEventListeners(eventType, tree) { + const eventLogs = []; + const makeComposedPathResult = (event) => event.composedPath().map((node) => node.id) + for (const id in tree) { + const node = tree[id]; + node.addEventListener(eventType, event => eventLogs.push( + ['bubbling', event.eventPhase, event.target.id, event.currentTarget.id, makeComposedPathResult(event)]), {capture: false}); + node.addEventListener(eventType, event => eventLogs.push( + ['capturing', event.eventPhase, event.target.id, event.currentTarget.id, makeComposedPathResult(event)]), {capture: true}); + } + return eventLogs; +} + +</script> +</head> +<body> + +<div id="test1"> + <div id="parent"> + <div id="target"></div> + </div> +</div> +<script> +test(() => { + const tree = createTestTree(document.getElementById('test1')); + const logs = attachEventListeners('my-event', tree); + tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); + + const composedPath = ['target', 'parent', 'test1']; + assert_object_equals(logs, [ + ['capturing', Event.CAPTURING_PHASE, 'target', 'test1', composedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'parent', composedPath], + ['capturing', Event.AT_TARGET, 'target', 'target', composedPath], + ['bubbling', Event.AT_TARGET, 'target', 'target', composedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'parent', composedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'test1', composedPath], + ]); +}, 'Capturing event listeners should be invoked before bubbling event listeners on the target without shadow trees'); +</script> + +<div id="test2"> + <div id="host"> + <template id="shadowRoot" data-mode="closed"> + <div id="target"></div> + </template> + </div> +</div> +<script> +test(() => { + const tree = createTestTree(document.getElementById('test2')); + const logs = attachEventListeners('my-event', tree); + tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); + + const innerComposedPath = ['target', 'shadowRoot', 'host', 'test2']; + const outerComposedPath = ['host', 'test2']; + assert_object_equals(logs, [ + ['capturing', Event.CAPTURING_PHASE, 'host', 'test2', outerComposedPath], + ['capturing', Event.AT_TARGET, 'host', 'host', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'shadowRoot', innerComposedPath], + ['capturing', Event.AT_TARGET, 'target', 'target', innerComposedPath], + ['bubbling', Event.AT_TARGET, 'target', 'target', innerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'shadowRoot', innerComposedPath], + ['bubbling', Event.AT_TARGET, 'host', 'host', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'host', 'test2', outerComposedPath], + ]); +}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree'); +</script> + +<div id="test3"> + <div id="outerHost"> + <template id="outerShadowRoot" data-mode="closed"> + <div id="innerHost"> + <template id="innerShadowRoot" data-mode="closed"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> +<script> +test(() => { + const tree = createTestTree(document.getElementById('test3')); + const logs = attachEventListeners('my-event', tree); + tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); + + const innerShadowComposedPath = ['target', 'innerShadowRoot', 'innerHost', 'outerShadowRoot', 'outerHost', 'test3']; + const outerShadowComposedPath = ['innerHost', 'outerShadowRoot', 'outerHost', 'test3']; + const outerComposedPath = ['outerHost', 'test3']; + assert_object_equals(logs, [ + ['capturing', Event.CAPTURING_PHASE, 'outerHost', 'test3', outerComposedPath], + ['capturing', Event.AT_TARGET, 'outerHost', 'outerHost', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'innerHost', 'outerShadowRoot', outerShadowComposedPath], + ['capturing', Event.AT_TARGET, 'innerHost', 'innerHost', outerShadowComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'innerShadowRoot', innerShadowComposedPath], + ['capturing', Event.AT_TARGET, 'target', 'target', innerShadowComposedPath], + + ['bubbling', Event.AT_TARGET, 'target', 'target', innerShadowComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'innerShadowRoot', innerShadowComposedPath], + ['bubbling', Event.AT_TARGET, 'innerHost', 'innerHost', outerShadowComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'innerHost', 'outerShadowRoot', outerShadowComposedPath], + ['bubbling', Event.AT_TARGET, 'outerHost', 'outerHost', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'outerHost', 'test3', outerComposedPath], + ]); +}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a doubly nested shadow tree'); +</script> + +<div id="test4"> + <div id="host"> + <template id="shadowRoot" data-mode="closed"> + <slot id="slot"></slot> + </template> + <div id="target"></div> + </div> +</div> +<script> +test(() => { + const tree = createTestTree(document.getElementById('test4')); + const logs = attachEventListeners('my-event', tree); + tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); + + const innerComposedPath = ['target', 'slot', 'shadowRoot', 'host', 'test4']; + const outerComposedPath = ['target', 'host', 'test4']; + assert_object_equals(logs, [ + ['capturing', Event.CAPTURING_PHASE, 'target', 'test4', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'host', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'shadowRoot', innerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'slot', innerComposedPath], + ['capturing', Event.AT_TARGET, 'target', 'target', outerComposedPath], + + ['bubbling', Event.AT_TARGET, 'target', 'target', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'slot', innerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'shadowRoot', innerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'host', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'test4', outerComposedPath], + ]); +}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched via a slot'); +</script> + +<div id="test5"> + <div id="upperHost"> + <template id="upperShadowRoot" data-mode="closed"> + <slot id="upperSlot"></slot> + </template> + <div id="lowerHost"> + <template id="lowerShadowRoot" data-mode="closed"> + <div id="target"></div> + </template> + </div> + </div> +</div> +<script> +test(() => { + const tree = createTestTree(document.getElementById('test5')); + const logs = attachEventListeners('my-event', tree); + tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); + + const lowerComposedPath = ['target', 'lowerShadowRoot', 'lowerHost', 'upperHost', 'test5']; + const upperComposedPath = ['lowerHost', 'upperSlot', 'upperShadowRoot', 'upperHost', 'test5']; + const outerComposedPath = ['lowerHost', 'upperHost', 'test5']; + assert_object_equals(logs, [ + ['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'test5', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperHost', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperShadowRoot', upperComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperSlot', upperComposedPath], + ['capturing', Event.AT_TARGET, 'lowerHost', 'lowerHost', outerComposedPath], + ['capturing', Event.CAPTURING_PHASE, 'target', 'lowerShadowRoot', lowerComposedPath], + ['capturing', Event.AT_TARGET, 'target', 'target', lowerComposedPath], + + ['bubbling', Event.AT_TARGET, 'target', 'target', lowerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'target', 'lowerShadowRoot', lowerComposedPath], + ['bubbling', Event.AT_TARGET, 'lowerHost', 'lowerHost', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperSlot', upperComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperShadowRoot', upperComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperHost', outerComposedPath], + ['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'test5', outerComposedPath], + ]); +}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree which passes through another shadow tree'); +</script> + +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/crashtests/move-to-new-tree-1343016.html b/testing/web-platform/tests/shadow-dom/crashtests/move-to-new-tree-1343016.html new file mode 100644 index 0000000000..853884b993 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/crashtests/move-to-new-tree-1343016.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html class="test-wait"> +<head> +<title>Shadow DOM: Move to new tree without crashing</title> +<meta name="author" title="Frank Liberato" href="mailto:liberato@google.com"> +<script src="/resources/testdriver.js"></script> +<script src='/resources/testdriver-vendor.js'></script> +</head> + +<body> +<video controls id="video"></video> + +<script> + +async function crash() { + await test_driver.bless('open popup window', () => { + let w = window.open("", "", "popup"); + w.document.body.appendChild(document.getElementById("video")); + }); + document.documentElement.removeAttribute("class"); +} + +crash(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-after-attachshadow.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-after-attachshadow.html new file mode 100644 index 0000000000..bfe2d66cfa --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-after-attachshadow.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM after attachShadow</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +let gotHost = false; +function myObserver(mutationsList, observer) { + for (let mutation of mutationsList) { + for (let n of mutation.addedNodes) { + if (n.id === 'host') { + gotHost = true; + const shadowRoot = n.attachShadow( {mode: 'closed'}); + assert_true(!!shadowRoot, 'Unable to attach shadow imperatively'); + } + } + } +} +const observer = new MutationObserver(myObserver); +observer.observe(document.body, { childList: true, subtree: true }); +assert_false(gotHost, 'No mutations yet'); +</script> + +<div id=host> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> + <template shadowrootmode=open> + Content + <slot>Fallback</slot> + </template> +</div> + +<script> +test(t => { + t.add_cleanup(function() { observer.disconnect(); }); + assert_true(gotHost); + let host = document.querySelector('#host'); + let template = host.querySelector('template'); + assert_true(!!template, 'Declarative shadow attach should fail, so template should be left over'); + assert_true(!host.shadowRoot, 'Shadow root should be closed (from attachShadow call)'); +}, 'Declarative Shadow DOM: declarative shadow should fail if attachShadow() already called'); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-parser-interaction.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-parser-interaction.html new file mode 100644 index 0000000000..f5ff13822b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-parser-interaction.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Declarative Shadow DOM</title> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://github.com/whatwg/dom/issues/831"> +<link rel="help" href="https://crbug.com/1203645"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="support/helpers.js"></script> + + +This test should not crash, and there should be two lines of text visible below. +<x-1> + <template shadowrootmode="open"> + <style> + @import 'non-existent.css'; + </style> + <slot></slot> + </template> + <p>Line 1</p> +</x-1> + +<x-2> + <template shadowrootmode="open"> + <slot></slot> + </template> + <p>Line 2</p> +</x-2> + +<script> + window.onload = function() { + const x1 = document.querySelector('x-1'); + const x2 = document.querySelector('x-2'); + test(() => { + assert_true(!!x1); + assert_true(!!x2); + }, 'Declarative Shadow DOM: Test for crashes and improper parsing'); + } +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-attachment.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-attachment.html new file mode 100644 index 0000000000..aec9a276f9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-attachment.html @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM Element Attachment</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../html/resources/common.js'></script> +<script src="support/helpers.js"></script> + +<script> +const shadowContent = '<span>Shadow tree</span><slot></slot>'; +function getDeclarativeContent(mode, delegatesFocus) { + const delegatesFocusText = delegatesFocus ? ' shadowrootdelegatesfocus' : ''; + return `<template shadowrootmode=${mode}${delegatesFocusText}>${shadowContent}</template>`; +} + +const lightDomTextContent = 'Light DOM'; +function addDeclarativeShadowRoot(elementType, mode, delegatesFocus) { + const declarativeString = `<${elementType} id=theelement>${getDeclarativeContent(mode, delegatesFocus)} + <span class='lightdom'>${lightDomTextContent}</span></${elementType}>`; + const wrapper = document.createElement('div'); + wrapper.setHTMLUnsafe(declarativeString); + const element = wrapper.querySelector('#theelement'); + return {wrapper: wrapper, element: element}; +} + +function testElementType(allowed, nochildren, elementType, mode, delegatesFocus) { + var t = test(function() { + const nodes = addDeclarativeShadowRoot(elementType, mode, delegatesFocus); + if (allowed) { + const element = nodes.element; + assert_true(!!element, 'Unable to locate the element'); + // Just one light DOM child, and no leftover template. + assert_true(!nodes.wrapper.querySelector('template')); + assert_equals(element.children.length, 1); + assert_equals(element.children[0].textContent, lightDomTextContent); + let originalShadowRoot = null; + if (mode === 'open') { + assert_true(!!element.shadowRoot, 'Shadow root should be present'); + assert_equals(element.shadowRoot.innerHTML, shadowContent, 'Correct shadow content'); + assert_equals(element.shadowRoot.delegatesFocus,delegatesFocus,'Correct delegatesFocus') + originalShadowRoot = element.shadowRoot; + } + + const oppositeMode = (mode === 'open') ? 'closed' : 'open'; + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: oppositeMode}); + }, 'Calling attachShadow with a declarative shadow fails if the mode doesn\'t match'); + + // Now, call attachShadow() and make sure we get back the same (original) shadowRoot, but empty. + const newShadow = element.attachShadow({mode: mode, delegatesFocus: delegatesFocus}); + if (mode === 'open') { + assert_equals(element.shadowRoot, originalShadowRoot, 'The same shadow root should be returned'); + assert_equals(element.shadowRoot.innerHTML, '', 'Empty shadow content'); + assert_equals(element.shadowRoot.mode, mode, 'Original shadow mode'); + } + + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: mode}); + }, 'Calling attachShadow a second time on an element with a declarative shadow fails (same mode)'); + + assert_throws_dom('NotSupportedError', () => { + element.attachShadow({mode: oppositeMode}); + }, 'Calling attachShadow a second time on an element with a declarative shadow fails (opposite mode)'); + } else { + if (!nochildren) { + // Invalid elements should retain a <template> element child with a shadowrootmode attribute. + const template = nodes.wrapper.querySelector('template[shadowrootmode]'); + assert_true(!!template); + assert_equals(template.getAttribute('shadowrootmode'), mode, `Template with shadowrootmode=${mode} should be left over`); + const span = nodes.wrapper.querySelector('span.lightdom'); + assert_true(!!span); + assert_equals(span.textContent, lightDomTextContent); + if (nodes.element) { + // For some tags (e.g. <html>) there won't be an element inside wrapper. + assert_true(!nodes.element.shadowRoot, 'Shadow root should not be present'); + } + } + } + }, `Declarative Shadow DOM as a child of <${elementType}>, with mode=${mode}, delegatesFocus=${delegatesFocus}. Should be ${allowed ? 'safelisted' : 'disallowed'}.`); +} + +function runAllTests() { + const noCheck = ['body', 'template']; + const safelisted = HTML5_SHADOW_ALLOWED_ELEMENTS.filter(el => !noCheck.includes(el)); + const disallowed = HTML5_SHADOW_DISALLOWED_ELEMENTS.filter(el => !noCheck.includes(el)); + const noChildElements = ['iframe','noscript','script','select','style','textarea','title']; + for (let delegatesFocus of [false, true]) { + for (let mode of ['open', 'closed', 'invalid']) { + for (let elementName of safelisted) { + testElementType(mode !== 'invalid', false, elementName, mode, delegatesFocus); + } + for (let elementName of disallowed) { + testElementType(false, noChildElements.includes(elementName), elementName, mode, delegatesFocus); + } + } + } +} + +runAllTests(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-available-to-element-internals.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-available-to-element-internals.html new file mode 100644 index 0000000000..16fe6fd1d5 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-available-to-element-internals.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Declarative Shadow DOM</title> +<link rel="author" href="mailto:avandolder@mozilla.com"> +<link rel="help" href="https://html.spec.whatwg.org/#parsing-main-inhead:available-to-element-internals"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<custom-element> + <template shadowrootmode="open"> + </template> +</custom-element> + +<script> + let elementInternals; + customElements.define("custom-element", class extends HTMLElement { + constructor() { + super(); + elementInternals = this.attachInternals(); + } + }); + + window.onload = () => { + test(() => { + assert_true(!!elementInternals); + assert_true(!!elementInternals.shadowRoot); + assert_equals(elementInternals.shadowRoot, document.querySelector("custom-element").shadowRoot); + }, "Declarative Shadow DOM: shadow root should be available to element internals"); + }; +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-basic.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-basic.html new file mode 100644 index 0000000000..8bc6bec5f5 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-basic.html @@ -0,0 +1,329 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Declarative Shadow DOM</title> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://github.com/whatwg/dom/issues/831"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div id="host" style="display:none"> + <template shadowrootmode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> +</div> + +<script> +test(() => { + const host = document.querySelector('#host'); + const c1 = host.querySelector('#c1'); + assert_true(!!c1); + assert_equals(host.querySelector('#s1'), null, "Should be inside shadow root"); + assert_equals(host.querySelector('template'), null, "No leftover template node"); + assert_true(!!host.shadowRoot,"No shadow root found"); + const s1 = host.shadowRoot.querySelector('#s1'); + assert_equals(c1.assignedSlot, s1); + assert_array_equals(s1.assignedNodes(), [c1]); +}, 'Declarative Shadow DOM: Basic test'); + +test(() => { + assert_true(HTMLTemplateElement.prototype.hasOwnProperty("shadowRootMode"),'Unable to feature detect'); +}, 'Declarative Shadow DOM: Feature detection'); + +test(() => { + const t = document.createElement('template'); + t.setAttribute('shadowrootmode','open'); + assert_equals(t.shadowRootMode,'open','The shadowRootMode IDL should reflect the content attribute'); + t.setAttribute('shadowrootmode','closed'); + assert_equals(t.shadowRootMode,'closed','"open" and "closed" should both be valid values'); + t.setAttribute('shadowrootmode','OpEn'); + assert_equals(t.shadowRootMode,'open','Case insensitive'); + t.setAttribute('shadowrootmode','INVALID'); + assert_equals(t.shadowRootMode,'','Invalid values map to empty string'); + t.removeAttribute('shadowrootmode'); + assert_equals(t.shadowRootMode,'','No shadowrootmode attribute maps to empty string'); +}, 'Shadowrootmode reflection'); + +test(() => { + const t = document.createElement('template'); + t.shadowRootMode = 'blah'; + assert_equals(t.shadowRootMode, ''); + t.getAttribute('shadowrootmode', 'blah'); + t.shadowRootMode = 'CLOSED'; + assert_equals(t.shadowRootMode, 'closed'); + t.getAttribute('shadowrootmode', 'CLOSED'); +}, 'Shadowrootmode reflection, setter'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> + `); + const host = div.querySelector('#host'); + const c1 = host.querySelector('#c1'); + assert_true(!!c1); + assert_equals(host.querySelector('#s1'), null, "Should be inside shadow root"); + assert_equals(host.querySelector('template'), null, "No leftover template node"); + assert_true(!!host.shadowRoot,"No shadow root found"); + const s1 = host.shadowRoot.querySelector('#s1'); + assert_equals(c1.assignedSlot, s1); + assert_array_equals(s1.assignedNodes(), [c1]); +}, 'Declarative Shadow DOM: Fragment parser basic test'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="invalid"> + </template> + </div> + `); + const host = div.querySelector('#host'); + assert_equals(host.shadowRoot, null, "Shadow root was found"); + const tmpl = host.querySelector('template'); + assert_true(!!tmpl,"Template should still be present"); + const shadowrootAttr = tmpl.getAttribute('shadowrootmode'); + assert_equals(shadowrootAttr,"invalid","'shadowrootmode' attribute should still be present"); +}, 'Declarative Shadow DOM: Invalid shadow root attribute'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="closed"> + </template> + </div> + `); + const host = div.querySelector('#host'); + assert_equals(host.shadowRoot, null, "Closed shadow root"); + assert_equals(host.querySelector('template'), null, "No template - converted to shadow root"); +}, 'Declarative Shadow DOM: Closed shadow root attribute'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open"> + <slot id="s1" name="slot1"></slot> + </div> + `); + const host = div.querySelector('#host'); + assert_equals(host.querySelector('#s1'), null, "Should be inside shadow root"); + assert_equals(host.querySelector('template'), null, "No leftover template node"); + assert_true(!!host.shadowRoot,"No shadow root found"); + const s1 = host.shadowRoot.querySelector('#s1'); + assert_true(!!s1,"Slot should be inside the shadow root"); +}, 'Declarative Shadow DOM: Missing closing tag'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open" shadowrootdelegatesfocus> + </template> + </div> + `); + var host = div.querySelector('#host'); + assert_true(!!host.shadowRoot,"No shadow root found"); + assert_true(host.shadowRoot.delegatesFocus,"delegatesFocus should be true"); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open"> + </template> + </div> + `); + host = div.querySelector('#host'); + assert_true(!!host.shadowRoot,"No shadow root found"); + assert_false(host.shadowRoot.delegatesFocus,"delegatesFocus should be false without the shadowrootdelegatesfocus attribute"); +}, 'Declarative Shadow DOM: delegates focus attribute'); + +test(() => { + const div = document.createElement('div'); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open" shadowrootclonable> + </template> + </div> + `); + var host = div.querySelector('#host'); + assert_true(!!host.shadowRoot,"No shadow root found"); + assert_true(host.shadowRoot.clonable,"clonable should be true"); + div.setHTMLUnsafe(` + <div id="host"> + <template shadowrootmode="open"> + </template> + </div> + `); + host = div.querySelector('#host'); + assert_true(!!host.shadowRoot,"No shadow root found"); + assert_false(host.shadowRoot.clonable,"clonable should be false without the shadowrootclonable attribute"); +}, 'Declarative Shadow DOM: clonable attribute'); +</script> + +<div id="multi-host" style="display:none"> + <template shadowrootmode="open"> + <span>root 1</span> + </template> + <template shadowrootmode="closed"> + <span>root 2</span> + </template> +</div> +<script> +test(() => { + const host = document.querySelector('#multi-host'); + const leftover = host.querySelector('template'); + assert_true(!!leftover, "The second (duplicate) template should be left in the DOM"); + assert_true(leftover instanceof HTMLTemplateElement); + assert_equals(leftover.getAttribute('shadowrootmode'),"closed"); + assert_equals(leftover.shadowRootMode,"closed"); + assert_true(!!host.shadowRoot,"No open shadow root found - first root should remain"); + const innerSpan = host.shadowRoot.querySelector('span'); + assert_equals(innerSpan.textContent, 'root 1', "Content should come from first declarative shadow root"); +}, 'Declarative Shadow DOM: Multiple roots'); + +</script> + +<template id="template-containing-shadow"> + <div class="innerdiv"> + <template shadowrootmode=open shadowrootclonable>Content</template> + </div> +</template> +<script> +test(() => { + const template = document.querySelector('#template-containing-shadow'); + const container1 = document.createElement('div'); + container1.style.display = 'none'; + document.body.appendChild(container1); + container1.appendChild(template.content.cloneNode(true)); + let innerDiv = container1.querySelector('div.innerdiv'); + const shadowRoot1 = innerDiv.shadowRoot; + assert_true(!!shadowRoot1,"Inner div should have a shadow root"); + assert_equals(innerDiv.querySelector('template'), null, "No leftover template node"); + + const container2 = document.createElement('div'); + container2.style.display = 'none'; + document.body.appendChild(container2); + container2.appendChild(template.content.cloneNode(true)); + innerDiv = container2.querySelector('div.innerdiv'); + const shadowRoot2 = innerDiv.shadowRoot; + assert_true(!!shadowRoot2,"Inner div should have a shadow root"); + assert_equals(innerDiv.querySelector('template'), null, "No leftover template node"); + + assert_not_equals(shadowRoot1,shadowRoot2,'Should not get back the same shadow root'); + + // Make sure importNode also works. + const container3 = document.createElement('div'); + container3.style.display = 'none'; + document.body.appendChild(container3); + container3.appendChild(document.importNode(template.content,true)); + innerDiv = container3.querySelector('div.innerdiv'); + const shadowRoot3 = innerDiv.shadowRoot; + assert_true(!!shadowRoot3,"Inner div should have a shadow root"); + assert_equals(innerDiv.querySelector('template'), null, "No leftover template node"); + assert_not_equals(shadowRoot1,shadowRoot3,'Should not get back the same shadow root'); + +}, 'Declarative Shadow DOM: template containing declarative shadow root (with shadowrootclonable)'); +</script> + +<template id="template-containing-deep-shadow"> + <div><div><div><div><div> + <div class="innerdiv"> + <template shadowrootmode=open shadowrootclonable>Content</template> + </div> + </div></div></div></div></div> +</template> +<script> +test(() => { + const template = document.querySelector('#template-containing-deep-shadow'); + const host = document.createElement('div'); + host.style.display = 'none'; + document.body.appendChild(host); + host.appendChild(template.content.cloneNode(true)); + assert_true(!!host.querySelector('div.innerdiv').shadowRoot,"Inner div should have a shadow root"); +}, 'Declarative Shadow DOM: template containing (deeply nested) declarative shadow root'); +</script> + +<template id="template-containing-template"> + <div> + <template id="inner-template"> + <div class="innerdiv"> + <template shadowrootmode=open shadowrootclonable>Content</template> + </div> + </template> + </div> +</template> +<script> +test(() => { + const template = document.querySelector('#template-containing-template'); + const host = document.createElement('div'); + host.style.display = 'none'; + document.body.appendChild(host); + host.appendChild(template.content.cloneNode(true)); + const innerTemplate = host.querySelector('#inner-template'); + assert_true(!!innerTemplate.content.querySelector('div.innerdiv').shadowRoot,"Inner div should have a shadow root"); +}, 'Declarative Shadow DOM: template containing a template containing declarative shadow root'); +</script> + +<template id="template-containing-ua-shadow"> + <div class="innerdiv"> + <template shadowrootmode=open shadowrootclonable> + <video></video> <!--Assumed to have UA shadow root--> + </template> + </div> +</template> +<script> +test(() => { + const template = document.querySelector('#template-containing-ua-shadow'); + const host = document.createElement('div'); + host.style.display = 'none'; + document.body.appendChild(host); + // Mostly make sure clone of template *does* clone the + // shadow root, and doesn't crash on cloning the <video>. + host.appendChild(template.content.cloneNode(true)); + let innerDiv = host.querySelector('div.innerdiv'); + const shadowRoot = innerDiv.shadowRoot; + assert_true(!!shadowRoot,"Inner div should have a shadow root"); + assert_equals(innerDiv.querySelector('template'), null, "No leftover template node"); + assert_true(!!innerDiv.shadowRoot.querySelector('video'),'Video element should be present'); +}, 'Declarative Shadow DOM: template containing declarative shadow root and UA shadow root'); +</script> + +<template id="template-containing-ua-shadow-closed"> + <div class="innerdiv"> + <template shadowrootmode=closed> + <video></video> <!--Assumed to have UA shadow root--> + </template> + </div> +</template> +<script> +test(() => { + const template = document.querySelector('#template-containing-ua-shadow-closed'); + const host = document.createElement('div'); + host.style.display = 'none'; + document.body.appendChild(host); + host.appendChild(template.content.cloneNode(true)); + let innerDiv = host.querySelector('div.innerdiv'); + assert_true(!innerDiv.shadowRoot,"Inner div should have a closed shadow root"); +}, 'Declarative Shadow DOM: template containing closed declarative shadow root and UA shadow root'); +</script> + +<template id="root-element-shadow"> + <template shadowrootmode=open>Content</template> +</template> +<script> +test(() => { + // Root element of this template is a <template shadowroot>: + const template = document.querySelector('#root-element-shadow'); + const host = document.createElement('div'); + host.appendChild(template.content.cloneNode(true)); + assert_equals(host.shadowRoot, null, "Shadow root should not be present"); + const tmpl = host.querySelector('template'); + assert_true(!!tmpl,"Template should still be present"); + assert_equals(tmpl.getAttribute('shadowrootmode'),"open","'shadowrootmode' attribute should still be present"); +}, 'Declarative Shadow DOM: template root element'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-opt-in.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-opt-in.html new file mode 100644 index 0000000000..1b8c168552 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-opt-in.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Declarative Shadow DOM</title> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://github.com/whatwg/dom/issues/831"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src='../../html/resources/common.js'></script> + +<body> +<style> + * { white-space: pre; } + iframe { display:none; } +</style> +<div id=log></div> + +<div id=mainpage style="display:none"> + <div class=wrapper> + <div class=host> + <template shadowrootmode=open> + <span class=content>Content</span> + </template> + </div> + </div> +</div> + +<script> +const content = ` + <html><body> + <div class=wrapper> + <div class=host> + <template shadowrootmode=open> + <span class=content>Content</span> + </template> + </div> + </div> + </body></html> +`; + +function assert_dsd(el,shouldHaveShadow) { + const wrapper = el.querySelector('.wrapper'); + assert_true(!!wrapper,'Unable to find wrapper element'); + const host = wrapper.querySelector('.host'); + assert_true(!!host,'Unable to find host element'); + if (shouldHaveShadow) { + assert_true(!!host.shadowRoot, 'Shadow root NOT FOUND.'); + assert_true(!!host.shadowRoot.querySelector('.content'),'Unable to locate content'); + } else { + assert_true(!host.shadowRoot, 'Shadow root FOUND - none should be present.'); + const tmpl = host.querySelector('template'); + assert_true(!!tmpl, 'The template should be left as a <template> element'); + assert_equals(tmpl.getAttribute('shadowrootmode'),'open','The shadowrootmode attribute should still be present'); + assert_true(!!tmpl.content.querySelector('.content'),'Unable to locate content'); + } +} + +test(() => { + const div = document.getElementById('mainpage'); + assert_dsd(div,true); +}, 'Non-fragment parsing needs no opt-in'); + +const noChildElements = ['iframe','noscript','script','select','style','template','textarea','title','colgroup']; +const elements = HTML5_ELEMENTS.filter(el => !noChildElements.includes(el)); +for (let elementName of elements) { + var t = test(function() { + const el1 = document.createElement(elementName); + el1.innerHTML = content; + assert_dsd(el1,false); + + const templateContent = `<template id=tmpl>${content}</template>`; + const el2 = document.createElement('div'); + el2.innerHTML = templateContent; + assert_dsd(el2.querySelector('#tmpl').content,false); + }, `innerHTML on a <${elementName}>`); +} + +test(() => { + const temp = document.createElement('template'); + temp.innerHTML = content; + assert_dsd(temp.content,false, 'innerHTML should not allow declarative shadow content'); +}, 'innerHTML on template'); + +test(() => { + const templateContent = `<template id=tmpl>${content}</template>`; + const temp = document.createElement('template'); + temp.innerHTML = templateContent; + assert_dsd(temp.content.querySelector('#tmpl').content,false); +}, 'innerHTML on template, with nested template content'); + +test(() => { + const div = document.createElement('div'); + const shadow = div.attachShadow({mode: 'open'}); + shadow.innerHTML = content; + assert_dsd(shadow,false); +}, 'innerHTML on shadowRoot'); + +test(() => { + const parser = new DOMParser(); + let fragment = parser.parseFromString(content, 'text/html'); + assert_dsd(fragment.body,false); + fragment = parser.parseFromString(content, 'text/html', {includeShadowRoots: false}); + assert_dsd(fragment.body,false); + fragment = parser.parseFromString(content, 'text/html', {includeShadowRoots: true}); + assert_dsd(fragment.body,false); +}, 'DOMParser (includeShadowRoots is historical)'); + +test(() => { + const doc = document.implementation.createHTMLDocument(''); + doc.body.innerHTML = content; + assert_dsd(doc.body,false); +}, 'createHTMLDocument with innerHTML - not supported'); + +test(() => { + const doc = document.implementation.createHTMLDocument(''); + let range = doc.createRange(); + range.selectNode(doc.body); + let documentFragment = range.createContextualFragment(content); + assert_dsd(documentFragment,false); +}, 'createContextualFragment - not supported'); + +async_test((t) => { + let client = new XMLHttpRequest(); + client.addEventListener('load', t.step_func_done(() => { + assert_true(client.status == 200 && client.responseXML != null); + assert_dsd(client.responseXML.body,false); + t.done(); + })); + client.open("GET", `data:text/html,${content}`); + client.responseType = 'document'; + client.send(); +}, 'XMLHttpRequest - not supported'); + +test(() => { + const div = document.createElement('div'); + div.insertAdjacentHTML('afterbegin',content); + assert_dsd(div,false); +}, 'insertAdjacentHTML on element - not supported'); + +test(() => { + const id = 'doc-write-1'; + document.write(`<div id=${id} style="display:none">${content}</div>`); + assert_dsd(document.getElementById(id),true); +}, 'document.write allowed from synchronous script loaded from main document'); + +test(() => { + const id = 'doc-write-2'; + const doc = document.implementation.createHTMLDocument(''); + doc.write(`<div id=${id}>${content}</div>`); + assert_dsd(doc.getElementById(id),false); +}, 'document.write disallowed on fresh document'); + + +async_test((t) => { + const iframe = document.createElement('iframe'); + iframe.style.display = "none"; + iframe.sandbox = "allow-same-origin"; + document.body.appendChild(iframe); + iframe.addEventListener('load', t.step_func_done(() => { + assert_dsd(iframe.contentDocument.body,true); + t.done(); + })); + iframe.srcdoc = content; +}, 'iframe'); + +async_test((t) => { + const iframe = document.createElement('iframe'); + iframe.style.display = "none"; + document.body.appendChild(iframe); + iframe.addEventListener('load', t.step_func_done(() => { + assert_dsd(iframe.contentDocument.body,true); + t.done(); + })); + iframe.srcdoc = content; +}, 'iframe, no sandbox'); + +function getHandler(t, name, shouldHaveShadow) { + return (e) => { + t.step(() => { + if (e.data.name == name) { + assert_false(e.data.error,e.data.msg); + assert_true(e.data.hasShadow == shouldHaveShadow); + t.done(); + } + }); + }; +} +async_test((t) => { + window.addEventListener('message', getHandler(t, 'iframe-sandbox', true)); +}, 'sandboxed iframe allows declarative Shadow DOM'); + +async_test((t) => { + window.addEventListener('message', getHandler(t,'iframe-no-sandbox', true)); +}, 'iframe with no sandbox allows declarative Shadow DOM'); + +</script> + +<iframe name="iframe-sandbox" sandbox="allow-scripts" src="support/declarative-child-frame.html" ></iframe> +<iframe name="iframe-no-sandbox" src="support/declarative-child-frame.html"></iframe> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-repeats.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-repeats.html new file mode 100644 index 0000000000..69f5c0f077 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-shadow-dom-repeats.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM Element Attachment</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/1235'> +<link rel='help' href='https://github.com/whatwg/html/pull/10069'> +<link rel='help' href='https://github.com/whatwg/dom/pull/1246'> + +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../html/resources/common.js'></script> +<script src="support/helpers.js"></script> + +<div id=multiple1> + <template shadowrootmode=open>Open</template> + <template shadowrootmode=closed>Closed</template> +</div> + +<div id=multiple2> + <template shadowrootmode=closed>Closed</template> + <template shadowrootmode=open>Open</template> +</div> + +<script> +test((t) => { + let shadow = multiple1.shadowRoot; + assert_true(!!shadow,'Remaining shadow root should be open'); + assert_equals(shadow.textContent,"Open"); + shadow = multiple2.shadowRoot; + assert_false(!!shadow,'Remaining shadow root should be closed'); + multiple1.remove(); // Cleanup + multiple2.remove(); +},'Repeated declarative shadow roots keep only the first'); +</script> + +<div id=open1> + <template shadowrootmode=open>Open</template> +</div> + +<script> +test((t) => { + assert_throws_dom("NotSupportedError",() => { + open1.attachShadow({mode: "closed"}); + },'Mismatched shadow root type should throw'); + const initialShadow = open1.shadowRoot; + const shadow = open1.attachShadow({mode: "open"}); // Shouldn't throw + assert_equals(shadow,initialShadow,'Same shadow should be returned'); + assert_equals(shadow.textContent,'','Shadow should be empty'); +},'Calling attachShadow() on declarative shadow root must match type'); +</script> + +<div id=open2> + <template shadowrootmode=open shadowrootdelegatesfocus shadowrootclonable> + Open, delegates focus (not the default), clonable (not the default) + named slot assignment (the default) + </template> +</div> + +<script> +test((t) => { + assert_throws_dom("NotSupportedError",() => { + open2.attachShadow({mode: "closed", delegatesFocus: true, slotAssignment: "named", clonable: true}); + },'Mismatched shadow root type should throw'); + assert_throws_dom("NotSupportedError",() => { + open2.attachShadow({mode: "open", delegatesFocus: false, slotAssignment: "named", clonable: true}); + },'Mismatched shadow root delegatesFocus should throw'); + assert_throws_dom("NotSupportedError",() => { + open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "manual", clonable: true}); + },'Mismatched shadow root slotAssignment should throw'); + assert_throws_dom("NotSupportedError",() => { + open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: false}); + },'Mismatched shadow root clonable should throw'); + + const initialShadow = open2.shadowRoot; + const shadow = open2.attachShadow({mode: "open", delegatesFocus: true, slotAssignment: "named", clonable: true}); // Shouldn't throw + assert_equals(shadow,initialShadow,'Same shadow should be returned'); + assert_equals(shadow.textContent,'','Shadow should be empty'); +},'Calling attachShadow() on declarative shadow root must match all parameters'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/declarative-with-disabled-shadow.html b/testing/web-platform/tests/shadow-dom/declarative/declarative-with-disabled-shadow.html new file mode 100644 index 0000000000..bcf53403ad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/declarative-with-disabled-shadow.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM with shadow disabled</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +class ShadowDisabledElement extends HTMLElement { + static get disabledFeatures() { + return ['shadow']; + } +} +customElements.define('shadow-disabled',ShadowDisabledElement); +</script> + +<shadow-disabled> + <template shadowrootmode="open"><span id=inside></span></template> +</shadow-disabled> + +<script> +test(t => { + let element = document.querySelector('shadow-disabled'); + assert_true(element instanceof ShadowDisabledElement); + let template = element.querySelector('template'); + assert_true(!!template, 'Declarative shadow attach should fail, since shadow-disabled is true'); + assert_true(!element.shadowRoot, 'Shadow root should not be present on custom element'); +}, 'Declarative Shadow DOM: declarative shadow should fail if attachShadow() already called'); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/declarative/gethtml.tentative.html b/testing/web-platform/tests/shadow-dom/declarative/gethtml.tentative.html new file mode 100644 index 0000000000..c48230c170 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/gethtml.tentative.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<title>getHTML behavior</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/html/issues/8867'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../html/resources/common.js'></script> + +<body> + +<script> +function testElementType(allowsShadowDom, elementType, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable, clonable) { + const t = test(t => { + // Create and attach element + let wrapper; + if (runGetHTMLOnShadowRoot) { + // This ensures we're testing both Element.getHTML() and ShadowRoot.getHTML(). + const host = document.createElement('div'); + t.add_cleanup(function() { host.remove(); }); + document.body.appendChild(host); + wrapper = host.attachShadow({mode: 'open'}); + } else { + wrapper = document.createElement('div'); + t.add_cleanup(function() { wrapper.remove(); }); + document.body.appendChild(wrapper); + } + + let shadowRoot; + const isOpen = mode === 'open'; + let initDict = {mode: mode, delegatesFocus: delegatesFocus, clonable}; + let expectedSerializable = null; + switch (serializable) { + case undefined: expectedSerializable = false; break; + case "true": initDict.serializable = expectedSerializable = true; break; + case "false": initDict.serializable = expectedSerializable = false; break; + default: throw new Error(`Invalid serializable ${serializable}`); + } + const delegatesAttr = delegatesFocus ? ' shadowrootdelegatesfocus=""' : ''; + const serializableAttr = expectedSerializable ? ' serializable=""' : ''; + const clonableAttr = clonable ? ' shadowrootclonable=""' : ''; + + if (allowsShadowDom && declarativeShadowDom) { + const html = `<${elementType}><template shadowrootmode=${mode}${delegatesAttr}${serializableAttr}${clonableAttr}>`; + wrapper.setHTMLUnsafe(html); + if (isOpen) { + shadowRoot = wrapper.firstElementChild.shadowRoot; + } else { + // For closed shadow root, we rely on the behavior of attachShadow to return it to us + shadowRoot = wrapper.firstElementChild.attachShadow(initDict); + } + } else { + // Imperative shadow dom + const element = document.createElement(elementType); + wrapper.appendChild(element); + if (allowsShadowDom) { + shadowRoot = element.attachShadow(initDict); + } + } + assert_true(!allowsShadowDom || !!shadowRoot); + + if (allowsShadowDom) { + const correctShadowHtml = `<template shadowrootmode="${mode}"${delegatesAttr}${serializableAttr}${clonableAttr}><slot></slot></template>`; + const correctHtml = `<${elementType}>${correctShadowHtml}</${elementType}>`; + assert_equals(shadowRoot.mode,mode); + assert_equals(shadowRoot.delegatesFocus,delegatesFocus); + assert_equals(shadowRoot.serializable,expectedSerializable); + assert_equals(shadowRoot.clonable,clonable); + shadowRoot.appendChild(document.createElement('slot')); + const emptyElement = `<${elementType}></${elementType}>`; + if (isOpen) { + if (expectedSerializable) { + assert_equals(wrapper.getHTML({includeShadowRoots: true}), correctHtml); + } else { + assert_equals(wrapper.getHTML({includeShadowRoots: true}), emptyElement); + } + } else { + // Closed shadow roots should not be returned unless shadowRoots specifically contains the shadow root: + assert_equals(wrapper.getHTML({includeShadowRoots: true}), emptyElement); + assert_equals(wrapper.getHTML({includeShadowRoots: true, shadowRoots: []}), emptyElement); + } + // If we provide the shadow root, serialize it, regardless of includeShadowRoots. + assert_equals(wrapper.getHTML({includeShadowRoots: true, shadowRoots: [shadowRoot]}),correctHtml); + assert_equals(wrapper.getHTML({shadowRoots: [shadowRoot]}),correctHtml); + // This should always throw - includeShadowRoots false, but we've provided roots. + assert_throws_dom("NotSupportedError",() => wrapper.getHTML({includeShadowRoots: false, shadowRoots: [shadowRoot]})); + } else { + // For non-shadow hosts, getHTML() should also match .innerHTML + assert_equals(wrapper.getHTML({includeShadowRoots: true}),wrapper.innerHTML); + } + + // Either way, make sure getHTML({includeShadowRoots: false}) matches .innerHTML + assert_equals(wrapper.getHTML({includeShadowRoots: false}),wrapper.innerHTML,'getHTML() with includeShadowRoots false should return the same as .innerHTML'); + // ...and that the default for includeShadowRoots is false. + assert_equals(wrapper.getHTML(),wrapper.innerHTML,'The default for includeShadowRoots should be false'); + + }, `${runGetHTMLOnShadowRoot ? 'ShadowRoot' : 'Element'}.getHTML() on <${elementType}>${allowsShadowDom ? `, with ${declarativeShadowDom ? 'declarative' : 'imperative'} shadow, mode=${mode}, delegatesFocus=${delegatesFocus}, serializable=${serializable}, clonable=${clonable}.` : ''}`); +} + +function runAllTests() { + const allElements = [...HTML5_ELEMENTS, 'htmlunknown']; + const safelisted = HTML5_SHADOW_ALLOWED_ELEMENTS.filter(el => el != 'body'); + for (const elementName of allElements) { + const allowsShadowDom = safelisted.includes(elementName); + for (const runGetHTMLOnShadowRoot of [false, true]) { + if (allowsShadowDom) { + for (const declarativeShadowDom of [false, true]) { + for (const delegatesFocus of [false, true]) { + for (const clonable of [false, true]) { + for (const mode of ['open', 'closed']) { + for (const serializable of [undefined, 'false', 'true']) { + testElementType(true, elementName, runGetHTMLOnShadowRoot, declarativeShadowDom, mode, delegatesFocus, serializable, clonable); + } + } + } + } + } + } else { + testElementType(false, elementName, runGetHTMLOnShadowRoot); + } + } + } +} + +runAllTests(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/getinnerhtml.tentative.html b/testing/web-platform/tests/shadow-dom/declarative/getinnerhtml.tentative.html new file mode 100644 index 0000000000..139cba2a51 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/getinnerhtml.tentative.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<title>getInnerHTML </title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../html/resources/common.js'></script> + +<body> + +<script> +function testElementType(allowsShadowDom, elementType, applyToShadow, mode, delegatesFocus) { + const t = test(t => { + // Create and attach element + let wrapper; + if (applyToShadow) { + const host = document.createElement('div'); + t.add_cleanup(function() { host.remove(); }); + document.body.appendChild(host); + wrapper = host.attachShadow({mode: 'open'}); + } else { + wrapper = document.createElement('div'); + t.add_cleanup(function() { wrapper.remove(); }); + document.body.appendChild(wrapper); + } + const element = document.createElement(elementType); + wrapper.appendChild(element); + + const isOpen = mode === 'open'; + if (allowsShadowDom) { + const delegatesAttr = delegatesFocus ? ' shadowrootdelegatesfocus=""' : ''; + const correctShadowHtml = `<template shadowrootmode="${mode}"${delegatesAttr}><slot></slot></template>`; + const correctHtml = `<${elementType}>${correctShadowHtml}</${elementType}>`; + const emptyElement = `<${elementType}></${elementType}>`; + const shadowRoot = element.attachShadow({mode: mode, delegatesFocus: delegatesFocus}); + shadowRoot.appendChild(document.createElement('slot')); + if (isOpen) { + // We can only test this for open roots + assert_equals(wrapper.getInnerHTML(),correctHtml,'The default for includeShadowRoots should be true'); + } else { + // Closed shadow roots should not be returned unless closedRoots contains the shadow root: + assert_equals(wrapper.getInnerHTML({includeShadowRoots: true}), emptyElement); + assert_equals(wrapper.getInnerHTML({includeShadowRoots: true, closedRoots: []}), emptyElement); + } + assert_equals(wrapper.getInnerHTML({includeShadowRoots: true, closedRoots: [shadowRoot]}),correctHtml); + // ClosedRoots are not included if includeShadowRoots is false: + assert_equals(wrapper.getInnerHTML({includeShadowRoots: false, closedRoots: [shadowRoot]}),emptyElement); + } else { + // For non-shadow hosts, getInnerHTML() should also match .innerHTML + assert_equals(wrapper.getInnerHTML({includeShadowRoots: true}),wrapper.innerHTML); + assert_equals(wrapper.getInnerHTML(),wrapper.innerHTML); + } + + // Either way, make sure getInnerHTML({includeShadowRoots: false}) matches .innerHTML + assert_equals(wrapper.getInnerHTML({includeShadowRoots: false}),wrapper.innerHTML,'getInnerHTML() with includeShadowRoots false should return the same as .innerHTML'); + + }, `${applyToShadow ? 'ShadowRoot' : 'Element'}.getInnerHTML() on <${elementType}>${allowsShadowDom ? `, with mode=${mode}, delegatesFocus=${delegatesFocus}.` : ''}`); +} + +function runAllTests() { + const allElements = [...HTML5_ELEMENTS, 'htmlunknown']; + const safelisted = HTML5_SHADOW_ALLOWED_ELEMENTS; + for (const elementName of allElements) { + const allowsShadowDom = safelisted.includes(elementName); + for (const applyToShadow of [false, true]) { + if (allowsShadowDom) { + for (const delegatesFocus of [false, true]) { + for (const mode of ['open', 'closed']) { + testElementType(true, elementName, applyToShadow, mode, delegatesFocus); + } + } + } else { + testElementType(false, elementName, applyToShadow); + } + } + } +} + +runAllTests(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/innerhtml-before-closing-tag.html b/testing/web-platform/tests/shadow-dom/declarative/innerhtml-before-closing-tag.html new file mode 100644 index 0000000000..f038d3ecb2 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/innerhtml-before-closing-tag.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM innerHTML</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +function myObserver(mutationsList) { + for (let mutation of mutationsList) { + if (Array.from(mutation.addedNodes).map(n=>n.id).includes('shadow')) { + assert_unreached('For streaming declarative Shadow DOM, the <template> element should never get added to the tree'); + } + } + const host = document.querySelector('#host'); + if (!host) + return; + const shadow = host.shadowRoot; + if (!shadow) + return; + const replaceNode = shadow.querySelector('#toreplace'); + assert_true(!!replaceNode); + replaceNode.innerHTML = "<span id=newcontent>This should be in <div>'s shadow root</span>"; + observer.disconnect(); +} +var observer = new MutationObserver(myObserver); +observer.observe(document.body, { childList: true, subtree: true }); +</script> + +<div id=host> + <template id=shadow shadowrootmode=open> + <span id=toreplace><span id=oldcontent>This should get removed</span></span> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> + </template> +</div> + +<script> +test(t => { + t.add_cleanup(function() { observer.disconnect(); }); + let host = document.querySelector('#host'); + let template = host.querySelector('template'); + assert_true(!template, 'Declarative shadow template should not be present'); + let shadowroot = host.shadowRoot; + assert_true(!!shadowroot, 'Shadow root should be present'); + assert_true(!!shadowroot.querySelector('#newcontent'),'The innerHTML replacement content should be present'); + assert_true(!shadowroot.querySelector('#oldcontent'),'The old replaced content should not be present'); +}, 'Declarative Shadow DOM: innerHTML should work while parsing streaming declarative shadow root <template>'); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/declarative/innerhtml-on-ordinary-template.html b/testing/web-platform/tests/shadow-dom/declarative/innerhtml-on-ordinary-template.html new file mode 100644 index 0000000000..87260013de --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/innerhtml-on-ordinary-template.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM innerHTML</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +function myObserver(mutationsList) { + for (let mutation of mutationsList) { + for (let n of mutation.addedNodes) { + if (n.id === 'has-imperative-root') { + const shadowRoot = n.attachShadow( {mode: 'open'}); + } + } + } +} +var observer = new MutationObserver(myObserver); +observer.observe(document.body, { childList: true, subtree: true }); +</script> + +<div id='has-imperative-root'> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> + <!-- The imperative shadow root should be attached now. --> + <template id=ordinarytemplate shadowrootmode=open> + <span id=toreplace>This should get removed</span> + </template> + <script> + ordinarytemplate.innerHTML = '<span id=replaced>This should replace template contents</span>'; + </script> +</div> + +<script> +test(t => { + t.add_cleanup(function() { observer.disconnect(); }); + let host = document.querySelector('#has-imperative-root'); + let shadowroot = host.shadowRoot; + assert_true(!!shadowroot, 'Shadow root should be present'); + assert_false(shadowroot.hasChildNodes(), 'Shadow root should not contain any children'); + let template = host.querySelector('template#ordinarytemplate'); + assert_true(!!template, 'Since host has imperative root, <template> should be left over'); + assert_false(template.hasChildNodes(),'template should not have direct children'); + assert_true(!!template.content.querySelector('#replaced'),'The innerHTML replacement content should be present'); + assert_true(!template.content.querySelector('#toreplace'),'The old replaced content should not be present'); +}, 'Declarative Shadow DOM: innerHTML should work on <template shadowroot> that is left over'); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/declarative/move-template-before-closing-tag.html b/testing/web-platform/tests/shadow-dom/declarative/move-template-before-closing-tag.html new file mode 100644 index 0000000000..a3d7806f5d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/move-template-before-closing-tag.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM: moving the template doesn't change attachment point</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +function myObserver(mutationsList, observer) { + for (let mutation of mutationsList) { + for (let n of mutation.addedNodes) { + if (n.className === 'template' && !n.alreadyMoved) { + n.alreadyMoved = true; + let testWrapper = n.parentElement.parentElement; + let newHost = testWrapper.querySelector('div.new-host'); + newHost.appendChild(n); + } + } + } +} +(new MutationObserver(myObserver)).observe(document.body, { childList: true, subtree: true }); + +function runTest(testdiv) { + let description = testdiv.getAttribute('description'); + let shouldAttach = testdiv.getAttribute('shouldattach') == 'true'; + test(t => { + let template = testdiv.querySelector('.template'); + let oldHost = testdiv.querySelector('.old-host'); + let newHost = testdiv.querySelector('.new-host'); + let shadowroot = oldHost.shadowRoot; + if (shouldAttach) { + assert_true(!template, 'Declarative shadow template should not be left over'); + assert_true(!oldHost.children.length, 'Old host should not contain children'); + assert_true(!newHost.children.length, 'New host should not contain any children'); + assert_true(!!shadowroot, 'Shadow root should be present'); + assert_true(!!shadowroot.querySelector('span.contents'), 'The shadow root should contain a span'); + } else { + assert_true(!oldHost.children.length, 'Old host should not contain children'); + assert_true(!!newHost.children.length, 'New host should now contain the template'); + assert_true(!shadowroot, 'Shadow root should not be present'); + assert_true(!!template, 'Declarative shadow template should be left over'); + assert_equals(template, newHost.querySelector('.template'), 'Declarative shadow template should be in the new host'); + assert_true(!!template.content.querySelector('span.contents'), 'The template should still contain its content'); + } + }, description); +} +</script> + +<style> + div.test { + display: none; + } +</style> + +<div class=test shouldattach=true description="Moving the template node during parsing should attach to initial parent (content before observer)"> + <div class=new-host></div> + <div class=old-host> + <template class='template' shadowrootmode=open> + <span class=contents>Template contents</span> + <script></script> <!-- Observer runs here, moves <template> node --> + </template> + </div> +</div> + +<div class=test shouldattach=true description="Moving the template node during parsing should attach to initial parent (content after observer)"> + <div class=new-host></div> + <div class=old-host> + <template class='template' shadowrootmode=open> + <script></script> <!-- Observer runs here, moves <template> node --> + <span class=contents>Template contents</span> + </template> + </div> +</div> + +<div class=test shouldattach=false description="Moving the template node from invalid parent to valid parent should still not attach"> + <div class=new-host></div> + <video class=old-host> + <template class='template' shadowrootmode=open> + <span class=contents>Template contents</span> + <script></script> <!-- Observer runs here, moves <template> node --> + </template> + </video> +</div> + +<script> + let tests = document.querySelectorAll('div.test'); + assert_true(tests.length > 0); + tests.forEach(t => { runTest(t); }); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/declarative/script-access.html b/testing/web-platform/tests/shadow-dom/declarative/script-access.html new file mode 100644 index 0000000000..7e1340d477 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/script-access.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<title>Declarative Shadow DOM</title> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://github.com/whatwg/dom/issues/831'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +let templatesSeen = 0; + +function myObserver(mutationsList, observer) { + document.querySelectorAll('.host').forEach(n => { + templatesSeen++; + n.className = 'done'; + switch (n.id) { + case 'openhost': + case 'closedhost': + assert_equals(n.querySelector('template'),null,'No template ever for streaming declarative Shadow DOM'); + assert_equals(n.innerHTML, "", 'Declarative shadow host innerHTML should be empty - all content is inside shadow'); + break; + case 'noroot': + const template = n.querySelector('template'); + assert_true(!!template,'Regular template should still be present'); + // Make sure adding 'shadowrootmode' attribute doesn't trigger a shadow root, + // even if added before parsing completes. + template.setAttribute('shadowrootmode','open'); + assert_not_equals(template.content, null, 'Regular template should have content, even after adding shadowrootmode attribute'); + assert_not_equals(template.innerHTML, "", 'Regular template should have innerHTML, even after adding shadowrootmode attribute'); + break; + default: + assert_unreached('Unrecognized template'); + } + }); +} +const observer = new MutationObserver(myObserver); +observer.observe(document.body, { childList: true, subtree: true }); +assert_equals(templatesSeen, 0, 'No mutations yet'); +</script> + +<div id=openhost class=host><template shadowrootmode=open> + <slot></slot> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> +</template></div> + +<div id=closedhost class=host><template shadowrootmode=closed> + <slot></slot> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> +</template></div> + +<div id=noroot class=host><template> + <slot></slot> + <!-- Ensure observer runs at this point (https://github.com/web-platform-tests/wpt/issues/35393) --> + <script> // some content, which shouldn't be necessary </script> +</template></div> + +<script> +test(t => { + t.add_cleanup(function() { observer.disconnect(); }); + + assert_equals(templatesSeen, 3); + + // Open shadow root + let host = document.querySelector('#openhost'); + assert_equals(host.querySelector('template'), null, 'No template nodes'); + assert_true(!!host.shadowRoot, 'Shadow root should exist'); + + // Closed shadow root + host = document.querySelector('#closedhost'); + assert_equals(host.querySelector('template'), null, 'No template nodes'); + assert_true(!host.shadowRoot, 'Closed shadow root (can\'t detect)'); + + // No shadow root + host = document.querySelector('#noroot'); + assert_true(!!host.querySelector('template'), 'Template node still present for invalid shadowrootmode value'); + assert_true(!host.shadowRoot, 'No shadow root'); +},'Streaming Declarative Shadow DOM: template .content() should be null'); +</script> + + +<script> +let synchronous_events_received = new Set(); +function synchronousHandler(e) { + synchronous_events_received.add(e.type); +} +const sync_events = ["DOMAttrModified","DOMAttributeNameChanged","DOMCharacterDataModified", + "DOMElementNameChanged","DOMNodeInserted","DOMNodeInsertedIntoDocument","DOMNodeRemoved", + "DOMNodeRemovedFromDocument","DOMSubtreeModified"]; +function addSyncObserver(evt) { + window.addEventListener(evt, synchronousHandler, true); +} +function removeSyncObserver(evt) { + window.removeEventListener(evt, synchronousHandler, true); +} +sync_events.forEach(e => addSyncObserver(e)); +</script> + +<div id=synchost1> + <template shadowrootmode=open> + <div class=foo>content</div> + <slot></slot> + </template> +</div> + +<div id=synchost2> + <template shadowrootmode=closed> + <div class=foo>content</div> + <slot></slot> + </template> +</div> + +<script> +test(t => { + t.add_cleanup(function() { sync_events.forEach(e => removeSyncObserver(e)) }); + assert_true(!synchronous_events_received.size,`Synchronous mutation events fired: ${Array.from(synchronous_events_received)}`); +},'Synchronous mutation events should not be fired during streaming declarative shadow DOM parsing'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/declarative/support/declarative-child-frame.html b/testing/web-platform/tests/shadow-dom/declarative/support/declarative-child-frame.html new file mode 100644 index 0000000000..603c47743b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/declarative/support/declarative-child-frame.html @@ -0,0 +1,23 @@ +<div id=iframe> + <div id=wrapper> + <div id=host> + <template shadowrootmode=open> + <span id=content>Content</span> + </template> + </div> + </div> +</div> + +<script> +function sendStatus(error, hasShadow, msg) { + const name = window.name; + parent.postMessage({ name, error, hasShadow, msg }, '*'); +} + +window.addEventListener('load', () => { + const host = document.querySelector('#host'); + if (!host) + return sendStatus(true, false, 'Unable to find host element'); + return sendStatus(false, !!host.shadowRoot); +}); +</script> diff --git a/testing/web-platform/tests/shadow-dom/directionality-001-ref.html b/testing/web-platform/tests/shadow-dom/directionality-001-ref.html new file mode 100644 index 0000000000..7a2eaeb868 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/directionality-001-ref.html @@ -0,0 +1,6 @@ +<!doctype html> +<title>CSS Test Reference</title> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<div dir="rtl"> 123 456 <span><span> 789 101112 </span></span></div> +<div dir="rtl"> 123 456 <span dir="ltr"><span> 789 101112 </span></span></div> +<div dir="rtl"> 123 456 <span><span> 789 101112 </span></span></div> diff --git a/testing/web-platform/tests/shadow-dom/directionality-001.tentative.html b/testing/web-platform/tests/shadow-dom/directionality-001.tentative.html new file mode 100644 index 0000000000..d96926ad65 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/directionality-001.tentative.html @@ -0,0 +1,19 @@ +<!doctype html> +<title>Test: directionality propagation in Shadow DOM.</title> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel="help" href="https://html.spec.whatwg.org/#the-dir-attribute"> +<link rel="help" href="https://github.com/whatwg/html/issues/3699"> +<link rel="match" href="directionality-001-ref.html"> +<div id="host0" dir="rtl"><span> 789 101112 </span></div> +<div id="host1" dir="rtl"><span> 789 101112 </span></div> +<div id="host2" dir="rtl"><span> 789 101112 </span></div> +<script> + host0.attachShadow({mode: 'closed'}).innerHTML = + '<div> 123 456 <span><slot></slot></span></div>'; + + host1.attachShadow({mode: 'closed'}).innerHTML = + '<div> 123 456 <span dir="ltr"><slot></slot></span></div>'; + + host2.attachShadow({mode: 'closed'}).innerHTML = + '<div> 123 456 <span><slot dir="ltr"></slot></span></div>'; +</script> diff --git a/testing/web-platform/tests/shadow-dom/directionality-002-ref.html b/testing/web-platform/tests/shadow-dom/directionality-002-ref.html new file mode 100644 index 0000000000..454b2fecab --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/directionality-002-ref.html @@ -0,0 +1,6 @@ +<!doctype html> +<title>CSS Test Reference</title> +<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io"> +<link rel="author" title="Mozilla" href="https://mozilla.org"> +<body dir="rtl"> +<div style="width: 100px; height: 100px; background: green"></div> diff --git a/testing/web-platform/tests/shadow-dom/directionality-002.tentative.html b/testing/web-platform/tests/shadow-dom/directionality-002.tentative.html new file mode 100644 index 0000000000..7ff036194b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/directionality-002.tentative.html @@ -0,0 +1,25 @@ +<!doctype html> +<title>Directionality is properly computed for slotted children</title> +<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io"> +<link rel="author" title="Mozilla" href="https://mozilla.org"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1562425"> +<link rel="match" href="directionality-002-ref.html"> +<style> + .slotted { + width: 100px; + height: 100px; + background: red; + } + .slotted:dir(rtl) { + background: green; + } +</style> +<body dir="rtl"> +<script> + let root = document.createElement("div") + let slotted = document.createElement("div"); + slotted.className = "slotted"; + root.appendChild(slotted); + root.attachShadow({ mode: "open" }).appendChild(document.createElement("slot")); + document.body.appendChild(root); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-composed-path-after-dom-mutation.html b/testing/web-platform/tests/shadow-dom/event-composed-path-after-dom-mutation.html new file mode 100644 index 0000000000..fd129e036a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-composed-path-after-dom-mutation.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<title>Shadow DOM: Event.composedPath() should return the same result even if DOM is mutated</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="host"> + <template id="sr" data-mode="closed"> + <div id="target"></div> + </template> + </div> +</div> + +<script> +async_test((t) => { + const n = createTestTree(document.querySelector('#test1')); + n.host.addEventListener('my-event', t.step_func((e) => { + const path_before = e.composedPath(); + // Move the target out of a closed shadow tree + n.host.append(n.target); + const path_after = e.composedPath(); + assert_array_equals(path_before, path_after); + t.done(); + })); + const event = new Event('my-event', { bubbles: true, composed: true }); + n.target.dispatchEvent(event); +}, 'Event.composedPath() should return the same result even if DOM is mutated (1/2)'); +</script> + +<div id="test2"> + <div id="host1"> + <template id="sr1" data-mode="closed"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +async_test((t) => { + const n = createTestTree(document.querySelector('#test2')); + n.host1.addEventListener('my-event', t.step_func((e) => { + const path_before = e.composedPath(); + // Move nodes out of a closed shadow tree + n.host1.append(n.host2); + n.host1.append(n.target); + const path_after = e.composedPath(); + assert_array_equals(path_before, path_after); + t.done(); + })); + const event = new Event('my-event', { bubbles: true, composed: true }); + n.target.dispatchEvent(event); +}, 'Event.composedPath() should return the same result even if DOM is mutated (2/2)'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-composed-path-with-related-target.html b/testing/web-platform/tests/shadow-dom/event-composed-path-with-related-target.html new file mode 100644 index 0000000000..f6dff13a93 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-composed-path-with-related-target.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<title>Shadow DOM: Event path and Event.composedPath() (with related target)</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="target"></div> + <div id="related"></div> +</div> + +<script> +test(() => { + let n = createTestTree(test1); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related })); + let path = ['target', 'test1']; + assert_event_path_equals(log, [['target', 'target', 'related', path], + ['test1', 'target', 'related', path]]); +}, 'Event path for an event with a relatedTarget. relatedTarget != target.'); +</script> + +<script> +test(() => { + let n = createTestTree(test1); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.target })); + let path = ['target', 'test1']; + assert_event_path_equals(log, [['target', 'target', 'target', path], + ['test1', 'target', 'target', path]]); +}, 'Event path for an event with a relatedTarget. Event should be dispatched even when target and relatedTarget are same.'); +</script> + +<div id="test2"> + <div id="host"> + <template id="sr" data-mode="open"> + <div id="target"></div> + <div id="related"></div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test2); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related })); + let path = ['target', 'sr']; + assert_event_path_equals(log, [['target', 'target', 'related', path], + ['sr', 'target', 'related', path]]); +}, 'Event path for an event with a relatedTarget. Event should stop at the shadow root'); + +test(() => { + let n = createTestTree(test2); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.target })); + let path = ['target', 'sr']; + assert_event_path_equals(log, [['target', 'target', 'target', path], + ['sr', 'target', 'target', path]]); +}, 'Event path for an event with a relatedTarget which is identical to target. Event should be dispatched and should stop at the shadow root.'); +</script> + +<div id="test3_1"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="target1"></div> + </template> + </div> +</div> + +<div id="test3_2"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="target2"></div> + </template> + </div> +</div> + +<script> +test(() => { + let n1 = createTestTree(test3_1); + let n2 = createTestTree(test3_2); + let log = dispatchEventWithLog(n1, n1.target1, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n2.target2 })); + let path = ['target1', 'sr1', 'host1', 'test3_1']; + assert_event_path_equals(log, [['target1', 'target1', 'host2', path], + ['sr1', 'target1', 'host2', path], + ['host1', 'host1', 'host2', path], + ['test3_1', 'host1', 'host2', path]]); +}, 'Event path for an event with a relatedTarget. target and relaterTarget do not share any shadow-including ancestor. target is in a shadow tree.'); + +test(() => { + let n1 = createTestTree(test3_1); + let n2 = createTestTree(test3_2); + let log = dispatchEventWithLog(n1, n1.host1, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n2.target2 })); + let path = ['host1', 'test3_1']; + assert_event_path_equals(log, [['host1', 'host1', 'host2', path], + ['test3_1', 'host1', 'host2', path]]); +}, 'Event path for an event with a relatedTarget. target and relaterTarget do not share any shadow-including ancestor. target is not in a shadow tree'); +</script> + +<div id="test4"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="target"></div> + </template> + </div> + <div id="host3"> + <template id="sr3" data-mode="open"> + <div id="related"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test4); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related })); + let path = ['target', 'sr2', 'host2', 'sr1']; + assert_event_path_equals(log, [['target', 'target', 'host3', path], + ['sr2', 'target', 'host3', path], + ['host2', 'host2', 'host3', path], + ['sr1', 'host2', 'host3', path]]); +}, 'Event path for an event with a relatedTarget. target and relaterTarget share the same shadow-including ancestor. Both are in shadow trees.'); + +test(() => { + let n = createTestTree(test4); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.host1 })); + let path = ['target', 'sr2', 'host2', 'sr1']; + assert_event_path_equals(log, [['target', 'target', 'host1', path], + ['sr2', 'target', 'host1', path], + ['host2', 'host2', 'host1', path], + ['sr1', 'host2', 'host1', path]]); +}, 'Event path for an event with a relatedTarget. relatedTarget is a shadow-including ancestor of target.'); + +test(() => { + let n = createTestTree(test4); + let log = dispatchEventWithLog(n, n.host1, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.target })); + assert_event_path_equals(log, []); +}, 'Event path for an event with a relatedTarget. target is a shadow-including ancestor of relatedTarget.'); +</script> + +<div id="test5"> + <div id="host"> + <template id="sr" data-mode="open"> + <slot id="slot"></slot> + <div id="related"></div> + </template> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test5); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related })); + let path = ['target', 'slot', 'sr', 'host', 'test5']; + assert_event_path_equals(log, [['target', 'target', 'host', path], + ['slot', 'target', 'related', path], + ['sr', 'target', 'related', path], + ['host', 'target', 'host', path], + ['test5', 'target', 'host', path]]); +}, 'Event path for an event with a relatedTarget. target is assigned to a slot.'); + +test(() => { + let n = createTestTree(test5); + let log = dispatchEventWithLog(n, n.related, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.target })); + let path = ['related', 'sr', 'host', 'test5']; + assert_event_path_equals(log, [['related', 'related', 'target', path], + ['sr', 'related', 'target', path], + ['host', 'host', 'target', path], + ['test5', 'host', 'target', path]]); +}, 'Event path for an event with a relatedTarget. relatedTarget is assigned to a slot.'); +</script> + +<div id="test6"> + <div id="host0"> + <template id="sr0" data-mode="open"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <slot id="slot2"></slot> + <div id="related2"></div> + </template> + <div id="related1"></div> + <div id="d1"> + <slot id="slot1"></slot> + </div> + </div> + </template> + <div id="host3"> + <template id="sr3" data-mode="open"> + <div id="host4"> + <template id="sr4" data-mode="open"> + <div id="host5"> + <template id="sr5" data-mode="open"> + <slot id="slot5"></slot> + </template> + <slot id="slot4"></slot> + </div> + </template> + <div id="target"></div> + </div> + </template> + </div> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test6); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related1 })); + let path = ['target', 'slot4', 'slot5', 'sr5', 'host5', 'sr4', 'host4', 'sr3', 'host3', 'slot1', 'd1', 'slot2', 'sr2', 'host2', 'sr1', 'host1', 'sr0']; + assert_event_path_equals(log, [['target', 'target', 'host1', path], + ['slot4', 'target', 'host1', path], + ['slot5', 'target', 'host1', path], + ['sr5', 'target', 'host1', path], + ['host5', 'target', 'host1', path], + ['sr4', 'target', 'host1', path], + ['host4', 'target', 'host1', path], + ['sr3', 'target', 'host1', path], + ['host3', 'host3', 'host1', path], + ['slot1', 'host3', 'related1', path], + ['d1' , 'host3', 'related1', path], + ['slot2', 'host3', 'related1', path], + ['sr2', 'host3', 'related1', path], + ['host2', 'host3', 'related1', path], + ['sr1', 'host3', 'related1', path], + ['host1', 'host3', 'host1', path], + ['sr0', 'host3', 'host1' , path]]); +}, 'Event path for an event with a relatedTarget. Event should be dispatched at every slots.'); + +test(() => { + let n = createTestTree(test6); + let log = dispatchEventWithLog(n, n.target, new FocusEvent('my-focus', { bubbles: true, composed: true, relatedTarget: n.related2 })); + let path = ['target', 'slot4', 'slot5', 'sr5', 'host5', 'sr4', 'host4', 'sr3', 'host3', 'slot1', 'd1', 'slot2', 'sr2', 'host2', 'sr1', 'host1', 'sr0']; + assert_event_path_equals(log, [['target', 'target', 'host1', path], + ['slot4', 'target', 'host1', path], + ['slot5', 'target', 'host1', path], + ['sr5', 'target', 'host1', path], + ['host5', 'target', 'host1', path], + ['sr4', 'target', 'host1', path], + ['host4', 'target', 'host1', path], + ['sr3', 'target', 'host1', path], + ['host3', 'host3', 'host1', path], + ['slot1', 'host3', 'host2', path], + ['d1' , 'host3', 'host2', path], + ['slot2', 'host3', 'related2', path], + ['sr2', 'host3', 'related2', path], + ['host2', 'host3', 'host2', path], + ['sr1', 'host3', 'host2', path], + ['host1', 'host3', 'host1', path], + ['sr0', 'host3', 'host1' , path]]); +}, 'Event path for an event with a relatedTarget. Event should be dispatched at every slots. relatedTarget should be correctly retargeted.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-composed-path.html b/testing/web-platform/tests/shadow-dom/event-composed-path.html new file mode 100644 index 0000000000..8c37b1c3bd --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-composed-path.html @@ -0,0 +1,281 @@ +<!DOCTYPE html> +<title>Shadow DOM: Event path and Event.composedPath()</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="d1"> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test1); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'd1', 'test1']; + assert_event_path_equals(log, + [['target', 'target', null, path], + ['d1', 'target', null, path], + ['test1', 'target', null, path]]); +}, 'Event Path without ShadowRoots.'); +</script> + +<div id="test2"> + <div id="host"> + <template id="sr" data-mode="open"> + <div id="target"></div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test2); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'sr', 'host', 'test2']; + assert_event_path_equals(log, + [['target', 'target', null, path], + ['sr', 'target', null, path], + ['host', 'host', null, path], + ['test2', 'host', null, path]]); +}, 'Event Path with an open ShadowRoot.'); +</script> + +<div id="test3"> + <div id="host"> + <template id="sr" data-mode="closed"> + <div id="target"></div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test3); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target','sr', 'host', 'test3']; + let path1 = ['host', 'test3']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['sr', 'target', null, path], + ['host', 'host', null, path1], + ['test3', 'host', null, path1]]); +}, 'Event Path with a closed ShadowRoot.'); +</script> + +<div id="test4"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test4); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'sr2', 'host2', 'sr1', 'host1', 'test4']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'host2', null, path], + ['sr1', 'host2', null, path], + ['host1', 'host1', null, path], + ['test4', 'host1', null, path]]); +}, 'Event Path with nested ShadowRoots: open > open.'); +</script> + +<div id="test5"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="host2"> + <template id="sr2" data-mode="closed"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test5); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'sr2', 'host2', 'sr1', 'host1', 'test5']; + let path1 = ['host2', 'sr1', 'host1', 'test5']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'host2', null, path1], + ['sr1', 'host2', null, path1], + ['host1', 'host1', null, path1], + ['test5', 'host1', null, path1]]); +}, 'Event Path with nested ShadowRoots: open > closed.'); +</script> + +<div id="test6"> + <div id="host1"> + <template id="sr1" data-mode="closed"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test6); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'sr2', 'host2', 'sr1', 'host1', 'test6']; + let path1 = ['host1', 'test6']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'host2', null, path], + ['sr1', 'host2', null, path], + ['host1', 'host1', null, path1], + ['test6', 'host1', null, path1]]); +}, 'Event Path with nested ShadowRoots: closed > open.'); +</script> + +<div id="test7"> + <div id="host1"> + <template id="sr1" data-mode="closed"> + <div id="host2"> + <template id="sr2" data-mode="closed"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test7); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'sr2', 'host2', 'sr1', 'host1', 'test7']; + let path1 = ['host2', 'sr1', 'host1', 'test7']; + let path2 = ['host1', 'test7']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'host2', null, path1], + ['sr1', 'host2', null, path1], + ['host1', 'host1', null, path2], + ['test7', 'host1', null, path2]]); +}, 'Event Path with nested ShadowRoots: closed > closed.'); +</script> + +<div id="test8"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <slot id="slot"></slot> + </template> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test8); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target','slot', 'sr1', 'host1', 'test8']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['slot', 'target', null, path], + ['sr1', 'target', null, path], + ['host1', 'target', null, path], + ['test8', 'target', null, path]]); +}, 'Event Path with a slot in an open Shadow Root.'); +</script> + +<div id="test9"> + <div id="host1"> + <template id="sr1" data-mode="closed"> + <slot id="slot"></slot> + </template> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test9); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'slot', 'sr1', 'host1', 'test9']; + let path1 = ['target', 'host1', 'test9']; + assert_event_path_equals(log, [['target', 'target', null, path1], + ['slot', 'target', null, path], + ['sr1', 'target', null, path], + ['host1', 'target', null, path1], + ['test9', 'target', null, path1]]); +}, 'Event Path with a slot in a closed Shadow Root.'); +</script> + +<div id="test10"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="host2"> + <template id="sr2" data-mode="open"> + <slot id="slot2"></slot> + </template> + <slot id="slot1"></slot> + </div> + </template> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test10); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target','slot1', 'slot2', 'sr2', 'host2', 'sr1', 'host1', 'test10']; + assert_event_path_equals(log, [['target', 'target', null, path], + ['slot1', 'target', null, path], + ['slot2', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'target', null, path], + ['sr1', 'target', null, path], + ['host1', 'target', null, path], + ['test10', 'target', null, path]]); +}, 'Event Path with slots in nested ShadowRoots: open > open.'); +</script> + +<div id="test11"> + <div id="host1"> + <template id="sr1" data-mode="closed"> + <div id="host2"> + <template id="sr2" data-mode="closed"> + <slot id="slot2"></slot> + </template> + <slot id="slot1"></slot> + </div> + </template> + <div id="target"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test11); + let log = dispatchEventWithLog(n, n.target, new Event('my-event', { bubbles: true, composed: true })); + let path = ['target', 'slot1', 'slot2', 'sr2', 'host2', 'sr1', 'host1', 'test11']; + let path1 = ['target', 'slot1', 'host2', 'sr1', 'host1', 'test11']; + let path2 = ['target', 'host1', 'test11']; + assert_event_path_equals(log, [['target', 'target', null, path2], + ['slot1', 'target', null, path1], + ['slot2', 'target', null, path], + ['sr2', 'target', null, path], + ['host2', 'target', null, path1], + ['sr1', 'target', null, path1], + ['host1', 'target', null, path2], + ['test11', 'target', null, path2]]); +}, 'Event Path with slots in nested ShadowRoots: closed > closed.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-composed.html b/testing/web-platform/tests/shadow-dom/event-composed.html new file mode 100644 index 0000000000..2d6a5e3658 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-composed.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<title>Shadow DOM: Event composed</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> +<div id=host> + <template id=shadowRoot data-mode=open> + <div id=target></div> + </template> +</div> +<script> +test(() => { + const e = new Event('test'); + assert_equals(e.composed, false); +}, 'A new events composed value should be set to false by default.'); + +test(() => { + const e = new Event('test', { composed: true }); + assert_equals(e.composed, true); +}, 'Users should be able to set a composed value.'); + +function assertScoped(event) { + let nodes = createTestTree(host); + let log = dispatchEventWithLog(nodes, nodes.target, event); + let expectedPath = ['target', 'shadowRoot']; + assert_event_path_equals(log, + [['target', 'target', null, expectedPath], + ['shadowRoot', 'target', null, expectedPath]]); +} + +function assertComposed(event) { + let nodes = createTestTree(host); + let log = dispatchEventWithLog(nodes, nodes.target, event); + let expectedPath = ['target', 'shadowRoot', 'host']; + assert_event_path_equals(log, + [['target', 'target', null, expectedPath], + ['shadowRoot', 'target', null, expectedPath], + ['host', 'host', null, expectedPath]]); +} + +test(() => { + assertScoped(new Event('test', { bubbles: true })); +}, 'An event should be scoped by default'); + +test(() => { + assertComposed(new Event('test', { bubbles: true, composed: true })); +}, 'An event should not be scoped if composed is specified'); + +test(() => { + assertScoped(new MouseEvent('click', { bubbles: true })); +}, 'A synthetic MouseEvent should be scoped by default'); + +test(() => { + assertComposed(new MouseEvent('click', { bubbles: true, composed: true })); +}, 'A synthetic MouseEvent with composed=true should not be scoped'); + +test(() => { + assertScoped(new FocusEvent('focus', { bubbles: true })); +}, 'A synthetic FocusEvent should be scoped by default'); + +test(() => { + assertComposed(new FocusEvent('focus', { bubbles: true, composed: true })); +}, 'A synthetic FocusEvent with composed=true should not be scoped'); + +function assertUAComposed(eventType, callback) { + let nodes = createTestTree(host); + let log = dispatchUAEventWithLog(nodes, nodes.target, eventType, callback); + let expectedPath = ['target', 'shadowRoot', 'host']; + assert_event_path_equals(log, + [['target', 'target', null, expectedPath], + ['shadowRoot', 'target', null, expectedPath], + ['host', 'host', null, expectedPath]]); +} + +test(() => { + assertUAComposed('click', (target) => { target.click(); }); +}, 'A UA click event should not be scoped'); + +// TODO(hayato): Test other UIEvents. +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-dispatch-order.tentative.html b/testing/web-platform/tests/shadow-dom/event-dispatch-order.tentative.html new file mode 100644 index 0000000000..1e88740f53 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-dispatch-order.tentative.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<title>Shadow DOM: event dispatch order for capture and non-capture listerns at a shadow host</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="help" href="https://github.com/whatwg/dom/issues/685"> +<link rel="help" href="https://github.com/whatwg/dom/pull/686"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> +<div id=host> + <template id=shadowroot data-mode=open> + <div id=target></div> + </template> +</div> +<script> +test(() => { + let nodes = createTestTree(host); + let log = dispatchEventWithLog(nodes, nodes.target, + new Event('my-event', { bubbles: true, composed: true }), + { capture: true }); + let path = ['target', 'shadowroot', 'host']; + assert_event_path_equals(log, + [['host', 'host', null, path, 'capture'], + ['shadowroot', 'target', null, path, 'capture'], + ['target', 'target', null, path, 'capture'], + ['target', 'target', null, path, 'non-capture'], + ['shadowroot', 'target', null, path, 'non-capture'], + ['host', 'host', null, path, 'non-capture'], + ]); +}, 'Event dispatch order: capture listerns should be called in capturing phase at a shadow host'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-inside-shadow-tree.html b/testing/web-platform/tests/shadow-dom/event-inside-shadow-tree.html new file mode 100644 index 0000000000..a7405a5956 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-inside-shadow-tree.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: Firing an event inside a shadow tree</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="The event path calculation algorithm must be used to determine event path"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#event-paths"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> + +function dispatchEventWithLog(target, event) { + var log = []; + + for (var node = target; node; node = node.parentNode || node.host) { + node.addEventListener(event.type, (function (event) { + log.push([this, event.target]); + }).bind(node)); + } + + target.dispatchEvent(event); + + return log; +} + +function createShadowRootWithGrandChild(mode) { + var host = document.createElement('div'); + var root = host.attachShadow({mode: mode}); + + var parent = document.createElement('span'); + root.appendChild(parent); + + var target = document.createElement('b'); + parent.appendChild(target); + return {target: target, parent: parent, root: root, host: host}; +} + +function testEventInDetachedShadowTree(mode) { + test(function () { + var shadow = createShadowRootWithGrandChild(mode); + + log = dispatchEventWithLog(shadow.target, new Event('foo', {composed: true, bubbles: true})); + + assert_equals(log.length, 4, 'EventPath must contain [target, parent, shadow root, shadow host]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.parent, shadow.target], 'EventPath[1] must be the parent of the target'); + assert_array_equals(log[2], [shadow.root, shadow.target], 'EventPath[2] must be the shadow root'); + assert_array_equals(log[3], [shadow.host, shadow.host], 'EventPath[3] must be the shadow host'); + + }, 'Firing an event inside a grand child of a detached ' + mode + ' mode shadow tree'); +} + +testEventInDetachedShadowTree('open'); +testEventInDetachedShadowTree('closed'); + +function testEventInShadowTreeInsideDocument(mode) { + test(function () { + var shadow = createShadowRootWithGrandChild(mode); + document.body.appendChild(shadow.host); + + log = dispatchEventWithLog(shadow.target, new Event('foo', {composed: true, bubbles: true})); + + assert_equals(log.length, 7, 'EventPath must contain [target, parent, shadow root, shadow host, body, html, document]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.parent, shadow.target], 'EventPath[1] must be the parent of the target'); + assert_array_equals(log[2], [shadow.root, shadow.target], 'EventPath[2] must be the shadow root'); + assert_array_equals(log[3], [shadow.host, shadow.host], 'EventPath[3] must be the shadow host'); + assert_array_equals(log[4], [document.body, shadow.host], 'EventPath[4] must be the body element (parent of shadow host)'); + assert_array_equals(log[5], [document.documentElement, shadow.host], 'EventPath[5] must be the html element'); + assert_array_equals(log[6], [document, shadow.host], 'EventPath[6] must be the document node'); + + }, 'Firing an event inside a grand child of an in-document ' + mode + ' mode shadow tree'); +} + +testEventInShadowTreeInsideDocument('open'); +testEventInShadowTreeInsideDocument('closed'); + +function createNestedShadowRoot(innerMode, outerMode) { + var outerHost = document.createElement('div'); + var outerRoot = outerHost.attachShadow({mode: outerMode}); + + var outerChild = document.createElement('p'); + outerRoot.appendChild(outerChild); + + var innerHost = document.createElement('span'); + outerChild.appendChild(innerHost); + + var innerRoot = innerHost.attachShadow({mode: innerMode}); + var innerChild = document.createElement('span'); + innerRoot.appendChild(innerChild); + + return {target: innerChild, innerRoot: innerRoot, innerHost: innerHost, outerChild: outerChild, outerRoot: outerRoot, outerHost: outerHost}; +} + +function testEventInDetachedNestedShadowTree(innerMode, outerMode) { + test(function () { + var shadow = createNestedShadowRoot(innerMode, outerMode); + + log = dispatchEventWithLog(shadow.target, new Event('bar', {composed: true, bubbles: true})); + + assert_equals(log.length, 6, 'EventPath must contain [target, inner root, inner host, parent, outer root, outer host]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.innerRoot, shadow.target], 'EventPath[1] must be the inner shadow root'); + assert_array_equals(log[2], [shadow.innerHost, shadow.innerHost], 'EventPath[2] must be the inner shadow host'); + assert_array_equals(log[3], [shadow.outerChild, shadow.innerHost], 'EventPath[3] must be the parent of the inner shadow host'); + assert_array_equals(log[4], [shadow.outerRoot, shadow.innerHost], 'EventPath[4] must be the outer shadow root'); + assert_array_equals(log[5], [shadow.outerHost, shadow.outerHost], 'EventPath[5] must be the outer shadow host'); + + }, 'Firing an event inside a detached ' + innerMode + ' mode shadow tree inside ' + outerMode + ' mode shadow tree'); +} + +testEventInDetachedNestedShadowTree('open', 'open'); +testEventInDetachedNestedShadowTree('open', 'closed'); +testEventInDetachedNestedShadowTree('closed', 'open'); +testEventInDetachedNestedShadowTree('closed', 'closed'); + +function testEventInNestedShadowTreeInsideDocument(innerMode, outerMode) { + test(function () { + var shadow = createNestedShadowRoot(innerMode, outerMode); + document.body.appendChild(shadow.outerHost); + + log = dispatchEventWithLog(shadow.target, new Event('bar', {composed: true, bubbles: true})); + + assert_equals(log.length, 9, 'EventPath must contain [target, inner root, inner host, parent, outer root, outer host]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.innerRoot, shadow.target], 'EventPath[1] must be the inner shadow root'); + assert_array_equals(log[2], [shadow.innerHost, shadow.innerHost], 'EventPath[2] must be the inner shadow host'); + assert_array_equals(log[3], [shadow.outerChild, shadow.innerHost], 'EventPath[3] must be the parent of the inner shadow host'); + assert_array_equals(log[4], [shadow.outerRoot, shadow.innerHost], 'EventPath[4] must be the outer shadow root'); + assert_array_equals(log[5], [shadow.outerHost, shadow.outerHost], 'EventPath[5] must be the outer shadow host'); + assert_array_equals(log[6], [document.body, shadow.outerHost], 'EventPath[6] must be the body element'); + assert_array_equals(log[7], [document.documentElement, shadow.outerHost], 'EventPath[7] must be the html element'); + assert_array_equals(log[8], [document, shadow.outerHost], 'EventPath[8] must be the document node'); + + }, 'Firing an event inside an in-document ' + innerMode + ' mode shadow tree inside ' + outerMode + ' mode shadow tree'); +} + +testEventInNestedShadowTreeInsideDocument('open', 'open'); +testEventInNestedShadowTreeInsideDocument('open', 'closed'); +testEventInNestedShadowTreeInsideDocument('closed', 'open'); +testEventInNestedShadowTreeInsideDocument('closed', 'closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/event-inside-slotted-node.html b/testing/web-platform/tests/shadow-dom/event-inside-slotted-node.html new file mode 100644 index 0000000000..5f8d3b93c6 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-inside-slotted-node.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<html> +<head> + <title>Shadow DOM: Firing an event inside a node assigned to a slot</title> + <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> + <meta name="assert" content="The event path calculation algorithm must be used to determine event path"> + <link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#event-paths"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> + <div id="log"></div> + <script> + + function dispatchEventWithLog(shadow, event) { + var log = []; + + var attachedNodes = []; + for (var nodeKey in shadow) { + var startingNode = shadow[nodeKey]; + for (var node = startingNode; node; node = node.parentNode) { + if (attachedNodes.indexOf(node) >= 0) + continue; + attachedNodes.push(node); + node.addEventListener(event.type, (function (event) { + log.push([this, event.target]); + }).bind(node)); + } + } + + shadow.target.dispatchEvent(event); + + return log; + } + + function element(name, children, className) { + var element = document.createElement(name); + if (className) + element.className = className; + if (children) { + for (var child of children) + element.appendChild(child); + } + return element; + } + + function attachShadow(host, mode, children) { + var shadowRoot = host.attachShadow({mode: mode}); + if (children) { + for (var child of children) + shadowRoot.appendChild(child); + } + return shadowRoot; + } + + function createShadowHostWithAssignedGrandChild(mode) { + var host = element('div', [ + element('b', [ + element('i') + ]) + ]); + + var root = attachShadow(host, mode, [ + element('span', [ + element('slot') + ]) + ]); + + return {target: host.querySelector('i'), targetParent: host.querySelector('b'), host: host, + slot: root.querySelector('slot'), slotParent: root.querySelector('span'), root: root}; + } + + function testEventInDetachedShadowHostDescendant(mode) { + test(function () { + var shadow = createShadowHostWithAssignedGrandChild(mode); + + log = dispatchEventWithLog(shadow, new Event('foo', {bubbles: true, composed: true})); + + assert_equals(log.length, 6, 'EventPath must contain [target, target parent, slot, slot parent, shadow root, shadow host]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.targetParent, shadow.target], 'EventPath[1] must be the parent of the target'); + assert_array_equals(log[2], [shadow.slot, shadow.target], 'EventPath[2] must be the slot'); + assert_array_equals(log[3], [shadow.slotParent, shadow.target], 'EventPath[3] must be the parent of the slot'); + assert_array_equals(log[4], [shadow.root, shadow.target], 'EventPath[4] must be the shadow root'); + assert_array_equals(log[5], [shadow.host, shadow.target], 'EventPath[5] must be the shadow host'); + + }, 'Firing an event inside a grand child of a detached ' + mode + ' mode shadow host'); + } + + testEventInDetachedShadowHostDescendant('open'); + testEventInDetachedShadowHostDescendant('closed'); + + function testEventInShadowHostDescendantInsideDocument(mode) { + test(function () { + var shadow = createShadowHostWithAssignedGrandChild(mode); + document.body.appendChild(shadow.host); + + log = dispatchEventWithLog(shadow, new Event('foo', {bubbles: true, composed: true})); + + assert_equals(log.length, 9, 'EventPath must contain [target, target parent, slot, slot parent, shadow root, shadow host, body, html, document]'); + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.targetParent, shadow.target], 'EventPath[1] must be the parent of the target'); + assert_array_equals(log[2], [shadow.slot, shadow.target], 'EventPath[2] must be the slot'); + assert_array_equals(log[3], [shadow.slotParent, shadow.target], 'EventPath[3] must be the parent of the slot'); + assert_array_equals(log[4], [shadow.root, shadow.target], 'EventPath[4] must be the shadow root'); + assert_array_equals(log[5], [shadow.host, shadow.target], 'EventPath[5] must be the shadow host'); + assert_array_equals(log[6], [document.body, shadow.target], 'EventPath[6] must be the body element'); + assert_array_equals(log[7], [document.documentElement, shadow.target], 'EventPath[7] must be the html element'); + assert_array_equals(log[8], [document, shadow.target], 'EventPath[8] must be the html element'); + + }, 'Firing an event inside a grand child of an in-document ' + mode + ' mode shadow host'); + } + + testEventInShadowHostDescendantInsideDocument('open'); + testEventInShadowHostDescendantInsideDocument('closed'); + + function createNestedShadowTreesWithSlots(innerMode, outerUpperMode, outerLowerMode) { + var upperHost = element('upper-host', [ + element('p', [ + element('lower-host', [ + element('a') + ]) + ]) + ]); + + var upperShadow = attachShadow(upperHost, outerUpperMode, [ + element('b', [ + element('slot', [], 'upper-slot') + ]) + ]); + + var lowerHost = upperHost.querySelector('lower-host'); + var lowerShadow = attachShadow(lowerHost, outerLowerMode, [ + element('em', [ + element('inner-host', [ + element('span', [ + element('slot', [], 'lower-slot') + ]) + ]) + ]) + ]); + + innerShadow = attachShadow(lowerShadow.querySelector('inner-host'), innerMode, [ + element('i', [ + element('slot', [], 'inner-slot') + ]) + ]); + + return { + host: upperHost, + target: upperHost.querySelector('a'), + upperShadow: upperShadow, + upperSlot: upperShadow.querySelector('slot'), + lowerShadow: lowerShadow, + lowerSlot: lowerShadow.querySelector('slot'), + innerShadow: innerShadow, + innerSlot: innerShadow.querySelector('slot'), + }; + } + + /* + upper-host (14) -- (upperShadow; 13) + + p (10) + b (12) + | + slot (upperSlot; 11) + + lower-host (9) -- (lowerShadow; 8) + + a (target; 0) + em (7) + + inner-host (6) -------- (innerShadow; 5) + + span (2) + i (4) + + slot (lowerSlot; 1) + slot (innerSlot; 3) + */ + + function testEventUnderTwoShadowRoots(outerUpperMode, outerLowerMode, innerMode) { + test(function () { + var shadow = createNestedShadowTreesWithSlots(innerMode, outerUpperMode, outerLowerMode); + + log = dispatchEventWithLog(shadow, new Event('foo', {bubbles: true, composed: true})); + + assert_equals(log.length, 15, 'EventPath must contain 15 targets'); + + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.lowerSlot, shadow.target], 'EventPath[1] must be the slot inside the lower shadow tree'); + assert_array_equals(log[2], [shadow.lowerSlot.parentNode, shadow.target], 'EventPath[2] must be the parent of the slot inside the lower shadow tree'); + assert_array_equals(log[3], [shadow.innerSlot, shadow.target], 'EventPath[3] must be the slot inside the shadow tree inside the lower shadow tree'); + assert_array_equals(log[4], [shadow.innerSlot.parentNode, shadow.target], 'EventPath[4] must be the child of the inner shadow root'); + assert_array_equals(log[5], [shadow.innerShadow, shadow.target], 'EventPath[5] must be the inner shadow root'); + assert_array_equals(log[6], [shadow.innerShadow.host, shadow.target], 'EventPath[6] must be the host of the inner shadow tree'); + assert_array_equals(log[7], [shadow.lowerShadow.firstChild, shadow.target], 'EventPath[7] must be the parent of the inner shadow host'); + assert_array_equals(log[8], [shadow.lowerShadow, shadow.target], 'EventPath[8] must be the lower shadow root'); + assert_array_equals(log[9], [shadow.lowerShadow.host, shadow.target], 'EventPath[9] must be the lower shadow host'); + assert_array_equals(log[10], [shadow.host.firstChild, shadow.target], 'EventPath[10] must be the parent of the grand parent of the target'); + assert_array_equals(log[11], [shadow.upperSlot, shadow.target], 'EventPath[11] must be the slot inside the upper shadow tree'); + assert_array_equals(log[12], [shadow.upperSlot.parentNode, shadow.target], 'EventPath[12] must be the parent of the slot inside the upper shadow tree'); + assert_array_equals(log[13], [shadow.upperShadow, shadow.target], 'EventPath[13] must be the upper shadow root'); + assert_array_equals(log[14], [shadow.host, shadow.target], 'EventPath[14] must be the host'); + + }, 'Firing an event on a node with two ancestors with a detached ' + outerUpperMode + ' and ' + outerLowerMode + + ' shadow trees with an inner ' + innerMode + ' shadow tree'); + } + + testEventUnderTwoShadowRoots('open', 'open', 'open'); + testEventUnderTwoShadowRoots('open', 'open', 'closed'); + testEventUnderTwoShadowRoots('open', 'closed', 'open'); + testEventUnderTwoShadowRoots('open', 'closed', 'closed'); + testEventUnderTwoShadowRoots('closed', 'open', 'open'); + testEventUnderTwoShadowRoots('closed', 'open', 'closed'); + testEventUnderTwoShadowRoots('closed', 'closed', 'open'); + testEventUnderTwoShadowRoots('closed', 'closed', 'closed'); + + /* + upper-host (11) -- (upperShadow; 10) + + p (7) + b (9) + | + slot (upperSlot; 8) + + lower-host (6) -- (lowerShadow; 5) + + a + em (4) + + inner-host (3) -- (innerShadow; 2) + + span + i (1) + + slot + slot (innerSlot, target; 0) + */ + + function testEventInsideNestedShadowsUnderAnotherShadow(outerUpperMode, outerLowerMode, innerMode) { + test(function () { + var shadow = createNestedShadowTreesWithSlots(innerMode, outerUpperMode, outerLowerMode); + shadow.deepestNodeInLightDOM = shadow.target; // Needed for dispatchEventWithLog to attach event listeners. + shadow.target = shadow.innerSlot; + + log = dispatchEventWithLog(shadow, new Event('foo', {bubbles: true, composed: true})); + + assert_equals(log.length, 12, 'EventPath must contain 12 targets'); + + assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target'); + assert_array_equals(log[1], [shadow.target.parentNode, shadow.target], 'EventPath[1] must be the parent of the target'); + assert_array_equals(log[2], [shadow.innerShadow, shadow.target], 'EventPath[2] must be the inner shadow root'); + assert_array_equals(log[3], [shadow.innerShadow.host, shadow.innerShadow.host], 'EventPath[3] must be the inner shadow host'); + assert_array_equals(log[4], [shadow.lowerShadow.firstChild, shadow.innerShadow.host], 'EventPath[4] must be the parent of the inner shadow host'); + assert_array_equals(log[5], [shadow.lowerShadow, shadow.innerShadow.host], 'EventPath[5] must be the lower (but outer) shadow root'); + assert_array_equals(log[6], [shadow.lowerShadow.host, shadow.lowerShadow.host], 'EventPath[6] must be the lower (but outer) shadow root'); + assert_array_equals(log[7], [shadow.host.firstChild, shadow.lowerShadow.host], 'EventPath[7] must be the slot inside the upper shadow tree'); + assert_array_equals(log[8], [shadow.upperSlot, shadow.lowerShadow.host], 'EventPath[8] must be the slot inside the upper shadow tree'); + assert_array_equals(log[9], [shadow.upperSlot.parentNode, shadow.lowerShadow.host], 'EventPath[9] must be the parent of the slot inside the upper shadow tree'); + assert_array_equals(log[10], [shadow.upperShadow, shadow.lowerShadow.host], 'EventPath[10] must be the upper shadow root'); + assert_array_equals(log[11], [shadow.upperShadow.host, shadow.lowerShadow.host], 'EventPath[11] must be the host'); + + }, 'Firing an event on a node within a ' + innerMode + ' shadow tree that is itself a ' + outerLowerMode + + ' shadow tree (the latter being the descendent of a host for a separate ' + outerUpperMode + ' shadow tree)'); + } + + testEventInsideNestedShadowsUnderAnotherShadow('open', 'open', 'open'); + testEventInsideNestedShadowsUnderAnotherShadow('open', 'open', 'closed'); + testEventInsideNestedShadowsUnderAnotherShadow('open', 'closed', 'open'); + testEventInsideNestedShadowsUnderAnotherShadow('open', 'closed', 'closed'); + testEventInsideNestedShadowsUnderAnotherShadow('closed', 'open', 'open'); + testEventInsideNestedShadowsUnderAnotherShadow('closed', 'open', 'closed'); + testEventInsideNestedShadowsUnderAnotherShadow('closed', 'closed', 'open'); + testEventInsideNestedShadowsUnderAnotherShadow('closed', 'closed', 'closed'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/event-on-pseudo-element-crash.html b/testing/web-platform/tests/shadow-dom/event-on-pseudo-element-crash.html new file mode 100644 index 0000000000..c03f51a64f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-on-pseudo-element-crash.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://crbug.com/1180286"> +<meta name="assert" content="The renderer should not crash."> + +<span>This test passes if the renderer does not crash.</span> + +<div> + <option></option> +</div> + +<style> + div { + direction: rtl; + zoom: 0.01; + } + option { + zoom: 5; + } + option::before { + display: table-caption; + overflow: scroll; + content: open-quote; + padding-right: 1px; + -webkit-column-width: 1px; + } +</style> diff --git a/testing/web-platform/tests/shadow-dom/event-post-dispatch.html b/testing/web-platform/tests/shadow-dom/event-post-dispatch.html new file mode 100644 index 0000000000..239881c7e1 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-post-dispatch.html @@ -0,0 +1,226 @@ +<!DOCTYPE html> +<title>Shadow DOM: Event dispatch post result for event properties.</title> +<meta name="author" title="Eriko Kurimoto" href="mailto:elkurin@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/event-path-test-helpers.js"></script> + +<div id="test1"> + <div id="d1"> + <div id="target"></div> + </div> +</div> + +<script> +let n1 = createTestTree(test1); +document.body.appendChild(n1.test1); +test(() => { + let log = dispatchEventWithEventLog(n1, n1.target, new Event('my-event', { bubbles: true, composed: true })); + assert_equals(log.event.target, n1.target); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch without ShadowRoots (composed: true).'); + +test(() => { + let log = dispatchEventWithEventLog(n1, n1.target, new Event('my-event', { bubbles: true, composed: false })); + assert_equals(log.event.target, n1.target); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch without ShadowRoots (composed: false).'); +document.body.removeChild(n1.test1); +</script> + +<div id="test2"> + <div id="host"> + <template id="sr" data-mode="open"> + <div id="target"></div> + </template> + </div> +</div> + +<script> +let n2 = createTestTree(test2); +document.body.appendChild(n2.test2); +test(() => { + let log = dispatchEventWithEventLog(n2, n2.target, new Event('my-event', { bubbles: true, composed: true })); + assert_equals(log.event.target, n2.host); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with an open ShadowRoot (composed: true).'); + +test(() => { + let log = dispatchEventWithEventLog(n2, n2.target, new Event('my-event', { bubbles: true, composed: false })); + assert_equals(log.event.target, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with an open ShadowRoot (composed: false).'); +document.body.removeChild(n2.test2); +</script> + +<div id="test3"> + <div id="host"> + <template id="sr" data-mode="closed"> + <div id="target"></div> + </template> + </div> +</div> + +<script> +let n3 = createTestTree(test3); +document.body.appendChild(n3.test3); +test(() => { + let log = dispatchEventWithEventLog(n3, n3.target, new Event('my-event', { bubbles: true, composed: true })); + assert_equals(log.event.target, n3.host); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with a closed ShadowRoot (composed: true).'); + +test(() => { + let log = dispatchEventWithEventLog(n3, n3.target, new Event('my-event', { bubbles: true, composed: false })); + assert_equals(log.event.target, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with a closed ShadowRoot (composed: false).'); +document.body.removeChild(n3.test3); +</script> + +<div id="test4"> + <div id="host1"> + <template id="sr" data-mode="open"> + <div id="host2"> + <template id="sr" data-mode="open"> + <div id="target"></div> + </template> + </div> + </template> + </div> +</div> + +<script> +let n4 = createTestTree(test4); +document.body.appendChild(n4.test4); +test(() => { + let log = dispatchEventWithEventLog(n4, n4.target, new Event('my-event', { bubbles: true, composed: true })); + assert_equals(log.event.target, n4.host1); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with nested ShadowRoots (composed: true).'); + +test(() => { + let log = dispatchEventWithEventLog(n4, n4.target, new Event('my-event', { bubbles: true, composed: false })); + assert_equals(log.event.target, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with nested ShadowRoots (composed: false).'); +document.body.removeChild(n4.test4); +</script> + +<div id="test5"> + <div id="host"> + <template id="sr" data-mode="open"> + <div id="relatedTarget"> + <div id="target"></div> + </div> + </template> + </div> +</div> + +<script> +let n5 = createTestTree(test5); +document.body.appendChild(n5.test5); +test(() => { + let log = dispatchEventWithEventLog(n5, n5.target, new MouseEvent('my-event', {bubbles: true, compoesed: true, relatedTarget: n5.relatedTarget})); + assert_equals(log.event.target, null); + assert_equals(log.event.relatedTarget, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the same shadow tree. (composed: true)'); + +test(() => { + let log = dispatchEventWithEventLog(n5, n5.target, new MouseEvent('my-event', {bubbles: true, compoesed: false, relatedTarget: n5.relatedTarget})); + assert_equals(log.event.target, null); + assert_equals(log.event.relatedTarget, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the same shadow tree. (composed: false)'); +document.body.removeChild(n5.test5); +</script> + +<div id="test6"> + <div id="host"> + <template id="sr" data-mode="open"> + <div id="target"></div> + </template> + </div> + <div id="relatedTarget"></div> +</div> + +<script> +let n6 = createTestTree(test6); +document.body.appendChild(n6.test6); +test(() => { + let log = dispatchEventWithEventLog(n6, n6.target, new MouseEvent('my-event', {bubbles: true, composed: true, relatedTarget: n6.relatedTarget})); + assert_equals(log.event.target, n6.host); + assert_equals(log.event.relatedTarget, n6.relatedTarget); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the document tree and the shadow tree. (composed: true)'); + +test(() => { + let log = dispatchEventWithEventLog(n6, n6.target, new MouseEvent('my-event', {bubbles: true, composed: false, relatedTarget: n6.relatedTarget})); + assert_equals(log.event.target, null); + assert_equals(log.event.relatedTarget, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the document tree and the shadow tree. (composed: false)'); +document.body.removeChild(n6.test6); +</script> + +<div id="test7"> + <div id="host1"> + <template id="sr1" data-mode="open"> + <div id="target"></div> + </template> + </div> + <div id="host2"> + <template id="sr2" data-mode="open"> + <div id="relatedTarget"></div> + </template> + </div> +</div> + +<script> +let n7 = createTestTree(test7); +document.body.appendChild(n7.test7); +test(() => { + let log = dispatchEventWithEventLog(n7, n7.target, new MouseEvent('my-event', {bubbles: true, composed: true, relatedTarget: n7.relatedTarget})); + assert_equals(log.event.target, n7.host1); + assert_equals(log.event.relatedTarget, n7.host2); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the different shadow trees. (composed: true)'); + +test(() => { + let log = dispatchEventWithEventLog(n7, n7.target, new MouseEvent('my-event', {bubbles: true, composed: false, relatedTarget: n7.relatedTarget})); + assert_equals(log.event.target, null); + assert_equals(log.event.relatedTarget, null); + assert_equals(log.event.eventPhase, 0); + assert_equals(log.event.currentTarget, null); + assert_equals(log.event.composedPath().length, 0); +}, 'Event properties post dispatch with relatedTarget in the different shadow trees. (composed: false)'); +document.body.removeChild(n7.test7); +</script> diff --git a/testing/web-platform/tests/shadow-dom/event-with-related-target.html b/testing/web-platform/tests/shadow-dom/event-with-related-target.html new file mode 100644 index 0000000000..77da447602 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/event-with-related-target.html @@ -0,0 +1,256 @@ +<!DOCTYPE html> +<html> +<head> + <title>Shadow DOM: Firing an event with relatedTarget inside a shadow tree</title> + <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> + <meta name="assert" content="The retargeting algorithm is used to determine relative targets"> + <link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#retargeting-relatedtarget"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/event-path-test-helpers.js"></script> +</head> +<body> + <div id="log"></div> + <script> + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A ----------------------------------------------- A-SR + + B ----------- B-SR (4) + A1 --- A1-SR + + C + B1 (3) [*; 0-4] --- B1-SR (2) + A2-S + A1a + + D --- D-SR + B1a (*; 0) + B1b [1,2] --- B1b-SR + + D1 + B1c-S (1) + B1b1 + + B1b2 + */ + function testEventAtB1aWithB1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1})); + + assert_array_equals(log.eventPath, + ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['B1', 'B1', 'B1', 'B1', 'B1'], 'The related targets must be correct.'); + + }, 'Firing an event at B1a with relatedNode at B1 with ' + mode + ' mode shadow trees'); + } + + testEventAtB1aWithB1a('open'); + testEventAtB1aWithB1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A ------------------------------------------------- A-SR + + B ----------- B-SR (4) + A1 --- A1-SR + + C + B1 (3) [0,3-4] --- B1-SR (2) + A2-S + A1a + + D --- D-SR + B1a (*; 0) + B1b [1,2] --- B1b-SR + + D1 + B1c-S (1) + B1b1 [*] + + B1b2 + */ + function testEventAtB1aWithB1b1(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1b1})); + + assert_array_equals(log.eventPath, + ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['B1', 'B1b', 'B1b', 'B1', 'B1'], 'The related targets must be correct.'); + + }, 'Firing an event at B1a with relatedNode at B1b1 with ' + mode + ' mode shadow trees'); + } + + testEventAtB1aWithB1b1('open'); + testEventAtB1aWithB1b1('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A -------------------------------------------------- A-SR + + B ------------- B-SR (5) + A1 --- A1-SR + + C + B1 (4) ------- B1-SR (3) + A2-S + A1a + + D --- D-SR + B1a [*; 0-5] + B1b (2) --- B1b-SR (1) + + D1 + B1c-S + B1b1 (*; 0) + + B1b2 + */ + function testEventAtB1b1WithB1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.B1b1, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1a})); + + assert_array_equals(log.eventPath, + ['B1b1', 'B1b-SR', 'B1b', 'B1-SR', 'B1', 'B-SR'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['B1a', 'B1a', 'B1a', 'B1a', 'B1a', 'B1a'], 'The related targets must be correct.'); + + }, 'Firing an event at B1b1 with relatedNode at B1a with ' + mode + ' mode shadow trees'); + } + + testEventAtB1b1WithB1a('open'); + testEventAtB1b1WithB1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A (8) -------------------------------------------------- A-SR (7) + + B (5) -------------- B-SR (4) + A1 -------- A1-SR + + C + B1 (3) [*; 0-4] --- B1-SR (2) + A2-S (6) + A1a + + D [0-8] --- D-SR + B1a (*; 0) + B1b ------ B1b-SR + + D1 [*] + B1c-S (1) + B1b1 + + B1b2 + */ + function testEventAtB1aWithD1(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.D1})); + + assert_array_equals(log.eventPath, + ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D'], 'The related targets must be correct.'); + + }, 'Firing an event at B1a with relatedNode at D1 with ' + mode + ' mode shadow trees'); + } + + testEventAtB1aWithD1('open'); + testEventAtB1aWithD1('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A (6) ----------------------------------- A-SR (5) + + B (3) [0] ----------- B-SR + A1 ------ A1-SR + + C + B1 ----- B1-SR + A2-S (4) + A1a + + D (2) --- D-SR (1) + B1a [*] + B1b --- B1b-SR + + D1 (*; 0) + B1c-S + B1b1 + + B1b2 + */ + function testEventAtD1WithB1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.D1, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1a})); + + assert_array_equals(log.eventPath, + ['D1', 'D-SR', 'D', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['B', 'B', 'B', 'B', 'B', 'B', 'B'], 'The related targets must be correct.'); + + }, 'Firing an event at D1 with relatedNode at B1a with ' + mode + ' mode shadow trees'); + } + + testEventAtD1WithB1a('open'); + testEventAtD1WithB1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A (8) [0-5,8] ---------------------------------------- A-SR (7) + + B (5) ------- B-SR (4) + A1 [6,7] --- A1-SR + + C + B1 (3) ----- B1-SR (2) + A2-S (6) + A1a [*] + + D --- D-SR + B1a (*; 0) + B1b ------- B1b-SR + + D1 + B1c-S (1) + B1b1 + + B1b2 + */ + function testEventAtB1aWithA1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.A1a})); + + assert_array_equals(log.eventPath, + ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['A', 'A', 'A', 'A', 'A', 'A', 'A1', 'A1', 'A'], 'The related targets must be correct.'); + + }, 'Firing an event at B1a with relatedNode at A1a with ' + mode + ' mode shadow trees'); + } + + testEventAtB1aWithA1a('open'); + testEventAtB1aWithA1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A (4) ----------------------------------------- A-SR (3) + + B [0-4] ----- B-SR + A1 (2) --- A1-SR (1) + + C + B1 ------- B1-SR + A2-S + A1a (*; 0) + + D --- D-SR + B1a [*] + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 + */ + function testEventAtA1aWithB1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + log = dispatchEventWithEventLog(nodes, nodes.A1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1a})); + + assert_array_equals(log.eventPath, + ['A1a', 'A1-SR', 'A1', 'A-SR', 'A'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['B', 'B', 'B', 'B', 'B'], 'The related targets must be correct.'); + + }, 'Firing an event at A1a with relatedNode at B1a with ' + mode + ' mode shadow trees'); + } + + testEventAtA1aWithB1a('open'); + testEventAtA1aWithB1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A (8) ----------------------------------- A-SR (7) + + B (5) ----- B-SR (4) + A2-S (6) + + C + B1 (3) ----- B1-SR (2) + + D --- D-SR + B1a (*; 0) + B1b ------- B1b-SR + + D1 + B1c-S (1) + B1b1 + + B1b2 + A1 [0-6] --- A1-SR + + A1a [*] + */ + function testEventAtB1aWithDetachedA1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + nodes['A-SR'].removeChild(nodes.A1); + log = dispatchEventWithEventLog(nodes, nodes.B1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.A1a})); + + assert_array_equals(log.eventPath, + ['B1a', 'B1c-S', 'B1-SR', 'B1', 'B-SR', 'B', 'A2-S', 'A-SR', 'A'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, + ['A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1'], 'The related targets must be correct.'); + + }, 'Firing an event at B1a with relatedNode at A1a (detached) with ' + mode + ' mode shadow trees'); + } + + testEventAtB1aWithDetachedA1a('open'); + testEventAtB1aWithDetachedA1a('closed'); + + /* + -SR: ShadowRoot -S: Slot target: (~) relatedTarget: [~] *: indicates start digit: event path order + A ----------------------------------- A-SR + + B [0-3] ----- B-SR + A2-S + + C + B1 -------- B1-SR + + D --- D-SR + B1a [*] + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 + A1 (2) --- A1-SR (1) + + A1a (*; 0) + */ + function testEventAtA1aWithDetachedB1a(mode) { + test(function () { + var nodes = createFixedTestTree(mode); + + nodes['A-SR'].removeChild(nodes.A1); + log = dispatchEventWithEventLog(nodes, nodes.A1a, new MouseEvent('foo', {bubbles: true, composed: true, relatedTarget: nodes.B1a})); + + assert_array_equals(log.eventPath, ['A1a', 'A1-SR', 'A1'], 'The event path must be correct.'); + assert_array_equals(log.relatedTargets, ['B', 'B', 'B' ], 'The related targets must be correct.'); + + }, 'Firing an event at A1a with relatedNode at B1a (detached) with ' + mode + ' mode shadow trees'); + } + + testEventAtA1aWithDetachedB1a('open'); + testEventAtA1aWithDetachedB1a('closed'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html b/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html new file mode 100644 index 0000000000..dde18128ad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<!-- Adapted from http://jsbin.com/dexinu/6/edit for layout test --> + +<template id='XMenuTemplate'> + <style> + :host { + display: inline-block; + position: relative; + background-color: #aaa; + } + :host(:focus) { + background-color: #ccc; + } + li { + display: inline-block; + position: relative; + background-color: #eee; + } + li:focus { + background-color: #fff; + } + </style> + <li tabindex='0'>Item One</li> + <li tabindex='0'>Item Two</li> + <li tabindex='0'>Item Three</li> +</template> + +<section> + <x-menu id='XMenu1' tabindex='0'></x-menu> +</section> +<section> + <x-menu id='XMenu2' tabindex='0' delegatesFocus></x-menu> + <x-menu id='XMenu3' tabindex='0' delegatesFocus></x-menu> +</section> +<section> + <x-menu id='XMenu4' tabindex='0' delegatesFocus></x-menu> +</section> + +<script> +'use strict'; + +const template = document.querySelector('#XMenuTemplate'); + +customElements.define('x-menu', class extends HTMLElement { + connectedCallback() { + const delegatesFocus = this.hasAttribute('delegatesFocus'); + this.attachShadow({mode: 'open', delegatesFocus: delegatesFocus}) + .appendChild(document.importNode(template.content, true)); + } +}); + +promise_test(async () => { + let xmenu1 = document.getElementById('XMenu1'); + + xmenu1.focus(); + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu1'); + assert_background_color('XMenu1', 'rgb(204, 204, 204)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu2'); + await assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + await assert_background_color('XMenu2', 'rgb(204, 204, 204)'); + await assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + await assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu3'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(204, 204, 204)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu4'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(204, 204, 204)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu3'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(204, 204, 204)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu2'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(204, 204, 204)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu1'); + assert_background_color('XMenu1', 'rgb(204, 204, 204)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); +}, 'crbug/474687 :focus style should properly be applied to shadow hosts.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html new file mode 100644 index 0000000000..365edb00b8 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across shadow boundaries.<br> +To manually test, press tab key six times then shift+tab seven times.<br> +It should traverse focusable elements in the increasing numerical order and then in the reverse order.</p> + +<div id='host'> + <div slot='slot5' id='i2' tabindex=4>2. Assigned to slot5 whose tabindex is 2.</div> + <template data-mode='open'> + <slot name='slot1'> + x. The default tabindex for a slot node is set to 0. + <div id='i5' tabindex=0>5. The parent slot node's tabindex is 0. Second.</div> + <div id='i4' tabindex=2>4. The parent slot node's tabindex is 0. First.</div> + </slot> + + <slot name='slot2' id='x1' tabindex=3> + x. The tabindex is 3. The slot node should be ignored. + <div id='i3' tabindex=10>3. The parent slot node's tabindex is 3. The slot node's tabindex matters. This element's tabindex comes after.</div> + </slot> + + <slot name='slot3' id='x2' tabindex=0> + x. The tabindex is 0. The slot node should be ignored. If there is another slot node in same tabindex, the younger child comes first. + <div id='i6' tabindex=1>6. The parent slot node's tabindex is 0. First.</div> + <div id='i7' tabindex=1>7. The parent slot node's tabindex is 0. Second.</div> + </slot> + + <slot name='slot4' id='x3' tabindex=1> + x. The tabindex is 1. The slot node should be ignored. + <div id='i1' tabindex=5>1. The slot node tabindex is 1.</div> + </slot> + + <slot name='slot5' id='x5' tabindex=2> + x. The tabindex is 2. The slot node should be ignored. The host child is assigned to this slot node. + <div id='-' tabindex=1>-. The host child is assigned to the parent slot node. This text shouldn't apeare.</div> + </slot> + + <slot name='slot6' id='x6' tabindex=5> + x. The tabindex is 5. The slot node should be ignored. + <div id='x6' tabindex=-1>x. tabindex is -1. Should be skipped.</div> + </slot> + + <slot name='slot7' id='x7' tabindex=-1> + x. tabindex is -1. Should be skipped. + <div id='x8' tabindex=1>x. The parent slot node is skipped.</div> + </slot> + </template> +</div> +<script> + +promise_test(async () => { + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'host/i1', + 'i2', + 'host/i3', + 'host/i4', + 'host/i5', + 'host/i6', + 'host/i7' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Default tabindex for a slot node should be 0.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html new file mode 100644 index 0000000000..54ee14c34b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> j2 -> [s1]]<br> + <b>slot #s1: [k1 -> [x-bar] -> k0 -> [s2] -> [s3]]</b><br> + x-bar's shadow tree: [m1 -> m2]<br> + slot #s2: [i1 -> i2]<br> + <b>slot #s3: [l1]<b><br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> j2 -> k1 -> x-bar -> m1 -> m2 -> k0 -> i1 -> i2 -> l1]</b><br> +</p> + +<input id='i0' tabindex=0 value='i0'> +<div id='x-foo'> + <input id='i2' slot='s2' tabindex=2 value='i2'> + <input id='i1' slot='s2' tabindex=1 value='i1'> + <template data-mode='open'> + <input id='j1' tabindex=1 value='j1'> + <slot id='s1' name='s1'> <!-- This slot does not have any assigned elements --> + <input id='k0' tabindex=0 value='k0'> + <input id='k1' tabindex=1 value='k1'> + <slot id='s2' name='s2'> + <input id='should-be-ignored'> + </slot> + <slot id='s3' name='s3'> <!-- This slot does not have any assigned elements --> + <input id='l1' value='l1'> + </slot> + <div id='x-bar' tabindex=2> + <template data-mode='open'> + <input id='m2' value='m2' tabindex=2> + <input id='m1' value='m1' tabindex=1> + </template> + </div> + </slot> + <input id='j2' tabindex=2 value='j2'> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'x-foo/j2', + 'x-foo/k1', + 'x-foo/x-bar', + 'x-foo/x-bar/m1', + 'x-foo/x-bar/m2', + 'x-foo/k0', + 'i1', + 'i2', + 'x-foo/l1' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should jump to fallback elements when a slot does not have any assigned nodes.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html new file mode 100644 index 0000000000..7d733ea7c2 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id='i0'> +<div id='outer'> + <template data-mode='open'> + <input id='outer-before'> + <slot></slot> + <input id='outer-after'> + </template> + <div id='dummy1'></div> + <div id='nested1'> + <template data-mode='open'> + <input id='inner-before'> + <div id='inner-div' tabindex='0'><slot></slot></div> + <input id='inner-after'> + </template> + <div id='dummy2'></div> + <div id='nested2'> + <template data-mode='open'> + <input id='innermost-before'> + <slot></slot> + <input id='innermost-after'> + </template> + <input id='innermost1'> + <input id='innermost2'> + </div> + <span>button</span> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var outer = document.querySelector('#outer'); + convertTemplatesToShadowRootsWithin(outer); + + var elements = [ + 'i0', + 'outer/outer-before', + 'nested1/inner-before', + 'nested1/inner-div', + 'nested2/innermost-before', + 'innermost1', + 'innermost2', + 'nested2/innermost-after', + 'nested1/inner-after', + 'outer/outer-after', + 'i1' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html new file mode 100644 index 0000000000..5e4ab3ce4b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<!-- +This test case is based on the crbug.com/618587 reproduction case: +http://jsbin.com/bonudiwagu/1/edit?html,output +--> +<input id='i0'> +<div id='x-foo'> + <template data-mode='open' data-delegatesFocus> + <input id='inner-before'> + <slot></slot> + <input id='inner-after'> + </template> + <div id='nested'> + <template data-mode='open' data-delegatesFocus> + <input id='nested-x'> + <slot></slot> + <input id='nested-y'> + </template> + <input id='light'> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var xFoo = document.querySelector('#x-foo'); + convertTemplatesToShadowRootsWithin(xFoo); + + var elements = [ + 'i0', + 'x-foo/inner-before', + 'nested/nested-x', + 'light', + 'nested/nested-y', + 'x-foo/inner-after', + 'i1' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html new file mode 100644 index 0000000000..66d8c14b2b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across shadow boundaries.<br> +To manually test, press tab key six times then shift+tab six times.<br> +It should traverse focusable elements in the increasing numerical order and then in the reverse order.</p> +<div id='host'> + <template data-mode='open'> + <slot name='slot1'>Slot shouldn't be focused. + <slot name='slot2'>Slot shouldn't be focused. + <div id='non1' tabindex=0>This text shouldn't appear.</div> + </slot> + <slot name='slot3'>Slot shouldn't be focused. + <div id='second' tabindex=0>2. No host child is assigned to slot3.</div> + </slot> + </slot> + + <div id='third' tabindex=0>3. Inner Shadow Host. + <template data-mode='open'> + <slot name='slot4'>Slot shouldn't be focused. + <slot name='slot5'>Slot shouldn't be focused. + <div id='non2' tabindex=0>This text shouldn't appear.</div> + </slot> + </slot> + </template> + <div id='fourth' slot='slot4' tabindex=0>4. Assigned to slot4.</div> + <div id='non3' slot='slot5' tabindex=0> + This text shouldn't appear. slot5 is in the fallback content of slot4 which has assigned nodes.</div> + </div> + + <div id='fifth' tabindex=0>5. Inner Shadow Host. + <template data-mode='open'> + <slot name='slot6'>Slot shouldn't be focused. + <div id='non4' tabindex=0>This text shouldn't appear.</div> + </slot> + </template> + <slot name='slot7' slot='slot6'>Slot shouldn't be focused. Assigned to slot6. + <div id='non5' tabindex=0>This text shouldn't appear.</div> + </slot> + </div> + </template> + <div id='first' slot='slot2' tabindex=0>1. Assigned to slot2.</div> + <div id='sixth' slot='slot7' tabindex=0>6. Assigned to slot7 which is assigned to slot6.</div> +</div> +<script> +'use strict'; + +promise_test(async () => { + let host = document.getElementById('host'); + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'first', + 'host/second', + 'host/third', + 'host/fourth', + 'host/fifth', + 'sixth' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should cover assigned elements of an assigned slot espacially there are fallback contents.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html new file mode 100644 index 0000000000..ae966afc76 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<!-- +This test case is based on the crbug.com/618587 reproduction case: +http://jsbin.com/bonudiwagu/1/edit?html,output +--> +<input id='i0'> +<div id='x-foo'> + <template data-mode='open'> + <input id='inner-before'> + <slot></slot> + <input id='inner-after'> + </template> + <div id='nested'> + <template data-mode='open'> + <input id='nested-x'> + <slot></slot> + <input id='nested-y'> + </template> + <input id='light'> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var xFoo = document.querySelector('#x-foo'); + convertTemplatesToShadowRootsWithin(xFoo); + + var elements = [ + 'i0', + 'x-foo/inner-before', + 'nested/nested-x', + 'light', + 'nested/nested-y', + 'x-foo/inner-after', + 'i1' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html new file mode 100644 index 0000000000..418da8f5ff --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id="i0" tabindex=0 value="i0"> +<div id="fallback"> + <template data-mode="open"> + <slot id="s0" name="s0"> + <div id="x-foo"> + <template data-mode="open"> + <input id="a2" tabindex=3 value="a2"> + <slot id="s1" name="s1" tabindex=2> + <slot id="s2" name="s2"> + <input id="a1" slot="s2" value="a1"> + </slot> + </slot> + <input id="a0" tabindex=1 value="a0"> + </template> + </div> + </slot> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let fallback = document.getElementById('fallback'); + convertTemplatesToShadowRootsWithin(fallback); + + let elements = [ + 'i0', + 'fallback/x-foo/a0', + 'fallback/x-foo/a1', + 'fallback/x-foo/a2' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html new file mode 100644 index 0000000000..eef3e1b611 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id="i0" tabindex=0 value="i0"> +<div id="assigned"> + <template data-mode="open"> + <slot id="s0" name="s0"> + <div id="x-foo"> + <input id="a1" slot="s2" value="a1"> + <template data-mode="open"> + <input id="a2" tabindex=3 value="a2"> + <slot id="s1" name="s1" tabindex=2> + <slot id="s2" name="s2"></slot> + </slot> + <input id="a0" tabindex=1 value="a0"> + </template> + </div> + </slot> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let assigned = document.getElementById('assigned'); + convertTemplatesToShadowRootsWithin(assigned); + + let elements = [ + 'i0', + 'assigned/x-foo/a0', + 'assigned/a1', + 'assigned/x-foo/a2' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html new file mode 100644 index 0000000000..d43085e984 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> [s1] -> [s2] -> j2 ->[x-bar]]<br> + x-bar's shadow tree: [[s3] -> k1]<br> + slot #s1: [i1 -> i2]<br> + slot #s2: [i3]<br> + slot #s3: [l1 -> l2]<br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> i1 -> i2 -> i3 -> j2 -> x-bar -> l1 -> l2 -> k1]</b><br> +</p> + +<input id='i0' tabindex=0 value='i0'> +<div id='x-foo'> + <input id='i2' slot='s1' tabindex=2 value='i2'> + <input id='i1' slot='s1' tabindex=1 value='i1'> + <input id='i3' slot='s2' tabindex=3 value='i3'> + <template data-mode='open'> + <div id='x-bar' tabindex=5> + <input id='l2' slot='s3' tabindex=2 value='l2'> + <input id='l1' slot='s3' tabindex=1 value='l1'> + <template data-mode='open'> + <slot id='s3' name='s3' tabindex=1></slot> + <input id='k1' tabindex=2 value='k1'> + </template> + </div> + <input id='j1' tabindex=1 value='j1'> + <slot id='s2' name='s2' tabindex=3></slot> + <slot id='s1' name='s1' tabindex=2></slot> + <input id='j2' tabindex=4 value='j2'> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'i1', + 'i2', + 'i3', + 'x-foo/j2', + 'x-foo/x-bar', + 'x-foo/l1', + 'x-foo/l2', + 'x-foo/x-bar/k1', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Slots tabindex should be considred in focus navigation.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html new file mode 100644 index 0000000000..39b0806603 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across nodes in slot scope.<br> + +<div id="b1" tabindex="0">outside</div> +<div id='host'> + <template data-mode='open'> + <slot></slot> + </template> + <slot> + <div id="1A" tabindex="0">single nested slot</div> + <div id="1B" tabindex="0">single nested slot</div> + </slot> + <slot> + <div id="1C" tabindex="0">single nested slot</div> + </slot> + <slot> + <slot> + <div id="2A" tabindex="0">double nested slot</div> + <div id="2B" tabindex="0">double nested slot</div> + </slot> + </slot> + <slot> + <div id="3A" tabindex="0">single nested slot</div> + <slot> + <div id="3B" tabindex="0">double nested slot</div> + <slot> + <div id="3C" tabindex="0">Triple nested slot</div> + <div id="3D" tabindex="0">Triple nested slot</div> + </slot> + <div id="3E" tabindex="0">double nested slot</div> + </slot> + <div id="3F" tabindex="0">single nested slot</div> + </slot> +</div> +<div id="b2" tabindex="0">outside</div> + +<script> +'use strict'; + +promise_test(async () => { + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'b1', + '1A', + '1B', + '1C', + '2A', + '2B', + '3A', + '3B', + '3C', + '3D', + '3E', + '3F', + 'b2', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should cover assigned nodes of slot, especially for nested slots in slot scope.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html new file mode 100644 index 0000000000..0d129ae771 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> [s1] -> [s2] -> j2 ->[x-bar]]<br> + x-bar's shadow tree: [k1 -> [s3]]<br> + slot #s1: [i1 -> i2]<br> + slot #s2: [i3]<br> + slot #s3: [[s4]]<br> + slot #s4: [i4 -> i5]<br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> i1 -> i2 -> i3 -> j2 -> x-bar -> k1 -> i4 -> i5]</b><br> +</p> + +<input id="i0" tabindex=0 value="i0"> +<div id="x-foo"> + <input id="i2" slot="s1" tabindex=2 value="i2"> + <input id="i1" slot="s1" tabindex=1 value="i1"> + <input id="i3" slot="s2" tabindex=3 value="i3"> + <input id="i4" slot="s4" tabindex=4 value="i4"> + <input id="i5" slot="s4" tabindex=5 value="i5"> + <template data-mode="open"> + <div id="x-bar" tabindex=5> + <slot id="s4" name="s4" slot="s3"></slot> + <template data-mode="open"> + <slot id="s3" name="s3" tabindex=2></slot> + <input id="k1" tabindex=1 value="k1"> + </template> + </div> + <input id="j1" tabindex=1 value="j1"> + <slot id="s2" name="s2" tabindex=3></slot> + <slot id="s1" name="s1" tabindex=2></slot> + <input id="j2" tabindex=4 value="j2"> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'i1', + 'i2', + 'i3', + 'x-foo/j2', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'i4', + 'i5' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-radio.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-radio.html new file mode 100644 index 0000000000..9304cc2ce1 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-radio.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<template id="custom-radio"> + <span aria-role="radio" tabindex="0">🔘</span> + <slot></slot> +</template> + +<div tabindex="0" id="start">OUT</div> +<form> + <custom-radio name="radio" id="A">A</x-radio> + <custom-radio name="radio" id="B">B</x-radio> +</form> +<form> + <custom-radio name="radio" id="C">C</x-radio> + <custom-radio name="radio" id="D">D</x-radio> +</form> +<div tabindex="0" id="end">OUT</div> + +<script> +const template = document.querySelector('#custom-radio'); + +class CustomRadio extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open', delegatesFocus: true }).appendChild( + template.content.cloneNode(true), + ); + } +} +customElements.define('custom-radio', CustomRadio); + +async function assert_web_component_focus_navigation_forward(elements) { + let start = document.getElementById(elements[0]); + start.focus(); + for (let i = 1; i < elements.length; i++) { + await navigateFocusForward(); + assert_equals(document.activeElement.id, elements[i]); + } +} + +async function assert_web_component_focus_navigation_backward(elements) { + let end = document.getElementById(elements[elements.length - 1]); + end.focus(); + for (let i = elements.length - 2; i >= 0; i--) { + await navigateFocusBackward(); + assert_equals(document.activeElement.id, elements[i]); + } +} + +promise_test(async () => { + let elements = [ + 'start', + 'A', + 'B', + 'C', + 'D', + 'end' + ]; + + await assert_web_component_focus_navigation_forward(elements); + await assert_web_component_focus_navigation_backward(elements); +}, 'Focus for web component input type elements should be bound by <form> inside shadow DOM'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html new file mode 100644 index 0000000000..64942a109e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html @@ -0,0 +1,326 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<p>This tests TAB focus navigation with delegatesFocus flag on shadow hosts</p> +<pre id="console"></pre> +<div id="sandbox"></div> +<script> + +function prepareDOMTree(parent, mode, tabindex, delegatesFocus) { + parent.innerHTML = ` + <div id="testform"> + <input id="input-before"> + <div id="host-div"> + <input id="inner-input"> + </div> + <input id="input-after"> + </div> + `; + const hostDiv = document.getElementById('host-div'); + const shadowRoot = hostDiv.attachShadow({ mode, delegatesFocus }); + + const inputBefore = document.getElementById('input-before'); + const innerInput = document.getElementById('inner-input'); + const inputAfter = document.getElementById('input-after'); + shadowRoot.appendChild(innerInput); + + if (tabindex !== null) + hostDiv.tabIndex = tabindex; + + return { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + }; + +} + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', null, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, no tabindex and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', null, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, no tabindex and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 0, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + 'input-before', + 'host-div', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=0 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 0, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + 'input-before', + // 'host-div', // should skip host when delegatesFocus=true + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=0 and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', -1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=-1 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', -1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + // 'host-div/inner-input', // The whole shadow tree should be skipped + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=-1 and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + 'host-div', + 'host-div/inner-input', + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=1 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + // 'host-div', // should skip host when delegatesFocus=true + 'host-div/inner-input', + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Testing tab navigation order with mode open, tabindex=1 and delegatesFocus=true.'); + + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', null, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, no tabindex and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', null, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, no tabindex and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 0, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + [inputBefore], + [hostDiv], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=0 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 0, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + [inputBefore], + // [hostDiv], // should skip host when delegatesFocus=true + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=0 and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', -1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=-1 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', -1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + // [innerInput, shadowRoot], // The whole shadow tree should be skipped + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=-1 and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + [hostDiv], + [innerInput, shadowRoot], + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=1 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + // [hostDiv], // should skip host when delegatesFocus=true + [innerInput, shadowRoot], + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_bidirectional_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=1 and delegatesFocus=true.'); + + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html new file mode 100644 index 0000000000..9e593eb100 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j5 -> [x-bar] -> j6]<br> + x-bar's shadow tree: [k1 -> k0 -> [s2]]<br> + slot #s2: [j1 -> j2 -> j3 -> j4 -> [s1] -> j0]<br><br> + slot #s1: [i1 -> i2]<br> + <b>v1 ideal nav forward: [i0 -> j5 -> xbar -> k1 -> k0 -> j1 -> j2 -> j3 -> j4 -> i1 -> i2 -> j0 -> j6]</b><br> +</p> + +<input id="i0" tabindex=0 value="i0"> +<div id="x-foo"> + <input id="i2" slot="s1" tabindex=2 value="i2"> + <input id="i1" slot="s1" tabindex=1 value="i1"> + <template data-mode="open"> + <div id="x-bar" tabindex=4> + <input id="j1" slot="s2" tabindex=1 value="j1"> + <slot id="s1" name="s1" slot="s2"></slot> + <input id="j0" slot="s2" tabindex=0 value="j0"> + <input id="j3" slot="s2" tabindex=2 value="j3"> + <div id="j4" slot="s2" tabindex=3> + <input id="j2" tabindex=1 value="j2"> + </div> + <template data-mode="open"> + <input id="k0" tabindex=0 value="k0"> + <slot id="s2" name="s2"></slot> + <input id="k1" tabindex=1 value="k1"> + </template> + </div> + <input id="j6" tabindex=4 value="j6"> + <input id="j5" tabindex=3 value="j5"> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + let sr = xfoo.shadowRoot; + let xbar = sr.querySelector('div'); + convertTemplatesToShadowRootsWithin(xbar); + + let elements = [ + 'i0', + 'x-foo/j5', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'x-foo/x-bar/k0', + 'x-foo/j1', + 'x-foo/j2', + 'x-foo/j3', + 'x-foo/j4', + 'i1', + 'i2', + 'x-foo/j0', + 'x-foo/j6' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus controller should treat slots as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html new file mode 100644 index 0000000000..85d784bc04 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209217"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<div id=div1 tabindex=0>one</div> +<span> + <template shadowrootmode=open> + <slot name=myslot></slot> + </template> + <slot slot=myslot> + <div id=div2 tabindex=0>two</div> + <div id=div3 tabindex=0>three</div> + <div id=div4 tabindex=0>four</div> + </slot> +</span> +<div id=div5 tabindex=0>five</div> + +<script> + +promise_test(async () => { + div1.focus(); + assert_equals(document.activeElement, div1); + + await navigateFocusForward(); + assert_equals(document.activeElement, div2); + await navigateFocusForward(); + assert_equals(document.activeElement, div3); + await navigateFocusForward(); + assert_equals(document.activeElement, div4); + await navigateFocusForward(); + assert_equals(document.activeElement, div5); + await navigateFocusBackward(); + assert_equals(document.activeElement, div4); + await navigateFocusBackward(); + assert_equals(document.activeElement, div3); + await navigateFocusBackward(); + assert_equals(document.activeElement, div2); + await navigateFocusBackward(); + assert_equals(document.activeElement, div1); +}, `Verifies that focus order goes in flat tree order with buttons inside nested slots which have a mixture of assigned and unassigned states.`); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html new file mode 100644 index 0000000000..ddb72bdfc3 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1014868"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<div> + <template shadowrootmode=open> + <slot></slot> + </template> + <slot> + <input id=input1> + </slot> + <slot> + <input id=input2> + </slot> +</div> + +<script> +promise_test(async () => { + input2.focus(); + assert_equals(document.activeElement, input2); + + await navigateFocusBackward(); + assert_equals(document.activeElement, input1); +}, `Verifies that focusing backwards from an input inside a slot which has no shadow root goes to the previous focusable element in light DOM.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html new file mode 100644 index 0000000000..e19ea44288 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id=i0 value=i0> +<div id=outer> + <template data-mode=open> + <div id=inner> + <template data-mode=open> + <div> + <slot name=inside></slot> + </div> + </template> + <slot name=inside slot=inside> + <input id=i1 value=i1> + </slot> + </div> + </template> +</div> +<input id=i2 value=i2> + +<script> +promise_test(async () => { + convertTemplatesToShadowRootsWithin(document.getElementById('outer')); + + const elements = [ + 'i2', + 'outer/i1', + 'i0' + ]; + await assert_focus_navigation_backward(elements); +}, `Verifies that focusing backwards from a button inside a slot which has no assigned nodes goes to the previous focusable element.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html new file mode 100644 index 0000000000..135569d94c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209217"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<slot> + <input id=input1> +</slot> +<slot> + <input id=input2> +</slot> + +<script> +promise_test(async () => { + input1.focus(); + assert_equals(document.activeElement, input1); + + await navigateFocusForward(); + assert_equals(document.activeElement, input2); +}, `Verifies that focusing forwards from an input inside a slot which has no shadow root goes to the next focusable element in light DOM.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html new file mode 100644 index 0000000000..ed3fac9a6c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j5 -> [x-bar] -> j6]<br> + x-bar's shadow tree: [k1 -> k0 -> [s2]]<br> + slot #s2: [j1 -> j2 -> j3 -> j4 -> [s1] -> j0]<br><br> + slot #s1: [i1 -> i2]<br> + <b>v1 ideal nav forward: [i0 -> j5 -> xbar -> k1 -> k0 -> j6]</b><br> +</p> + + <input id='i0' tabindex=0 value='i0'> + <div id='x-foo'> + <input id='i2' slot='s1' tabindex=2 value='i2'> + <input id='i1' slot='s1' tabindex=1 value='i1'> + <template data-mode='open'> + <div id='x-bar' tabindex=4> + <input id='j1' slot='s2' tabindex=1 value='j1'> + <slot id='s1' name='s1' slot='s2'></slot> + <input id='j0' slot='s2' tabindex=0 value='j0'> + <input id='j3' slot='s2' tabindex=2 value='j3'> + <div id='j4' slot='s2' tabindex=3> + <input id='j2' tabindex=1 value='j2'> + </div> + <template data-mode='open'> + <input id='k0' tabindex=0 value='k0'> + <slot id='s2' name='s2' tabindex=-1></slot> + <input id='k1' tabindex=1 value='k1'> + </template> + </div> + <div id='to-be-ignored-host' tabindex=-1> + <template data-mode='open'> + <input id='ignored-input-in-shadow-host1' tabindex=1 value='ignored'> + <input id='ignored-input-in-shadow-host2' tabindex=2 value='ignored'> + </template> + </div> + <input id='j6' tabindex=4 value='j6'> + <input id='j5' tabindex=3 value='j5'> + </template> + </div> +</div> + +<script> +'use strict'; + +let xfoo = document.getElementById('x-foo'); +convertTemplatesToShadowRootsWithin(xfoo); +let sr = xfoo.shadowRoot; + +promise_test(async () => { + let elements = [ + 'i0', + 'x-foo/j5', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'x-foo/x-bar/k0', + 'x-foo/j6' + ]; + + await assert_focus_navigation_bidirectional(elements); +}, 'Focus controller should treat slots as a focus scope.'); + +promise_test(async () => { + let ignoredHost = sr.getElementById('to-be-ignored-host'); + let ignoredInput1 = ignoredHost.shadowRoot.querySelector('input'); + let ignoredInput2 = ignoredInput1.nextElementSibling; + + let elements = [ + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host1', + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host2', + 'x-foo/j6' + ]; + + await assert_focus_navigation_forward(elements); + + let elementsBackward = [ + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host2', + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host1', + 'x-foo/x-bar/k0' + ]; + await assert_focus_navigation_backward(elementsBackward); +}, 'This is a regression test: After focusing negative tabindex-ed elements, focus moves in tree order.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js new file mode 100644 index 0000000000..f593267cc3 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js @@ -0,0 +1,164 @@ +'use strict'; + +function navigateFocusForward() { + // TAB = '\ue004' + return test_driver.send_keys(document.body, "\ue004"); +} + +async function navigateFocusBackward() { + return new test_driver.Actions() + .keyDown('\uE050') + .keyDown('\uE004') + .keyUp('\uE004') + .keyUp('\uE050') + .send(); +} + +// If shadow root is open, can find element using element path +// If shadow root is open, can find the shadowRoot from the element + +function innermostActiveElement(element) { + element = element || document.activeElement; + if (isIFrameElement(element)) { + if (element.contentDocument.activeElement) + return innermostActiveElement(element.contentDocument.activeElement); + return element; + } + if (isShadowHost(element)) { + let shadowRoot = element.shadowRoot; + if (shadowRoot) { + if (shadowRoot.activeElement) + return innermostActiveElement(shadowRoot.activeElement); + } + } + return element; +} + +function isInnermostActiveElement(path) { + const element = getNodeInComposedTree(path); + if (!element) + return false; + return element === innermostActiveElement(); +} + +async function shouldNavigateFocus(fromElement, direction) { + if (!fromElement) + return false; + + fromElement.focus(); + if (fromElement !== innermostActiveElement()) + return false; + + if (direction == 'forward') + await navigateFocusForward(); + else + await navigateFocusBackward(); + + return true; +} + +async function assert_focus_navigation_element(fromPath, toPath, direction) { + const fromElement = getNodeInComposedTree(fromPath); + const result = await shouldNavigateFocus(fromElement, direction); + assert_true(result, 'Failed to focus ' + fromPath); + + const message = + `Focus should move ${direction} from ${fromPath} to ${toPath}`; + const toElement = getNodeInComposedTree(toPath); + assert_equals(innermostActiveElement(), toElement, message); +} + +async function assert_focus_navigation_elements(elements, direction) { + assert_true( + elements.length >= 2, + 'length of elements should be greater than or equal to 2.'); + for (var i = 0; i + 1 < elements.length; ++i) + await assert_focus_navigation_element(elements[i], elements[i + 1], direction); + +} + +async function assert_focus_navigation_forward(elements) { + return assert_focus_navigation_elements(elements, 'forward'); +} + +async function assert_focus_navigation_backward(elements) { + return assert_focus_navigation_elements(elements, 'backward'); +} + +async function assert_focus_navigation_bidirectional(elements) { + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +} + + +// If shadow root is closed, need to pass shadowRoot and element to find +// innermost active element + +function isShadowHostOfRoot(shadowRoot, node) { + return shadowRoot && shadowRoot.host.isEqualNode(node); +} + +function innermostActiveElementWithShadowRoot(shadowRoot, element) { + element = element || document.activeElement; + if (isIFrameElement(element)) { + if (element.contentDocument.activeElement) + return innermostActiveElementWithShadowRoot(shadowRoot, element.contentDocument.activeElement); + return element; + } + if (isShadowHostOfRoot(shadowRoot, element)) { + if (shadowRoot.activeElement) + return innermostActiveElementWithShadowRoot(shadowRoot, shadowRoot.activeElement); + } + return element; +} + +async function shouldNavigateFocusWithShadowRoot(from, direction) { + const [fromElement, shadowRoot] = from; + if (!fromElement) + return false; + + fromElement.focus(); + if (fromElement !== innermostActiveElementWithShadowRoot(shadowRoot)) + return false; + + if (direction == 'forward') + await navigateFocusForward(); + else + await navigateFocusBackward(); + + return true; +} + +async function assert_focus_navigation_element_with_shadow_root(from, to, direction) { + const result = await shouldNavigateFocusWithShadowRoot(from, direction); + const [fromElement] = from; + const [toElement, toShadowRoot] = to; + assert_true(result, 'Failed to focus ' + fromElement.id); + const message = + `Focus should move ${direction} from ${fromElement.id} to ${toElement.id}`; + assert_equals(innermostActiveElementWithShadowRoot(toShadowRoot), toElement, message); +} + +async function assert_focus_navigation_elements_with_shadow_root(elements, direction) { + assert_true( + elements.length >= 2, + 'length of elements should be greater than or equal to 2.'); + for (var i = 0; i + 1 < elements.length; ++i) + await assert_focus_navigation_element_with_shadow_root(elements[i], elements[i + 1], direction); +} + +async function assert_focus_navigation_forward_with_shadow_root(elements) { + return assert_focus_navigation_elements_with_shadow_root(elements, 'forward'); +} + +async function assert_focus_navigation_backward_with_shadow_root(elements) { + return assert_focus_navigation_elements_with_shadow_root(elements, 'backward'); +} + +async function assert_focus_navigation_bidirectional_with_shadow_root(elements) { + await assert_focus_navigation_forward_with_shadow_root(elements); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements); +} + diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js new file mode 100644 index 0000000000..2789f05850 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js @@ -0,0 +1,165 @@ +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 convertTemplatesToShadowRootsWithin(node) { + var nodes = node.querySelectorAll('template'); + for (var i = 0; i < nodes.length; ++i) { + var template = nodes[i]; + var mode = template.getAttribute('data-mode'); + var delegatesFocus = template.hasAttribute('data-delegatesFocus'); + var parent = template.parentNode; + parent.removeChild(template); + var shadowRoot; + if (!mode) { + shadowRoot = parent.attachShadow({ mode: 'open' }); + } else { + shadowRoot = + parent.attachShadow({ 'mode': mode, 'delegatesFocus': delegatesFocus }); + } + var expose = template.getAttribute('data-expose-as'); + if (expose) + window[expose] = shadowRoot; + if (template.id) + shadowRoot.id = template.id; + var fragments = document.importNode(template.content, true); + shadowRoot.appendChild(fragments); + + convertTemplatesToShadowRootsWithin(shadowRoot); + } +} + +function isShadowHost(node) { + return node && node.nodeType == Node.ELEMENT_NODE && node.shadowRoot; +} + +function isIFrameElement(element) { + return element && element.nodeName == 'IFRAME'; +} + +// Returns node from shadow/iframe tree "path". +function getNodeInComposedTree(path) { + var ids = path.split('/'); + var node = document.getElementById(ids[0]); + for (var i = 1; node != null && i < ids.length; ++i) { + if (isIFrameElement(node)) + node = node.contentDocument.getElementById(ids[i]); + else if (isShadowHost(node)) + node = node.shadowRoot.getElementById(ids[i]); + else + return null; + } + return node; +} + +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; +} + +function dispatchEventWithLog(nodes, target, event) { + 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(event.type, (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); + }) + ]); + }); + } + } + target.dispatchEvent(event); + 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][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'); + } +} + +function assert_background_color(path, color) { + assert_equals( + window.getComputedStyle(getNodeInComposedTree(path)).backgroundColor, + color, 'backgroundColor for ' + path + ' should be ' + color); +} diff --git a/testing/web-platform/tests/shadow-dom/focus-within-shadow.html b/testing/web-platform/tests/shadow-dom/focus-within-shadow.html new file mode 100644 index 0000000000..c0a1e624fb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-within-shadow.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://crbug.com/1300584"> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<div id=host> + <template shadowrootmode=open> + <button><slot></slot></button> + </template> + <span>Content</span> +</div> + +<script> + const host = document.getElementById('host'); + test(() => { + const shadowButton = host.shadowRoot.querySelector('button'); + shadowButton.focus(); + assert_equals(document.activeElement,host,'nodes within shadow DOM aren\'t exposed'); + host.textContent = 'New Content'; + assert_equals(document.activeElement,host,'Clearing light DOM shouldn\'t clear focus'); + },'Don\'t clear focus within shadow root if light DOM children are cleared'); +</script> + diff --git a/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html b/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html new file mode 100644 index 0000000000..20456b057e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: DocumentOrShadowRoot.activeElement</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<body> +<script> +function createChildAndFocus(focusParent) { + const focused = document.createElement("div"); + focused.tabIndex = 0; + focusParent.appendChild(focused); + focused.focus(); + return focused; +} + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + document.body.appendChild(host); + + const focused = createChildAndFocus(shadowRoot); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, focused); +}, "activeElement on document & shadow root when focused element is in the shadow tree"); + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + document.body.appendChild(host); + + const focused = createChildAndFocus(document.body); + assert_equals(document.activeElement, focused); + assert_equals(shadowRoot.activeElement, null); +}, "activeElement on document & shadow root when focused element is in the document"); + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + shadowRoot.appendChild(document.createElement("slot")); + document.body.appendChild(host); + + // Child of |host|, will be slotted to the slot in |shadowRoot|. + const focused = createChildAndFocus(host); + assert_equals(document.activeElement, focused); + assert_equals(shadowRoot.activeElement, null); +}, "activeElement on document & shadow root when focused element is slotted"); + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + document.body.appendChild(host); + const neighborHost = document.createElement("div"); + const neighborShadowRoot = neighborHost.attachShadow({ mode: "open" }); + document.body.appendChild(neighborHost); + + const focused = createChildAndFocus(shadowRoot); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, focused); + assert_equals(neighborShadowRoot.activeElement, null); +}, "activeElement on a neighboring host when focused element is in another shadow tree"); + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + document.body.appendChild(host); + const nestedHost = document.createElement("div"); + const nestedShadowRoot = nestedHost.attachShadow({ mode: "open" }); + shadowRoot.appendChild(nestedHost); + + const focused = createChildAndFocus(nestedShadowRoot); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, nestedHost); + assert_equals(nestedShadowRoot.activeElement, focused); +}, "activeElement when focused element is in a nested shadow tree"); + +test(() => { + const host = document.createElement("div"); + const shadowRoot = host.attachShadow({ mode: "open" }); + document.body.appendChild(host); + const nestedHost = document.createElement("div"); + const nestedShadowRoot = nestedHost.attachShadow({ mode: "open" }); + shadowRoot.appendChild(nestedHost); + + const focused = createChildAndFocus(shadowRoot); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, focused); + assert_equals(nestedShadowRoot.activeElement, null); +}, "activeElement when focused element is in a parent shadow tree"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html new file mode 100644 index 0000000000..4b7fe4e50e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ShadowRoot's delegatesFocus attribute</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="host1"></div> +<div id="host2"></div> +<div id="host3"></div> +<script> +test(t => { + const host = document.getElementById("host1"); + const shadowRoot = host.attachShadow({mode: "closed"}); + assert_equals(shadowRoot.delegatesFocus, false); +}, "default delegatesFocus value"); + +test(t => { + const host = document.getElementById("host2"); + const shadowRoot = host.attachShadow({mode: "closed", delegatesFocus: false}); + assert_equals(shadowRoot.delegatesFocus, false); +}, "delegatesFocus set to false in init dict"); + +test(t => { + const host = document.getElementById("host3"); + const shadowRoot = host.attachShadow({mode: "closed", delegatesFocus: true}); + assert_equals(shadowRoot.delegatesFocus, true); +}, "delegatesFocus set to true in init dict"); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html new file mode 100644 index 0000000000..289b554372 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: Blur on shadow host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<div id="host"> + <input id="slotted"> +</div> + +<script> +const host = document.getElementById("host"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); + +shadowRoot.innerHTML = "<input><slot>" + +test(function() { + host.focus(); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, shadowRoot.querySelector("input")); + host.blur(); + assert_equals(document.activeElement, document.body); + assert_equals(shadowRoot.activeElement, null); +}, "Calling blur() on shadow host with delegatesFocus should remove the focus."); + +test(function() { + const slotted = document.getElementById("slotted"); + slotted.focus(); + assert_equals(document.activeElement, slotted); + assert_equals(shadowRoot.activeElement, null) + host.blur(); + assert_equals(document.activeElement, slotted); + assert_equals(shadowRoot.activeElement, null) +}, "Calling blur() on shadow host with delegatesFocus when the focus is on a slotted element should not remove the focus."); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html new file mode 100644 index 0000000000..0578c15582 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: click on shadow host with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> + +<body> +<div id="host"> + <div id="slotted">slotted</div> +</div> +<div id="outside">outside</div> +</body> + +<script> +const host = document.getElementById("host"); +const slotted = document.getElementById("slotted"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +const aboveSlot = document.createElement("div"); +aboveSlot.innerText = "aboveSlot"; +const slot = document.createElement("slot"); +shadowRoot.appendChild(aboveSlot); +shadowRoot.appendChild(slot); + +const elementsInFlatTreeOrder = [host, aboveSlot, slot, slotted, outside]; + +// Final structure: +// <div #host> (delegatesFocus=true) +// #shadowRoot +// <div #aboveSlot> +// <slot #slot> +// (slotted) <div #slotted> +// <div #outside> + +function setAllTabIndex(value) { + setTabIndex(elementsInFlatTreeOrder, value); +} + +function removeAllTabIndex() { + removeTabIndex(elementsInFlatTreeOrder); +} + +function resetTabIndexAndFocus() { + removeAllTabIndex(); + resetFocus(document); + resetFocus(shadowRoot); +} + +test(() => { + resetTabIndexAndFocus(); + setAllTabIndex(0); + host.click(); + assert_equals(shadowRoot.activeElement, null); + assert_equals(document.activeElement, document.body); +}, "call click() on host with delegatesFocus, all tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setAllTabIndex(0); + slotted.click(); + assert_equals(shadowRoot.activeElement, null); + assert_equals(document.activeElement, document.body); +}, "call click() on slotted element in delegatesFocus shadow tree, all tabindex=0"); + +function createNestedHosts(outerDelegatesFocus, innerDelegatesFocus) { + // Structure: + // <div> outerHost + // <input> outerLightChild + // #shadowRoot outerShadow delegatesFocus=true + // <div> spacer + // <span> innerHost + // #shadowRoot innerShadow delegatesFocus=true/false + // <input> innerShadowChild + // <input> outerShadowChild + const outerHost = document.createElement('div'); + const outerLightChild = document.createElement('input'); + outerHost.appendChild(outerLightChild); + const innerHost = document.createElement('span'); + const outerShadow = outerHost.attachShadow({mode: 'closed', delegatesFocus:outerDelegatesFocus}); + + const spacer = document.createElement("div"); + spacer.style = "height: 1000px;"; + outerShadow.appendChild(spacer); + + outerShadow.appendChild(innerHost); + const outerShadowChild = document.createElement('input'); + outerShadow.appendChild(outerShadowChild); + + const innerShadow = innerHost.attachShadow({mode: 'closed', delegatesFocus:innerDelegatesFocus}); + const innerShadowChild = document.createElement('input'); + innerShadow.appendChild(innerShadowChild); + + document.body.insertBefore(outerHost, document.body.firstChild); + return {outerHost: outerHost, + outerLightChild: outerLightChild, + outerShadow: outerShadow, + outerShadowChild: outerShadowChild, + innerHost: innerHost, + innerShadow: innerShadow, + innerShadowChild: innerShadowChild}; +} + +promise_test(async function() { + const dom = createNestedHosts(true, true); + await test_driver.click(dom.outerHost); + assert_equals(document.activeElement, dom.outerHost); + assert_equals(dom.outerShadow.activeElement, dom.innerHost); + assert_equals(dom.innerShadow.activeElement, dom.innerShadowChild); +}, "click on the host with delegatesFocus with another host with delegatesFocus and a focusable child"); + +promise_test(async function() { + const dom = createNestedHosts(true, false); + await test_driver.click(dom.outerHost); + assert_equals(document.activeElement, dom.outerHost); + assert_equals(dom.outerShadow.activeElement, dom.outerShadowChild); + assert_equals(dom.innerShadow.activeElement, null); +}, "click on the host with delegatesFocus with another host with no delegatesFocus and a focusable child"); + +promise_test(async function() { + const dom = createNestedHosts(false, true); + await test_driver.click(dom.outerHost); + assert_equals(document.activeElement, document.body); + assert_equals(dom.outerShadow.activeElement, null); + assert_equals(dom.innerShadow.activeElement, null); +}, "click on the host with no delegatesFocus with another host with delegatesFocus and a focusable child"); + +promise_test(async function() { + const dom = createNestedHosts(false, false); + await test_driver.click(dom.outerHost); + assert_equals(document.activeElement, document.body); + assert_equals(dom.outerShadow.activeElement, null); + assert_equals(dom.innerShadow.activeElement, null); +}, "click on the host with no delegatesFocus with another host with no delegatesFocus and a focusable child"); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html new file mode 100644 index 0000000000..4051db128a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: click on shadow host with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> + +<body> +<div id="host"> + <div id="slotted">slotted</div> +</div> +<div id="outside">outside</div> +</body> + +<script> +const host = document.getElementById("host"); +const slotted = document.getElementById("slotted"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +const aboveSlot = document.createElement("div"); +aboveSlot.innerText = "aboveSlot"; +const slot = document.createElement("slot"); +// Add an unfocusable spacer, because test_driver.click will click on the +// center point of #host, and we don't want the click to land on #aboveSlot +// or #slot. +const spacer = document.createElement("div"); +spacer.style = "height: 1000px;"; +shadowRoot.appendChild(spacer); +shadowRoot.appendChild(aboveSlot); +shadowRoot.appendChild(slot); + +const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside]; + +// Final structure: +// <div #host> (delegatesFocus=true) +// #shadowRoot +// <div #spacer> +// <div #aboveSlot> +// <slot #slot> +// (slotted) <div #slotted> +// <div #outside> + +function setAllTabIndex(value) { + setTabIndex(elementsInFlatTreeOrder, value); +} + +function removeAllTabIndex() { + removeTabIndex(elementsInFlatTreeOrder); +} + +function resetTabIndexAndFocus() { + removeAllTabIndex(); + resetFocus(document); + resetFocus(shadowRoot); +} + +promise_test(async () => { + resetTabIndexAndFocus(); + setTabIndex([aboveSlot], 2); + setTabIndex([slot, slotted], 1); + await test_driver.click(host); + assert_equals(shadowRoot.activeElement, aboveSlot); + assert_equals(document.activeElement, host); +}, "click on host with delegatesFocus, #aboveSlot tabindex = 2, #slot and #slotted tabindex = 1"); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html new file mode 100644 index 0000000000..5f7914f2a4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: click on shadow host with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> + +<body> +<div id="host"> + <div id="slotted">slotted</div> +</div> +<div id="outside">outside</div> +</body> + +<script> +const host = document.getElementById("host"); +const slotted = document.getElementById("slotted"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +const aboveSlot = document.createElement("div"); +aboveSlot.innerText = "aboveSlot"; +const slot = document.createElement("slot"); +// Add an unfocusable spacer, because test_driver.click will click on the +// center point of #host, and we don't want the click to land on #aboveSlot +// or #slot. +const spacer = document.createElement("div"); +spacer.style = "height: 1000px;"; +shadowRoot.appendChild(spacer); +shadowRoot.appendChild(aboveSlot); +shadowRoot.appendChild(slot); + +const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside]; + +// Final structure: +// <div #host> (delegatesFocus=true) +// #shadowRoot +// <div #spacer> +// <div #aboveSlot> +// <slot #slot> +// (slotted) <div #slotted> +// <div #outside> + +function setAllTabIndex(value) { + setTabIndex(elementsInFlatTreeOrder, value); +} + +function removeAllTabIndex() { + removeTabIndex(elementsInFlatTreeOrder); +} + +function resetTabIndexAndFocus() { + removeAllTabIndex(); + resetFocus(document); + resetFocus(shadowRoot); +} + +promise_test(async () => { + resetTabIndexAndFocus(); + setAllTabIndex(0); + removeTabIndex([spacer]); + await test_driver.click(host); + assert_equals(shadowRoot.activeElement, aboveSlot); + assert_equals(document.activeElement, host); +}, "click on host with delegatesFocus, all tabindex=0 except spacer"); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html b/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html new file mode 100644 index 0000000000..f159c22164 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> +<script> +test(() => { + const host = document.createElement('div'); + document.body.appendChild(host); + + const shadowRoot = host.attachShadow({mode: 'open', delegatesFocus: true}); + + const shadowInput = document.createElement('input'); + shadowRoot.appendChild(shadowInput); + + host.focus(); + assert_equals(document.activeElement, host, 'The shadow host should be focused.'); + + host.setAttribute('tabindex', '0'); + assert_equals(document.activeElement, host, 'The shadow host should remain focused after changing tabindex.'); +}, 'Setting tabindex on the shadow host of a focused element with delegatesFocus should not change focus.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html new file mode 100644 index 0000000000..75a50b84c6 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html @@ -0,0 +1,338 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Sean Feng" href="mailto:sefeng@mozilla.com"> +<meta name="assert" content="Elements with autofocus should have high precedence over other elements for delegates focus"> +<link rel="help" href="https://github.com/whatwg/html/pull/6990"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +</head> + +<body> + <script> + function createShadowDOMTree() { + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <div #firstOuterDiv> + // <div #innertHost> + // #shadowRoot + // <div #firstInnerDiv> + // <div #secondInnerDiv> + // <div #secondOuterDiv> + const host = document.createElement("div"); + host.setAttribute("id", "host"); + const outerRoot = host.attachShadow({mode: "open", delegatesFocus: true}); + + const firstOuterDiv = document.createElement("div"); + + const innerHost = document.createElement("div"); + const innerRoot = innerHost.attachShadow({mode: "open"}); + const firstInnerDiv = document.createElement("div"); + const secondInnerDiv = document.createElement("div"); + innerRoot.appendChild(firstInnerDiv); + innerRoot.appendChild(secondInnerDiv); + + const secondOuterDiv = document.createElement("div"); + + outerRoot.appendChild(firstOuterDiv); + outerRoot.appendChild(innerHost); + outerRoot.appendChild(secondOuterDiv); + document.body.appendChild(host); + return [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] + } + + function resetShadowDOMTree() { + const host = document.getElementById("host"); + if (host) { + host.remove(); + } + return createShadowDOMTree(); + } + + function resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ) { + firstOuterDiv.removeAttribute("tabindex"); + firstOuterDiv.removeAttribute("autofocus"); + + secondOuterDiv.removeAttribute("tabindex"); + secondOuterDiv.removeAttribute("autofocus"); + + firstInnerDiv.removeAttribute("tabindex"); + firstInnerDiv.removeAttribute("autofocus"); + + secondInnerDiv.removeAttribute("tabindex"); + secondInnerDiv.removeAttribute("autofocus"); + + resetFocus(document); + resetFocus(outerRoot); + resetFocus(innerRoot); + } + + function setAllTabIndexTo( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + tabIndex + ) { + firstOuterDiv.tabIndex = tabIndex; + secondOuterDiv.tabIndex = tabIndex; + firstInnerDiv.tabIndex = tabIndex; + secondInnerDiv.tabIndex = tabIndex; + } + + test(function() { + const [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] = resetShadowDOMTree(); + + resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ); + + setAllTabIndexTo( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + 0 + ); + + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <div tabIndex=0 #firstOuterDiv> + // <div #innertHost> + // #shadowRoot + // <div tabIndex=0 #firstInnerDiv> + // <div tabIndex=0 #secondInnerDiv> + // <div tabIndex=0 autofocus #secondOuterDiv> + secondOuterDiv.autofocus = true; + secondOuterDiv.setAttribute("autofocus", true); + + host.focus(); + + assert_equals(document.activeElement, host); + assert_equals(outerRoot.activeElement, secondOuterDiv); + }, "The second input should be focused since it has autofocus"); + + test(function() { + const [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] = resetShadowDOMTree(); + + resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ); + + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <div #firstOuterDiv> + // <div #innertHost> + // #shadowRoot + // <div tabIndex=0 #firstInnerDiv> + // <div tabIndex=0 autofocus #secondInnerDiv> + // <div #secondOuterDiv> + firstInnerDiv.tabIndex = 0; + secondInnerDiv.tabIndex = 0; + secondInnerDiv.setAttribute("autofocus", true); + + host.focus(); + assert_equals(document.activeElement, document.body); + assert_equals(outerRoot.activeElement, null); + }, "Focus should not be delegated to the autofocus element because the inner host doesn't have delegates focus"); + + test(function() { + const [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] = resetShadowDOMTree(); + + resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ); + + const newInnerHost = document.createElement("div"); + const newInnerRoot = newInnerHost.attachShadow({mode: "open", delegatesFocus: true}); + const newFirstInnerDiv = document.createElement("div"); + const newSecondInnerDiv = document.createElement("div"); + newFirstInnerDiv.setAttribute("tabIndex", 0); + newSecondInnerDiv.setAttribute("tabIndex", 0); + + newSecondInnerDiv.setAttribute("autofocus", true); + newInnerRoot.appendChild(newFirstInnerDiv); + newInnerRoot.appendChild(newSecondInnerDiv); + + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <div #firstOuterDiv> + // <div #innertHost> (delegatesFocus = true) + // #shadowRoot + // <div tabIndex=0 #newFirstInnerDiv> + // <div tabIndex=0 autofocus #newSecondInnerDiv> + // <div #secondOuterDiv> + outerRoot.replaceChild(newInnerHost, innerHost); + + host.focus(); + + assert_equals(document.activeElement, host); + assert_equals(outerRoot.activeElement, newInnerHost); + assert_equals(newInnerRoot.activeElement, newSecondInnerDiv); + }, "Focus should be delegated to the autofocus element when the inner host has delegates focus"); + + test(function() { + const [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] = resetShadowDOMTree(); + + resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ); + + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <slot> + // (slotted) <div autofocus tabIndex=0 #slottedAutofocus></div> + // <div tabIndex=0 #firstOuterDiv> + // <div #innertHost> + // #shadowRoot + // <div tabIndex=0 #firstInnerDiv> + // <div tabIndex=0 autofocus #secondInnerDiv> + // <div #secondOuterDiv> + + const slottedAutofocus = document.createElement("div"); + slottedAutofocus.tabIndex = 0; + slottedAutofocus.setAttribute("autofocus", true); + host.appendChild(slottedAutofocus); + + const slot = document.createElement("slot"); + outerRoot.insertBefore(slot, firstOuterDiv); + + firstOuterDiv.tabIndex = 0; + + host.focus(); + assert_equals(document.activeElement, host); + assert_equals(outerRoot.activeElement, firstOuterDiv); + }, "Focus should not be delegated to the slotted elements"); + + test(function() { + const [ + host, + outerRoot, + firstOuterDiv, + secondOuterDiv, + innerHost, + innerRoot, + firstInnerDiv, + secondInnerDiv + ] = resetShadowDOMTree(); + + resetTabIndexAndFocus( + firstOuterDiv, + secondOuterDiv, + firstInnerDiv, + secondInnerDiv, + outerRoot, + innerRoot + ); + + // <div #host> (delegatesFocus = true) + // #shadowRoot + // <div #firstOuterDiv> + // <div tabIndex=0 #firstNestedDiv> + // <div tabIndex=0 #secondNestedDiv> + // <div tabIndex=0 autofocus #thirdNestedDiv> + // <div #innertHost> + // #shadowRoot + // <div #firstInnerDiv> + // <div #secondInnerDiv> + // <div autofocus tabIndex=0 #secondOuterDiv> + + secondInnerDiv.tabIndex = 0; + secondInnerDiv.setAttribute("autofocus", true); + + const firstNestedDiv = document.createElement("div"); + const secondNestedDiv = document.createElement("div"); + const thirdNestedDiv = document.createElement("div"); + + firstNestedDiv.tabIndex = 0; + secondNestedDiv.tabIndex = 0; + thirdNestedDiv.tabIndex = 0; + thirdNestedDiv.setAttribute("autofocus", true); + + firstOuterDiv.appendChild(firstNestedDiv); + firstNestedDiv.appendChild(secondNestedDiv); + secondNestedDiv.appendChild(thirdNestedDiv); + + host.focus(); + + assert_equals(document.activeElement, host); + assert_equals(outerRoot.activeElement, thirdNestedDiv); + }, "Focus should be delegated to the nested div which has autofocus based on the tree order"); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html new file mode 100644 index 0000000000..7a318a0200 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: click on shadow host with delegatesFocus</title> +<link rel="author" href="mailto:dizhangg@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1327136"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<body> + <div id="host"></div> +</body> + +<script> +const host = document.getElementById("host"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +// Add an unfocusable spacer, because test_driver.click will click on the +// center point of #host, and we don't want the click to land on focusableDiv +const spacer = document.createElement("div"); +spacer.style = "height: 1000px;"; +shadowRoot.appendChild(spacer); + +const focusableDiv = document.createElement("div"); +focusableDiv.tabIndex = 0; +shadowRoot.appendChild(focusableDiv); + +promise_test(async () => { + assert_equals(document.activeElement, document.body); + // Mouse click + await test_driver.click(host); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, focusableDiv); + assert_true(host.matches(':focus')); + assert_true(focusableDiv.matches(':focus')); + assert_false(focusableDiv.matches(':focus-visible')); +}, ":focus should be applied to the host and the child node when the focus is moved by mouse click"); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html new file mode 100644 index 0000000000..d2724f17d5 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus() on shadow host within an iframe with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<body> +<script> +test(() => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.addEventListener("load", () => { + iframe.contentDocument.body.innerHTML = + `<div id="host"></div>`; + const host = iframe.contentDocument.getElementById("host"); + const firstInput = iframe.contentDocument.createElement("input"); + const secondInput = iframe.contentDocument.createElement("input"); + + host.attachShadow({mode: 'open', delegatesFocus: true}); + host.shadowRoot.appendChild(firstInput); + host.shadowRoot.appendChild(secondInput); + + iframe.contentDocument.body.appendChild(host); + + secondInput.focus(); + assert_equals(host.shadowRoot.activeElement, secondInput); + + // host is a shadow-including-ancestor of secondInput, so + // the focus should remain secondInput. + host.focus(); + assert_equals(host.shadowRoot.activeElement, secondInput); + }); +}, "focus delegate step should not be run when the focus target is a shadow-including inclusive ancestor of the current focus."); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html new file mode 100644 index 0000000000..99667029ad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html @@ -0,0 +1,312 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus() on shadow host with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> + +<body> +<div id="host"> + <div id="slottedToSecondSlot" slot="secondSlot">slottedToSecondSlot</div> + <div id="slottedToFirstSlot" slot="firstSlot">slottedToFirstSlot</div> +</div> +<div id="outside">outside</div> +</body> + +<script> +const host = document.getElementById("host"); +const slottedToSecondSlot = document.getElementById("slottedToSecondSlot"); +const slottedToFirstSlot = document.getElementById("slottedToFirstSlot"); +const outside = document.getElementById("outside"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +const aboveSlots = document.createElement("div"); +aboveSlots.innerText = "aboveSlots"; +const firstSlot = document.createElement("slot"); +firstSlot.name = "firstSlot"; +const secondSlot = document.createElement("slot"); +secondSlot.name = "secondSlot"; +const belowSlots = document.createElement("div"); +belowSlots.innerText = "belowSlots"; +shadowRoot.appendChild(aboveSlots); +shadowRoot.appendChild(firstSlot); +shadowRoot.appendChild(secondSlot); +shadowRoot.appendChild(belowSlots); + +const elementsInFlatTreeOrder = [host, aboveSlots, firstSlot, + slottedToFirstSlot, secondSlot, slottedToSecondSlot, belowSlots, outside]; + +// Final structure: +// <div #host> (delegatesFocus=true) +// #shadowRoot +// <div #aboveSlots> +// <slot #firstSlot> +// (slotted) <div #slottedToFirstSlot> +// <slot #secondSlot> +// (slotted) <div #slottedToSecondSlot> +// <div #belowSlots> +// <div #outside> + + +function setAllTabIndex(value) { + setTabIndex(elementsInFlatTreeOrder, value); +} + +function removeAllTabIndex() { + removeTabIndex(elementsInFlatTreeOrder); +} + +function resetTabIndexAndFocus() { + removeAllTabIndex(); + resetFocus(document); + resetFocus(shadowRoot); +} + +test(() => { + resetTabIndexAndFocus(); + setAllTabIndex(0); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> tabindex=0 + // <slot #firstSlot> tabindex=0 + // (slotted) <div #slottedToFirstSlot> tabindex=0 + // <slot #secondSlot> tabindex=0 + // (slotted) <div #slottedToSecondSlot> tabindex=0 + // <div #belowSlots> tabindex=0 + // <div #outside> tabindex=0 + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus, all tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setAllTabIndex(0); + setTabIndex([host], -1); + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex =-1, all other tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setTabIndex([aboveSlots, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0); + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & no tabindex, all other tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setAllTabIndex(-1); + setTabIndex([host], 0); + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex = 0, all other tabindex=-1"); + +test(() => { + resetTabIndexAndFocus(); + removeAllTabIndex(); + // No focusable element under #host in the flat tree. + host.focus(); + assert_equals(shadowRoot.activeElement, null); + assert_equals(document.activeElement, document.body); +}, "focus() on host with delegatesFocus, all without tabindex"); + +test(() => { + resetTabIndexAndFocus(); + // First focusable = #aboveSlots + setAllTabIndex(-1); + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus, all tabindex=-1"); + +test(() => { + resetTabIndexAndFocus(); + removeAllTabIndex(); + setTabIndex([host, belowSlots], 0); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> + // <slot #firstSlot> + // (slotted) <div #slottedToFirstSlot> + // <slot #secondSlot> + // (slotted) <div #slottedToSecondSlot> + // <div #belowSlots> tabindex=0 + // <div #outside> + // First focusable = #belowSlots + host.focus(); + assert_equals(shadowRoot.activeElement, belowSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + removeAllTabIndex(); + setTabIndex([host, outside], 0); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> + // <slot #firstSlot> + // (slotted) <div #slottedToFirstSlot> + // <slot #secondSlot> + // (slotted) <div #slottedToSecondSlot> + // <div #belowSlots> + // <div #outside> tabindex=0 + // No focusable element under #host in the flat tree. + host.focus(); + assert_equals(shadowRoot.activeElement, null); + assert_equals(document.activeElement, document.body); +}, "focus() on host with delegatesFocus & tabindex=0, #outside with tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setTabIndex([host, aboveSlots, belowSlots], 0); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> tabindex=0 + // <slot #firstSlot> + // (slotted) <div #slottedToFirstSlot> + // <slot #secondSlot> + // (slotted) <div #slottedToSecondSlot> + // <div #belowSlots> tabindex=0 + // <div #outside> + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots and #belowSlots with tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setTabIndex([host, aboveSlots], 0); + setTabIndex([belowSlots], 1); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> tabindex=0 + // <slot #firstSlot> + // (slotted) <div #slottedToFirstSlot> + // <slot #secondSlot> + // (slotted) <div #slottedToSecondSlot> + // <div #belowSlots> tabindex=1 + // <div #outside> + // First focusable = #aboveSlots + host.focus(); + assert_equals(shadowRoot.activeElement, aboveSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots with tabindex=0 and #belowSlots with tabindex=1"); + +test(() => { + resetTabIndexAndFocus(); + setTabIndex([host, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0); + // Structure: + // <div #host> (delegatesFocus=true) tabindex=0 + // #shadowRoot + // <div #aboveSlots> + // <slot #firstSlot> + // (slotted) <div #slottedToFirstSlot> tabindex=0 + // <slot #secondSlot> + // (slotted) <div #slottedToSecondSlot> tabindex=0 + // <div #belowSlots> tabindex=0 + // <div #outside> + // First focusable = #slottedToFirstSlot + host.focus(); + assert_equals(shadowRoot.activeElement, belowSlots); + assert_equals(document.activeElement, host); +}, "focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots with tabindex=0"); + +test(() => { + resetTabIndexAndFocus(); + setTabIndex([aboveSlots, belowSlots], 0); + belowSlots.focus(); + host.focus(); + assert_equals(shadowRoot.activeElement, belowSlots); +}, "focus() on host with delegatesFocus and already-focused non-first shadow descendant"); + +function createNestedHosts(innerDelegatesFocus) { + // Structure: + // <div> outerHost + // <input> outerLightChild + // #shadowRoot outerShadow delegatesFocus=true + // <span> innerHost + // #shadowRoot inneShadow delegatesFocus=true/false + // <input> innerShadowChild + // <input> outerShadowChild + const outerHost = document.createElement('div'); + const outerLightChild = document.createElement('input'); + outerHost.appendChild(outerLightChild); + const innerHost = document.createElement('span'); + const outerShadow = outerHost.attachShadow({mode: 'closed', delegatesFocus:true}); + outerShadow.appendChild(innerHost); + const outerShadowChild = document.createElement('input'); + outerShadow.appendChild(outerShadowChild); + + const innerShadow = innerHost.attachShadow({mode: 'closed', delegatesFocus:innerDelegatesFocus}); + const innerShadowChild = document.createElement('input'); + innerShadow.appendChild(innerShadowChild); + + document.body.insertBefore(outerHost, document.body.firstChild); + return {outerHost: outerHost, + outerLightChild: outerLightChild, + outerShadow: outerShadow, + outerShadowChild: outerShadowChild, + innerHost: innerHost, + innerShadow: innerShadow, + innerShadowChild: innerShadowChild}; +} + +test(() => { + const dom = createNestedHosts(false); + dom.outerHost.focus(); + assert_equals(document.activeElement, dom.outerHost); + assert_equals(dom.outerShadow.activeElement, dom.outerShadowChild); +}, 'focus() on host with delegatesFocus with another host with no delegatesFocus and a focusable child'); + +test(() => { + const dom = createNestedHosts(true); + dom.outerHost.focus(); + assert_equals(document.activeElement, dom.outerHost); + assert_equals(dom.outerShadow.activeElement, dom.innerHost); + assert_equals(dom.innerShadow.activeElement, dom.innerShadowChild); +}, 'focus() on host with delegatesFocus with another host with delegatesFocus and a focusable child'); + +test(() => { + // Structure: + // <div> host + // #shadowRoot root delegatesFocus=true + // <slot> + // (slotted) <div> + // <input> + // <input #firstFocusable> + const host = document.createElement("div"); + const slotted = document.createElement("div"); + slotted.appendChild(document.createElement("input")); + host.appendChild(slotted); + + const root = host.attachShadow({mode: "open", delegatesFocus: true}); + + const firstFocusable = document.createElement("input"); + root.innerHTML = "<slot>"; + root.appendChild(firstFocusable); + + document.body.appendChild(host); + + host.focus(); + assert_equals(document.activeElement, host); + assert_equals(root.activeElement, firstFocusable); +}, "focus() on host with delegatesFocus and slotted focusable children"); +</script> + diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html new file mode 100644 index 0000000000..8caea8ccda --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-dom.js"></script> + +<template id='ShadowTemplate'> + <ul> + <li tabindex='0' id='one'>One</li> + <li tabindex='0' id='two'>Two</li> + <li id='three'>Three</li> + </ul> +</template> +<template id='NoFocusableShadowTemplate'> + <ul> + <li id='one'>One</li> + <li id='two'>Two</li> + <li id='three'>Three</li> + </ul> +</template> +<body> +<input id='input0'> +<x-shadow id='xshadow0'></x-shadow> +<x-shadow id='xshadow1' tabindex='0'></x-shadow> +<x-shadow id='xshadow2' tabindex='0' delegatesFocus></x-shadow> +<x-shadow-nofocus id='xshadow3'></x-shadow-nofocus> +<x-shadow-nofocus id='xshadow4' tabindex='0'></x-shadow-nofocus> +<x-shadow-nofocus id='xshadow5' tabindex='0' delegatesFocus></x-shadow-nofocus> +</body> +<script> +'use strict'; + +function registerShadow(templateId, tagName) { + const template = document.getElementById(templateId); + + customElements.define(tagName, class extends HTMLElement { + connectedCallback() { + const delegatesFocus = this.hasAttribute('delegatesFocus'); + this.attachShadow({mode: 'open', delegatesFocus: delegatesFocus}) + .appendChild(document.importNode(template.content, true)); + } + }); +} + +registerShadow('ShadowTemplate', 'x-shadow'); +registerShadow('NoFocusableShadowTemplate', 'x-shadow-nofocus'); + +test(() => { + xshadow0.focus(); + assert_equals(document.activeElement.tagName, 'BODY'); + assert_equals(xshadow0.shadowRoot.activeElement, null); +}, 'xshadow0 is not focusable without tabindex.'); + +test(() => { + xshadow1.focus(); + assert_equals(document.activeElement.id, 'xshadow1'); + assert_equals(xshadow1.shadowRoot.activeElement, null); +}, 'xshadow1 becomes focusable with tabindex.'); + +test(() => { + xshadow2.focus(); + assert_equals(document.activeElement.id, 'xshadow2'); + assert_equals(xshadow2.shadowRoot.activeElement.id, 'one'); +}, 'on focus(), focusable xshadow2 with delegatesFocus=true delegates focus into its inner element.'); + +test(() => { + xshadow2.shadowRoot.querySelector('#two').focus(); + assert_equals(document.activeElement.id, 'xshadow2'); + assert_equals(xshadow2.shadowRoot.activeElement.id, 'two'); +}, 'if an element within shadow is focused, focusing on shadow host should not slide focus to its inner element.'); + +test(() => { + xshadow2.focus(); + assert_equals(document.activeElement.id, 'xshadow2'); + assert_equals(xshadow2.shadowRoot.activeElement.id, 'two'); +}, 'xshadow2.focus() shouldn\'t move focus to #one when its inner element is already focused.'); + +test(() => { + // Focus outside shadow DOMs. + input0.focus(); + + // within shadow root. This is different from mouse click behavior. + xshadow1.shadowRoot.querySelector('#three').focus(); + assert_equals(document.activeElement.id, 'input0'); + xshadow2.shadowRoot.querySelector('#three').focus(); + assert_equals(document.activeElement.id, 'input0'); +}, 'focus() inside shadow DOM should not focus its shadow host, nor focusable siblings.'); + +test(() => { + xshadow3.focus(); + assert_equals(document.activeElement.id, 'input0'); +}, 'If any element including shadow host is not focusable, focus doesn\'t change.'); + +test(() => { + xshadow4.focus(); + assert_equals(document.activeElement.id, 'xshadow4'); + xshadow5.focus(); + assert_equals(document.activeElement.id, 'xshadow4'); +}, 'If no element is focusable within a delegatesFocus shadow root, the host can\'t get focus regardless of host\'s tabIndex.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html new file mode 100644 index 0000000000..34f8c01294 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content=":focus should match a shadow host which contains the focused element"> +<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus"> +<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<input id="defaultFocus" autofocus> +<div id="log"></div> +<div id="container"></div> +<script> + +let focusedDefault = false; +function didFocusDefault() { } +function handleFocus() { + if (!focusedDefault) { + // Use step_timeout here to avoid nested focusing steps. + // For example, <input id="defaultFocus" autofocus> could run scripts + // while it's autofocusing which may run the tests, so that the + // focus() usage in the tests becomes nested focusing steps. + step_timeout(function() { + testInMode('open', false); + testInMode('open', true); + testInMode('closed', false); + testInMode('closed', true); + }, 0); + } + focusedDefault = true; + didFocusDefault(); +} +defaultFocus.addEventListener('focus', handleFocus); + +function prepare(test) +{ + test.add_cleanup(() => { + defaultFocus.focus(); + container.textContent = ''; + }); + return new Promise((resolve) => { + if (focusedDefault) + resolve(); + else + didFocusDefault = resolve; + }); +} + +function testInMode(mode, delegatesFocus) { + const modeString = `{mode:${mode}, delegatesFocus:${delegatesFocus}}`; + promise_test(async function () { + await prepare(this); + const host = document.createElement('div'); + container.appendChild(host); + const shadowRoot = host.attachShadow({mode, delegatesFocus}); + shadowRoot.innerHTML = '<input>'; + assert_equals(document.activeElement, defaultFocus); + assert_equals(shadowRoot.activeElement, null); + assert_false(host.matches(':focus')); + }, `:focus must not match a shadow host with ${modeString} shadow root that does not contain the focused element`); + + promise_test(async function () { + await prepare(this); + const host = document.createElement('div'); + document.body.appendChild(host); + const shadowRoot = host.attachShadow({mode, delegatesFocus}); + shadowRoot.innerHTML = '<input>'; + shadowRoot.firstChild.focus(); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, shadowRoot.firstChild); + assert_true(host.matches(':focus')); + }, `:focus must match a shadow host with ${modeString} shadow root that contains the focused element`); + + promise_test(async function () { + await prepare(this); + const host = document.createElement('div'); + container.appendChild(host); + const shadowRoot = host.attachShadow({mode, delegatesFocus}); + shadowRoot.innerHTML = '<slot>'; + host.innerHTML = '<input>'; + host.firstChild.focus(); + assert_equals(document.activeElement, host.firstChild); + assert_equals(shadowRoot.activeElement, null); + assert_false(host.matches(':focus')); + }, `:focus must not match a shadow host with ${modeString} shadow root contains the focused element assigned to a slot`); + + promise_test(async function() { + await prepare(this); + const host1 = document.body.appendChild(document.createElement('div')); + const shadowRoot1 = host1.attachShadow({mode, delegatesFocus}); + const host2 = shadowRoot1.appendChild(document.createElement('div')); + const shadowRoot2 = host2.attachShadow({mode, delegatesFocus}); + shadowRoot2.innerHTML = '<input>'; + shadowRoot2.firstChild.focus(); + assert_equals(document.activeElement, host1); + assert_equals(shadowRoot1.activeElement, host2); + assert_equals(shadowRoot2.activeElement, shadowRoot2.firstChild); + assert_true(host1.matches(':focus')); + assert_true(host2.matches(':focus')); + }, `:focus must match all shadow hosts which are ancestors of a foccused element; ${modeString}`); + + promise_test(async function() { + await prepare(this); + const host = document.body.appendChild(document.createElement('div')); + const shadowRoot = host.attachShadow({mode, delegatesFocus}); + shadowRoot.innerHTML = '<input>'; + const input = shadowRoot.firstChild; + const outer = document.body.appendChild(document.createElement('div')); + + assert_false(host.matches(':focus')); + input.focus(); + assert_true(host.matches(':focus')); + outer.appendChild(input); + assert_false(host.matches(':focus')); + }, `:focus behavior on tree structure changes; ${modeString}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html new file mode 100644 index 0000000000..ba900d6a6d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content=":focus should match a shadow host which contains the focused element"> +<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus"> +<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432"> +<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht"> +</head> +<body> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="host"></div> +<style> +#host { background: red; width: 100px; height: 100px; } +#host:focus { background: green; } +</style> +<script> + +const shadowRoot = host.attachShadow({mode: 'closed'}); +shadowRoot.innerHTML = '<div tabindex="0" style="outline: none;"></div>'; +shadowRoot.firstChild.focus(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html new file mode 100644 index 0000000000..f119008ee3 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content=":focus should match a shadow host which contains the focused element"> +<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus"> +<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432"> +<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css"> +</head> +<body> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="host"><span>FAIL</span></div> +<style> +#host { background: green; width: 100px; height: 100px; } +#host span { background: red; font: 10px/1 Ahem; } +#host:focus span { background: green; color: green; } +</style> +<script> + +const shadowRoot = host.attachShadow({mode: 'closed'}); +shadowRoot.innerHTML = '<div tabindex="0" style="outline: none;"><slot></slot></div>'; +shadowRoot.firstChild.focus(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html new file mode 100644 index 0000000000..e72d1b39d4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content=":focus should not match a shadow host if the focused element is a slotted content"> +<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus"> +<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432"> +<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht"> +</head> +<body> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="host"><div id="target" tabindex="0"></div></div> +<style> +#host { background: green; width: 100px; height: 100px; } +#host:focus #target { background: red; width: 100px; height: 100px; } +#target { outline: none; } +</style> +<script> + +const shadowRoot = host.attachShadow({mode: 'closed'}); +shadowRoot.innerHTML = '<slot></slot>'; +target.focus(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html new file mode 100644 index 0000000000..bbc1346a46 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8" /> + <title>CSS Test (Selectors): :focus behavior with shadow hosts & delegatesFocus </title> + <link rel="author" title="Rakina Zata Amni" href="rakina@chromium.org" /> + <link rel="help" href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-focus" /> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/shadow-utils.js"></script> +</head> + +<body> +<input> + +<script> +function createFocusableDiv() { + const div = document.createElement("div"); + div.innerText = "foo"; + div.tabIndex = 0; + return div; +} + +function createShadowHost(delegatesFocus, container) { + const host = document.createElement("div"); + host.attachShadow({ mode: "open", delegatesFocus: delegatesFocus }); + container.appendChild(host); + return host; +} + +const delegatesFocusValues = [true, false]; + +for (const delegatesFocus of delegatesFocusValues) { + test(() => { + resetFocus(); + const host = createShadowHost(delegatesFocus, document.body); + const shadowChild = createFocusableDiv(); + host.shadowRoot.appendChild(shadowChild); + + shadowChild.focus(); + assert_true(shadowChild.matches(":focus"), "element in shadow tree matches :focus"); + assert_true(host.matches(":focus"), "host matches :focus"); + }, `:focus applies to host with delegatesFocus=${delegatesFocus} when the shadow root's descendant has focus`); + + test(() => { + resetFocus(); + const host = createShadowHost(delegatesFocus, document.body); + const slotted = createFocusableDiv(); + host.shadowRoot.appendChild(document.createElement("slot")); + host.appendChild(slotted); + + slotted.focus(); + assert_true(slotted.matches(":focus"), "slotted element matches :focus"); + assert_false(host.matches(":focus"), "host matches :focus"); + }, `:focus does not apply to host with delegatesFocus=${delegatesFocus} when slotted element has focus`); + + for (const nestedDelegatesFocus of delegatesFocusValues) { + test(() => { + resetFocus(); + const host = createShadowHost(delegatesFocus, document.body); + const nestedHost = createShadowHost(nestedDelegatesFocus, host.shadowRoot); + const nestedShadowChild = createFocusableDiv(); + nestedHost.shadowRoot.appendChild(nestedShadowChild); + nestedShadowChild.focus(); + assert_true(nestedShadowChild.matches(":focus"), "element in nested shadow tree matches :focus"); + assert_true(nestedHost.matches(":focus"), "host of nested shadow tree matches focus"); + assert_true(host.matches(":focus"), "topmost host matches focus"); + }, `:focus applies to host with delegatesFocus=${delegatesFocus} when an element in a nested shadow tree with delegatesFocus=${nestedDelegatesFocus} is focused`); + + test(() => { + resetFocus(); + const host = createShadowHost(delegatesFocus, document.body); + const nestedHost = createShadowHost(nestedDelegatesFocus, host.shadowRoot); + const nestedShadowChild = createFocusableDiv(); + nestedHost.shadowRoot.appendChild(nestedShadowChild); + // All nested shadow hosts should has :focus applied + nestedShadowChild.focus(); + + const elementOutsideOfShadowDOM = document.querySelector("input"); + // Move the focus to an element which is outside of the nested + // shadow DOM trees + elementOutsideOfShadowDOM.focus(); + + assert_false(nestedShadowChild.matches(":focus"), "element in nested shadow tree doesn't matche :focus"); + assert_false(nestedHost.matches(":focus"), "host of nested shadow tree doesn't match focus"); + assert_false(host.matches(":focus"), "topmost host matches focus"); + assert_true(elementOutsideOfShadowDOM.matches(":focus"), "The element outside of shadow dom matches :focus"); + }, `:focus should be removed from hosts with delegatesFocus=${delegatesFocus} when none of the elements in a nested shadow tree with delegatesFocus=${nestedDelegatesFocus} is focused`); + } +} +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html b/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html new file mode 100644 index 0000000000..40f1b01f66 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +#host:focus { display: none; } +</style> +<div id='sandbox'></div> +<script> +'use strict'; + +// Check if shadow host with display:none CSS rule for :focus works. crbug.com/482830 + +var host; +var root; +var input; + +function setupShadowDOM(delegatesFocus) { + sandbox.innerHTML = ''; + host = sandbox.appendChild(document.createElement('div')); + host.id = 'host'; + + root = host.attachShadow({mode: 'open', delegatesFocus: delegatesFocus}); + input = document.createElement('input'); + root.appendChild(input); + + host.tabIndex = 0; +} + +promise_test(() => { + setupShadowDOM(false); + return new Promise( + function(resolve) { + host.focus(); + assert_equals(window.getComputedStyle(host).display, 'none'); + assert_equals(document.activeElement, host); + assert_equals(root.activeElement, null); + + function onBlur() { + assert_equals(window.getComputedStyle(host).display, 'block'); + assert_equals(document.activeElement, document.body); + assert_equals(root.activeElement, null); + host.removeEventListener('blur', onBlur); + resolve(); + } + host.addEventListener('blur', onBlur); + }); +}, 'when shadow host itself is focused, it should match display:none, lose focus then becomes display:block again.'); + +promise_test(() => { + setupShadowDOM(true); + return new Promise( + function(resolve) { + input.focus(); + assert_equals(window.getComputedStyle(host).display, 'none'); + assert_equals(document.activeElement, host); + assert_equals(root.activeElement, input); + + function onBlur() { + assert_equals(window.getComputedStyle(host).display, 'block'); + assert_equals(document.activeElement, document.body); + assert_equals(root.activeElement, null); + input.removeEventListener('blur', onBlur); + resolve(); + } + input.addEventListener('blur', onBlur); + }); +}, 'when shadow host with delegatesFocus=true has focused element inside the shadow, it should also match display:none, then lose focus and become display:block again.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html new file mode 100644 index 0000000000..0dffc0157f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: Use tab to navigate the focus to an element inside shadow host with delegatesFocus</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> + +<body> + <div id="host"></div> +</body> + +<script> +const host = document.getElementById("host"); + +const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true }); +const input = document.createElement("input"); +shadowRoot.appendChild(input); + +promise_test(async function() { + assert_equals(document.activeElement, document.body); + // Press <tab> + await navigateFocusForward(); + assert_equals(document.activeElement, host); + assert_equals(shadowRoot.activeElement, input); + assert_true(host.matches(':focus')); + assert_true(input.matches(':focus')); + assert_true(input.matches(':focus-visible')); +}, ":focus should be applied to the host and :focus-visible should be applied to the child node when the focus is moved by <tab>"); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html new file mode 100644 index 0000000000..356b0bb329 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=-1 in shadow tree</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot (delegatesFocus) +// <div #aboveSlot tabindex=-1> +// <slot #slotAbove tabindex=-1> +// (slotted) <div #slottedAbove tabindex=-1> +// <slot #slotBelow tabindex=-1> +// (slotted) <div #slottedBelow tabindex=-1> +// <div #belowSlot tabindex=-1> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true); + setTabIndex(elementsInFlatTreeOrder, -1); + setTabIndex([aboveHost, host, belowHost], 0); + resetFocus(); + // Focus should only land on #aboveHost and #belowHost (all others are non-sequentially focusable). + return assertFocusOrder([aboveHost, belowHost]); +}, "Order when all tabindex=-1 is and delegatesFocus = true"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html new file mode 100644 index 0000000000..ab25ea829b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and negative tabindex in shadow scope</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot +// <div #aboveSlot tabindex=-1> +// <slot #slotAbove tabindex=-1> +// (slotted) <div #slottedAbove tabindex=-1> +// <slot #slotBelow tabindex=-1> +// (slotted) <div #slottedBelow tabindex=-1> +// <div #belowSlot tabindex=-1> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, -1); + setTabIndex([aboveHost, host, belowHost], 0); + resetFocus(); + return assertFocusOrder([aboveHost, host, belowHost]); +}, "Order when all elements in shadow tree has negative tabindex"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html new file mode 100644 index 0000000000..3c9e70867c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0 except for one of the slot</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=1> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + slotBelow.tabIndex = 1; + resetFocus(); + // Focus should move first according to flat tree order to #aboveHost and #host, then into #host's focus scope. + // It will then move to #slottedBelow because #slotBelow has tabindex=1 (though we actually won't focus on the slot), + // and then back to #host's focus scope again, finally getting out to the document focus scope. + return assertFocusOrder([aboveHost, host, slottedBelow, aboveSlot, slottedAbove, belowSlot, belowHost]); +}, "Order when all tabindex=0, except for one slot that has tabindex=1"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html new file mode 100644 index 0000000000..67899cff4a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and tabindex in shadow tree varies</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot (delegatesFocus) +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=1> +// (slotted) <div #slottedAbove tabindex=3> +// <slot #slotBelow tabindex=2> +// (slotted) <div #slottedBelow tabindex=1> +// <div #belowSlot tabindex=-1> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true); + setTabIndex(elementsInFlatTreeOrder, -1); + setTabIndex([aboveHost, host, aboveSlot, belowHost], 0); + setTabIndex([slotAbove, slottedBelow], 1); + setTabIndex([slotBelow], 2); + setTabIndex([slottedAbove], 3); + resetFocus(); + return assertFocusOrder([aboveHost, slottedAbove, slottedBelow, aboveSlot, belowHost]); +}, "Order when tabindex varies and delegatesFocus = true"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html new file mode 100644 index 0000000000..1755aaf442 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom with varying tabindex values</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +promise_test(async () => { + function createButtonInShadowDOM(host) { + const root = host.attachShadow({mode: "open"}); + root.innerHTML = "<input>"; + document.body.appendChild(host); + return root; + } + const host1 = document.createElement("div"); + const root1 = createButtonInShadowDOM(host1); + + const forwarder = document.createElement("div"); + forwarder.setAttribute("tabindex", 0); + document.body.appendChild(forwarder); + + const host2 = document.createElement("div"); + host2.setAttribute("tabindex", -1); + const root2 = createButtonInShadowDOM(host2); + + const host3 = document.createElement("div"); + const root3 = createButtonInShadowDOM(host3); + + root1.querySelector("input").focus(); + + let forwarderFocused = false; + forwarder.addEventListener("focus", () => { + forwarderFocused = true; + root2.querySelector("input").focus(); + }); + + // Structure: + // <div #host1></div> + // #ShadowRoot + // <button>Button</button> + // <div #forwarder tabindex=0></div> + // <div #host2 tabindex=-1></div> + // #ShadowRoot + // <button>Button</button> + // <div #host3></div> + // #ShadowRoot + // <button>Button</button> + assert_equals(document.activeElement, host1); + assert_equals(root1.activeElement, root1.querySelector("input")); + + await navigateFocusForward(); + assert_true(forwarderFocused); + assert_equals(document.activeElement, host2); + assert_equals(root2.activeElement, root2.querySelector("input")); + + // In buggy Firefox build, the following focus navigation will + // move the focus back to #host1's button. + await navigateFocusForward(); + assert_equals(document.activeElement, host3); + assert_equals(root3.activeElement, root3.querySelector("input")); +}, "Order with different tabindex on host") +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html new file mode 100644 index 0000000000..e0570395ec --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with nested shadow dom with varying tabindex values</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +promise_test(async () => { + function createButtonInShadowDOM(host, parent) { + const root = host.attachShadow({mode: "open"}); + root.innerHTML = "<input>"; + parent.appendChild(host); + return root; + } + + const host1 = document.createElement("div"); + const root1 = createButtonInShadowDOM(host1, document.body); + + const forwarder = document.createElement("div"); + forwarder.setAttribute("tabindex", 0); + document.body.appendChild(forwarder); + + const host2 = document.createElement("div"); + host2.setAttribute("tabindex", -1); + const root2 = host2.attachShadow({mode: "open"}); + document.body.appendChild(host2); + + const host2_1 = document.createElement("div"); + const root2_1 = createButtonInShadowDOM(host2_1, root2); + + const host2_2 = document.createElement("div"); + host2_2.setAttribute("tabindex", -1); + const root2_2 = createButtonInShadowDOM(host2_2, root2); + + const host2_3 = document.createElement("div"); + const root2_3 = createButtonInShadowDOM(host2_3, root2); + + root1.querySelector("input").focus(); + + let forwarderFocused = false; + forwarder.addEventListener("focus", () => { + forwarderFocused = true; + root2_2.querySelector("input").focus(); + }); + + // Structure: + // <div #host1></div> + // #ShadowRoot + // <button>Button</button> + // <div #forwarder tabindex=0></div> + // <div #host2 tabindex=-1></div> + // #ShadowRoot + // <div #host2_1></div> + // #ShadowRoot + // <button>Button</button> + // <div #host2_2 tabindex=-1></div> + // #ShadowRoot + // <button>Button</button> + // <div #host2_3></div> + // #ShadowRoot + // <button>Button</button> + assert_equals(document.activeElement, host1); + assert_equals(root1.activeElement, root1.querySelector("input")); + + await navigateFocusForward(); + assert_true(forwarderFocused); + assert_equals(document.activeElement, host2); + assert_equals(root2_2.activeElement, root2_2.querySelector("input")); + + // In buggy Firefox build, the following focus navigation will + // move the focus back to #host1_1's button. + await navigateFocusForward(); + assert_equals(document.activeElement, host2); + assert_equals(root2_3.activeElement, root2_3.querySelector("input")); +}, "Order with different tabindex on host") +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html new file mode 100644 index 0000000000..875e5b6814 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom with varying tabindex values</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=3> +// <div #host tabindex=3> +// #shadowRoot +// <div #aboveSlot tabindex=2> +// <slot #slotAbove tabindex=1> +// (slotted) <div #slottedAbove tabindex=4> +// <slot #slotBelow tabindex=1> +// (slotted) <div #slottedBelow tabindex=4> +// <div #belowSlot tabindex=2> +// <div #belowHost tabindex=3> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex([slotAbove, slotBelow], 1); + setTabIndex([aboveSlot, belowSlot], 2); + setTabIndex([aboveHost, host, belowHost], 3); + setTabIndex([slottedAbove, slottedBelow], 4); + resetFocus(); + return assertFocusOrder([aboveHost, host, slottedAbove, slottedBelow, aboveSlot, belowSlot, belowHost]); +}, "Order with various tabindex values"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html new file mode 100644 index 0000000000..5e6ab3a90f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=0</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot (delegatesFocus) +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true); + setTabIndex(elementsInFlatTreeOrder, 0); + resetFocus(); + // Focus should move in flat tree order since every one of them has tabindex ==0, + // but doesn't include slots and #host. + return assertFocusOrder([aboveHost, aboveSlot, slottedAbove, slottedBelow, belowSlot, belowHost]); +}, "Order when all tabindex=0 is and delegatesFocus = true"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html new file mode 100644 index 0000000000..b491c7d237 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and negative host tabindex</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=-1> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + host.tabIndex = -1; + resetFocus(); + // Focus willl only move within the focus navigation scope of the document (not going to get into #host). + return assertFocusOrder([aboveHost, belowHost]); +}, "Order when all tabindex=0 except for host, which has tabindex=-1"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html new file mode 100644 index 0000000000..9a12d8b8f4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and scrollable/non-focusable host</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host style="overflow: scroll"> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + removeTabIndex([host]); + host.style = "overflow: scroll"; + resetFocus(); + // Focus should move in flat tree order since every one of them has tabindex ==0, + // but doesn't include #slot since it's not rendered and #host since its tabindex is not set + // (but #host is considered as 0 in focus scope navigation, keeping the flat tree order for the shadow root's descendants). + return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow && el !== host))); +}, "Order when all tabindex=0 except scrollable host (tabindex not set)"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html new file mode 100644 index 0000000000..f257261477 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and non-focusable host</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + removeTabIndex([host]); + resetFocus(); + // Focus should move in flat tree order since every one of them has tabindex ==0, + // but doesn't include #slot since it's not rendered and #host since its tabindex is not set + // (but #host is considered as 0 in focus scope navigation, keeping the flat tree order for the shadow root's descendants). + return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow && el !== host))); +}, "Order when all tabindex=0 except host (tabindex not set)"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html new file mode 100644 index 0000000000..1aa5292997 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0 except host (tabindex=1)</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=1> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + host.tabIndex = 1; + resetFocus(); + // Focus should move first to #host because it has tabindex=1, and then to the contents of its scope + // (e.g. the contents of its shadow tree) in flat tree order, and then outside to the document scope + // again (aboveHost & belowHost). + return assertFocusOrder([host, aboveSlot, slottedAbove, slottedBelow, belowSlot, aboveHost, belowHost]); +}, "Order when all tabindex=0 except for host, which has tabindex=1"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html new file mode 100644 index 0000000000..fa8090ca97 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and host is scrollable</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0 style="overflow: scroll"> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + host.style = "overflow: scroll"; + resetFocus(); + // Focus should move in flat tree order since every one of them has tabindex==0, + // but doesn't include slots. + return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow))); +}, "Order when all tabindex=0 and host is scrollable"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html new file mode 100644 index 0000000000..d8b12ed8ac --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/shadow-utils.js"></script> +<body> +<script> +// Structure: +// <div #aboveHost tabindex=0> +// <div #host tabindex=0> +// #shadowRoot +// <div #aboveSlot tabindex=0> +// <slot #slotAbove tabindex=0> +// (slotted) <div #slottedAbove tabindex=0> +// <slot #slotBelow tabindex=0> +// (slotted) <div #slottedBelow tabindex=0> +// <div #belowSlot tabindex=0> +// <div #belowHost tabindex=0> + +promise_test(() => { + let elementsInFlatTreeOrder; + let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = + elementsInFlatTreeOrder = prepareDOM(document.body, false); + setTabIndex(elementsInFlatTreeOrder, 0); + resetFocus(); + // Focus should move in flat tree order since every one of them has tabindex==0, + // but doesn't include slots. + return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow))); +}, "Order when all tabindex=0 is and delegatesFocus = false"); +</script> +</body> diff --git a/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js b/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js new file mode 100644 index 0000000000..8033ce0169 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js @@ -0,0 +1,80 @@ +// Structure: +// <div #aboveHost> +// <div #host> +// #shadowRoot +// <div #aboveSlot> +// <slot #slotAbove> +// (slotted) <div #slottedAbove> +// <slot #slotBelow> +// (slotted) <div #slottedBelow> +// <div #belowSlot> +// <div #belowHost> +function prepareDOM(container, delegatesFocus) { + + const aboveHost = document.createElement("div"); + aboveHost.innerText = "aboveHost"; + const host = document.createElement("div"); + host.id = "host"; + const slottedBelow = document.createElement("div"); + slottedBelow.innerText = "slotted below"; + slottedBelow.slot = "below"; + const slottedAbove = document.createElement("div"); + slottedAbove.innerText = "slotted above"; + slottedAbove.slot = "above"; + + const belowHost = document.createElement("div"); + belowHost.innerText = "belowHost"; + container.appendChild(aboveHost); + container.appendChild(host); + container.appendChild(belowHost); + host.appendChild(slottedBelow); + host.appendChild(slottedAbove); + const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: delegatesFocus}); + const aboveSlot = document.createElement("div"); + aboveSlot.innerText = "aboveSlot"; + + const slotAbove = document.createElement("slot"); + slotAbove.name = "above"; + const slotBelow = document.createElement("slot"); + slotBelow.name = "below"; + + const belowSlot = document.createElement("div"); + belowSlot.innerText = "belowSlot"; + shadowRoot.appendChild(aboveSlot); + shadowRoot.appendChild(slotAbove); + shadowRoot.appendChild(slotBelow); + shadowRoot.appendChild(belowSlot); + + return [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost]; +} + +function setTabIndex(elements, value) { + for (const el of elements) { + el.tabIndex = value; + } +} + +function removeTabIndex(elements) { + for (const el of elements) { + el.removeAttribute("tabindex"); + } +} + +function resetFocus(root = document) { + if (root.activeElement) + root.activeElement.blur(); +} + +function navigateFocusForward() { + // TAB = '\ue004' + return test_driver.send_keys(document.body, "\ue004"); +} + +async function assertFocusOrder(expectedOrder) { + const shadowRoot = document.getElementById("host").shadowRoot; + for (const el of expectedOrder) { + await navigateFocusForward(); + const focused = shadowRoot.activeElement ? shadowRoot.activeElement : document.activeElement; + assert_equals(focused, el); + } +} diff --git a/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html new file mode 100644 index 0000000000..7c92d35394 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<body> + <x-shadow id="withoutFocus"></x-shadow> + <x-shadow id="withFocus"></x-shadow> +</body> + +<script> +'use strict'; + +/** + * build shadow-root with delegates focus, a focusable element, and an (ideally) selectable text + */ +function buildShadowRootWithSelectableText( element, shouldDelegateFocus ) { + element.attachShadow({ mode: 'open', delegatesFocus: shouldDelegateFocus }); + const span = document.createElement('span'); + span.textContent = 'Example Text to Select '; + const button = document.createElement('button'); + button.textContent = 'Button'; + element.shadowRoot.append(span, button); +} + +/** + * command to select text in shadow-root + */ +function selectText(element, start, end) { + getSelection().empty(); + const actions = new test_driver.Actions(); + actions.pointerMove(start, 0, {origin: element}); + actions.pointerDown(); + actions.pointerMove(end, 0, {origin: element}); + actions.pointerUp(); + return actions.send(); +} + +promise_test(async () => { + const xShadow = document.getElementById('withoutFocus'); + buildShadowRootWithSelectableText(xShadow, false); + + // starting selection from the center of the element, and going right. + // The important part here is that we start the selection in the center + // (where mouse-down events may be delegated) + await selectText(xShadow, 0, 50) + const s = getSelection(); + + // because browsers may handle rendering differently, we can get different amounts of + // text selected, even when using the same start-end values. We opt in this case to + // verify just if any text is selected, since all we care about is if some text is + // selected. + assert_greater_than(s.toString().length, 0); +}, 'shadow root has selectable text when focus is not delegated'); + +promise_test(async () => { + const xShadow = document.getElementById('withFocus'); + buildShadowRootWithSelectableText(xShadow, true); + + await selectText(xShadow, 0, 50) + const s = getSelection(); + + assert_greater_than(s.toString().length, 0); +}, 'shadow root has selectable text when focus is delegated'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/form-control-form-attribute.html b/testing/web-platform/tests/shadow-dom/form-control-form-attribute.html new file mode 100644 index 0000000000..2e782b2587 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/form-control-form-attribute.html @@ -0,0 +1,80 @@ +<!doctype html> +<meta charset=utf-8> +<title>Form controls' form attribute</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id="testcontent"> + <form id="form">form</form> + <input id="input" form="form"> +</div> + +<script> + +test(() => { + assert_equals(document.getElementById('input').form, + document.getElementById('form')); +}, "Form control's form attribute should point to the form element."); + +test(() => { + var testcontent = document.getElementById("testcontent"); + var host = document.createElement("div"); + var sr = host.attachShadow({mode: "open"}); + sr.innerHTML = testcontent.innerHTML; + var input = sr.getElementById("input"); + var form = sr.getElementById("form"); + + // Should have null form when shadow DOM isn't connected. + assert_equals(input.form, null); + + testcontent.appendChild(host); + assert_equals(input.form, form); + + host.remove(); + assert_equals(input.form, null); + + testcontent.appendChild(host); + assert_equals(input.form, form); + + input.remove(); + assert_equals(input.form, null); + + sr.appendChild(input); + assert_equals(input.form, form); + + form.id = "foobar"; + assert_equals(input.form, null); + + form.id = "form"; + assert_equals(input.form, form); + + form.remove(); + assert_equals(input.form, null); + + sr.appendChild(form); + assert_equals(input.form, form); + + host.remove(); +}, "Shadow form control's form attribute should work also in shadow DOM."); + +test(() => { + var testcontent = document.getElementById("testcontent"); + var host = document.createElement("div"); + var sr = host.attachShadow({mode: "open"}); + sr.innerHTML = "<form id='form'><input id='input'></form>"; + var input = sr.getElementById("input"); + var form = sr.getElementById("form"); + + assert_equals(input.form, form); + + input.remove(); + assert_equals(input.form, null); + + form.appendChild(input); + assert_equals(input.form, form); + + form.remove(); + assert_equals(input.form, form); + + host.remove(); +}, "Form element as form control's ancestor should work also in shadow DOM."); +</script> diff --git a/testing/web-platform/tests/shadow-dom/getElementById-dynamic-001.html b/testing/web-platform/tests/shadow-dom/getElementById-dynamic-001.html new file mode 100644 index 0000000000..c2acda2a9c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/getElementById-dynamic-001.html @@ -0,0 +1,23 @@ +<!doctype html> +<title>Shadow DOM: ShadowRoot.getElementById in shadow trees keeps working after host is removed from tree</title> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid"> +<link rel="help" href="https://bugzil.la/1475203"> +<link rel="author" name="Emilio Cobos Álvarez" href="emilio@crisal.io"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="host"></div> +<script> +test(function() { + let host = document.getElementById("host"); + host.attachShadow({ mode: "open" }).innerHTML = `<div id="test-id"></div>`; + + let element = host.shadowRoot.getElementById("test-id"); + assert_true(!!element); + + host.remove(); + assert_equals(host.shadowRoot.getElementById("test-id"), element); + + element.remove(); + assert_equals(host.shadowRoot.getElementById("test-id"), null); +}, "ShadowRoot.getElementById keeps working after host has been removed"); +</script> diff --git a/testing/web-platform/tests/shadow-dom/historical.html b/testing/web-platform/tests/shadow-dom/historical.html new file mode 100644 index 0000000000..4fa8be1dbc --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/historical.html @@ -0,0 +1,25 @@ +<!doctype html> +<title>Shadow DOM v0 features should NOT be present</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(() => { + assert_false('createShadowRoot' in Element.prototype) +}, 'element.createShadowRoot should not exist') + +test(() => { + assert_false('getDestinationInsertionPoints' in Element.prototype) +}, 'element.getDestinationInsertionPoints should not exist') + +test(() => { + assert_false('getDestinationInsertionPoints' in Text.prototype) +}, 'text.getDestinationInsertionPoints should not exist') + +test(() => { + assert_false('path' in Event.prototype) +}, 'event.path should not exist') + +test(() => { + assert_false('HTMLContentElement' in window) +}, 'HTMLContentElement should not exist') +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-api-crash.html b/testing/web-platform/tests/shadow-dom/imperative-slot-api-crash.html new file mode 100644 index 0000000000..b029689211 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-api-crash.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#dom-slot-assign"> +<link rel="help" href="https://crbug.com/1201402"> + +<div id="host"> + <div id="child1"></div> +</div> +This test passes if it does not crash. +<script> + const host = document.querySelector("#host"); + const child1 = document.querySelector("#child1"); + const shadow_root = host.attachShadow({ mode: "open", slotAssignment: "manual" }); + var slot1 = document.createElement("slot"); + slot1.id = "first_slot"; + shadow_root.appendChild(slot1); + slot1.assign(child1); + slot1.remove(); + slot1 = document.createElement("slot"); + slot1.id = "second_slot"; + shadow_root.appendChild(slot1); + slot1.assign(child1); // Shouldn't crash here +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-api-slotchange.html b/testing/web-platform/tests/shadow-dom/imperative-slot-api-slotchange.html new file mode 100644 index 0000000000..236aaea18a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-api-slotchange.html @@ -0,0 +1,394 @@ +<!DOCTYPE html> +<title>Shadow DOM: Imperative Slot API slotchange event</title> +<meta name="author" title="Yu Han" href="mailto:yuzhehan@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> +<script src="resources/shadow-dom.js"></script> + +<div id="test_slotchange"> + <div id="host"> + <template id="shadow_root" data-mode="open" data-slot-assignment="manual"> + <slot id="s1"><div id="fb">fallback</div></slot> + <slot id="s2"></slot> + <div> + <slot id="s2.5"></slot> + </div> + <slot id="s3"></slot> + </template> + <div id="c1"></div> + <div id="c2"></div> + </div> + <div id="c4"></div> +</div> + +<script> +function getDataCollection() { + return { + s1EventCount: 0, + s2EventCount: 0, + s3EventCount: 0, + s1ResolveFn: null, + s2ResolveFn: null, + s3ResolveFn: null, + } +} + +function setupShadowDOM(id, test, data) { + let tTree = createTestTree(id); + tTree.s1.addEventListener('slotchange', (event) => { + if (!event.isFakeEvent) { + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, tTree.s1, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + data.s1EventCount++; + } + data.s1ResolveFn(); + }); + tTree.s2.addEventListener('slotchange', (event) => { + if (!event.isFakeEvent) { + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, tTree.s2, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + data.s2EventCount++; + } + data.s2ResolveFn(); + }); + tTree.s3.addEventListener('slotchange', (event) => { + if (!event.isFakeEvent) { + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + // listen to bubbling events. + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + data.s3EventCount++; + } + data.s3ResolveFn(); + }); + return tTree; +} + +function monitorSlots(data) { + const s1Promise = new Promise((resolve, reject) => { + data.s1ResolveFn = resolve; + }); + const s2Promise = new Promise((resolve, reject) => { + data.s2ResolveFn = resolve; + }); + const s3Promise = new Promise((resolve, reject) => { + data.s3ResolveFn = resolve; + }); + return [s1Promise, s2Promise, s3Promise]; +} +</script> + +<script> +// Tests: +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise, s2Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + tTree.s2.assign(tTree.c2); + + assert_equals(data.s1EventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(data.s2EventCount, 0); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 1); + assert_equals(data.s2EventCount, 1); + })); +}, 'slotchange event must not fire synchronously.'); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise, s2Promise] = monitorSlots(data); + + tTree.s1.assign();; + tTree.s2.assign(); + tTree.host.insertBefore(tTree.c4, tTree.c1); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 0); + assert_equals(data.s2EventCount, 0); + })); + + // use fake event to trigger event handler. + let fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + tTree.s1.dispatchEvent(fakeEvent); + tTree.s2.dispatchEvent(fakeEvent); +}, 'slotchange event should not fire when assignments do not change assignedNodes.'); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange,test, data); + let [s1Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1, tTree.c2); + + s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise] = monitorSlots(data); + tTree.s1.assign(tTree.c1, tTree.c2); + tTree.s1.assign(tTree.c1, tTree.c2, tTree.c1, tTree.c2, tTree.c2); + + s1Promise.then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 1); + })); + + let fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + tTree.s1.dispatchEvent(fakeEvent); + })); + +}, 'slotchange event should not fire when same node is assigned.'); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise, s2Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + tTree.s2.assign(tTree.c2); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 1); + assert_equals(data.s2EventCount, 1); + })); +}, "Fire slotchange event when slot's assigned nodes changes."); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise, s2Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + + s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise, s2Promise] = monitorSlots(data); + tTree.s2.assign(tTree.c1); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 2); + assert_equals(data.s2EventCount, 1); + })); + })); +}, "Fire slotchange event on previous slot and new slot when node is reassigned."); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + + s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise] = monitorSlots(data); + tTree.s1.assign(); + + s1Promise.then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 2); + })); + })); +}, "Fire slotchange event on node assignment and when assigned node is removed."); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1, tTree.c2); + + s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise] = monitorSlots(data); + tTree.s1.assign(tTree.c2, tTree.c1); + + s1Promise.then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 2); + })); + })); +}, "Fire slotchange event when order of assigned nodes changes."); + +promise_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + + return s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise] = monitorSlots(data); + tTree.c1.remove(); + + return s1Promise; + })) + .then(test.step_func(() => { + assert_equals(data.s1EventCount, 2); + })); +}, "Fire slotchange event when assigned node is removed."); + +promise_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + [s1Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + + return s1Promise.then(test.step_func(() => { + assert_equals(data.s1EventCount, 1); + + [s1Promise] = monitorSlots(data); + tTree.s1.remove(); + + return s1Promise; + })) + .then(test.step_func(() => { + assert_equals(data.s1EventCount, 2); + })); +}, "Fire slotchange event when removing a slot from Shadows Root that changes its assigned nodes."); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise] = monitorSlots(data); + + tTree.s1.remove(); + + let fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + tTree.s1.dispatchEvent(fakeEvent); + + s1Promise.then(test.step_func(() => { + assert_equals(data.s2EventCount, 0); + + [s1Promise, s2Promise] = monitorSlots(data); + tTree.shadow_root.insertBefore(tTree.s1, tTree.s2); + + tTree.s1.dispatchEvent(fakeEvent); + tTree.s2.dispatchEvent(fakeEvent); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 0); + assert_equals(data.s2EventCount, 0); + })); + })); + +}, "No slotchange event when adding or removing an empty slot."); + +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_slotchange, test, data); + let [s1Promise, s2Promise] = monitorSlots(data); + + tTree.host.appendChild(document.createElement("div")); + + let fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + tTree.s1.dispatchEvent(fakeEvent); + tTree.s2.dispatchEvent(fakeEvent); + + Promise.all([s1Promise, s2Promise]).then(test.step_func(() => { + assert_equals(data.s1EventCount, 0); + assert_equals(data.s2EventCount, 0); + + [s1Promise, s2Promise] = monitorSlots(data); + tTree.shadow_root.insertBefore(document.createElement("div"), tTree.s2); + + tTree.s1.dispatchEvent(fakeEvent); + tTree.s2.dispatchEvent(fakeEvent); + + Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 0); + assert_equals(data.s2EventCount, 0); + })); + })); + +}, "No slotchange event when adding another slotable."); + +</script> + +<div id="test_nested_slotchange"> + <div> + <template data-mode="open" data-slot-assignment="manual"> + <div> + <template data-mode="open" data-slot-assignment="manual"> + <slot id="s2"></slot> + <slot id="s3"></slot> + </template> + <slot id="s1"></slot> + </div> + </template> + <div id="c1"></div> + </div> +</div> + +<script> +async_test((test) => { + const data = getDataCollection(); + let tTree = setupShadowDOM(test_nested_slotchange, test, data); + let [s1Promise, s2Promise, s3Promise] = monitorSlots(data); + + tTree.s3.assign(tTree.s1); + + s3Promise.then(test.step_func(() => { + assert_equals(data.s3EventCount, 1); + [s1Promise, s2Promise, s3Promise] = monitorSlots(data); + + tTree.s1.assign(tTree.c1); + + Promise.all([s1Promise, s3Promise]).then(test.step_func_done(() => { + assert_equals(data.s1EventCount, 1); + assert_equals(data.s3EventCount, 2); + })); + })); +}, "Fire slotchange event when assign node to nested slot, ensure event bubbles ups."); + +promise_test(async t => { + async function mutationObserversRun() { + return new Promise(r => { + t.step_timeout(r, 0); + }); + } + let tTree = createTestTree(test_slotchange); + + tTree.s1.assign(tTree.c1); + tTree["s2.5"].assign(tTree.c2); + + let slotChangedOrder = []; + + // Clears out pending mutation observers + await mutationObserversRun(); + + tTree.s1.addEventListener("slotchange", function() { + slotChangedOrder.push("s1"); + }); + + tTree.s3.addEventListener("slotchange", function() { + slotChangedOrder.push("s3"); + }); + + tTree["s2.5"].addEventListener("slotchange", function() { + slotChangedOrder.push("s2.5"); + }); + + tTree.s3.assign(tTree.c2, tTree.c1); + await mutationObserversRun(); + assert_array_equals(slotChangedOrder, ["s1", "s2.5", "s3"]); +}, 'Signal a slot change should be done in tree order.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-api.html b/testing/web-platform/tests/shadow-dom/imperative-slot-api.html new file mode 100644 index 0000000000..bcf052bbab --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-api.html @@ -0,0 +1,303 @@ +<!DOCTYPE html> +<title>Shadow DOM: Imperative Slot API</title> +<meta name="author" title="Yu Han" href="mailto:yuzhehan@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test_basic"> + <div id="host1"></div> + <div id="host2"></div> + <div id="host3"></div> +</div> +<script> +test(() => { + let tTree = createTestTree(test_basic); + const shadow1 = tTree.host1.attachShadow({ mode: 'open', slotAssignment: 'manual'}); + assert_not_equals(shadow1, null, 'slot assignment manual should work'); + assert_equals(shadow1.slotAssignment, "manual", 'slotAssignment should return "manual"'); + const shadow2 = tTree.host2.attachShadow({ mode: 'open', slotAssignment: 'named'}); + assert_not_equals(shadow2, null, 'slot assignment named should work'); + assert_equals(shadow2.slotAssignment, "named", 'slotAssignment should return "named"'); + assert_throws_js(TypeError, () => { + tTree.host3.attachShadow({ mode: 'open', slotAssignment: 'exceptional' })}, + 'others should throw exception'); +}, 'attachShadow can take slotAssignment parameter.'); +</script> + +<div id="test_assign"> + <div id="host"> + <template id="shadow_root" data-mode="open" data-slot-assignment="manual"> + <slot id="s1"></slot> + <slot id="s2"></slot> + <slot id="s3"></slot> + </template> + <div id="c1"></div> + <div id="c2"></div> + <div id="c3"></div> + <div id="nested"> + <div id="ns1"></div> + </div> + </div> + <div id="c4"></div> + <div id="host4"> + <template id="shadow_root4" data-mode="open" data-slot-assignment="manual"> + <slot id="s4" name="s4"></slot> + </template> + </div> +</div> +<script> +test(() => { + let tTree = createTestTree(test_assign); + tTree.s1.assign(c1,c2); // Should work + assert_throws_js(TypeError, () => { + tTree.s1.assign([c1,c2]) + }, 'sequence not allowed'); + assert_throws_js(TypeError, () => { + tTree.s1.assign([]) + }, 'including empty sequences'); +}, 'slot.attach() should take variadic not sequence.'); + +test(() => { + let tTree = createTestTree(test_assign); + assert_array_equals(tTree.s2.assignedElements(), []); + assert_equals(tTree.c1.assignedSlot, null); + + tTree.s1.assign(tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + + tTree.s2.assign(tTree.c2, tTree.c3); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); + assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2, tTree.c3]); +}, 'Imperative slot API can assign nodes in manual slot assignment.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c2, tTree.c3, tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + assert_equals(tTree.c3.assignedSlot, tTree.s1); + + tTree.s1.assign(tTree.c1, tTree.c2); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + assert_equals(tTree.c3.assignedSlot, null); + + tTree.s1.assign(tTree.c3, tTree.c2, tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c3, tTree.c2, tTree.c1]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + assert_equals(tTree.c3.assignedSlot, tTree.s1); +}, 'Order of slottables is preserved in manual slot assignment.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c2, tTree.c3, tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3, tTree.c1]); + + tTree.s2.assign(tTree.c2); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c3, tTree.c1]); + assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s2); + assert_equals(tTree.c3.assignedSlot, tTree.s1); + + tTree.s3.assign(tTree.c3); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); + assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]); + assert_array_equals(tTree.s3.assignedNodes(), [tTree.c3]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s2); + assert_equals(tTree.c3.assignedSlot, tTree.s3); +}, 'Previously assigned slottable is moved to new slot when it\'s reassigned.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1); + tTree.s2.assign(tTree.c2, tTree.c1); + tTree.s3.assign(tTree.c1, tTree.c3); + + assert_array_equals(tTree.s1.assignedNodes(), []); + assert_array_equals(tTree.s2.assignedNodes(), [tTree.c2]); + assert_array_equals(tTree.s3.assignedNodes(), [tTree.c1, tTree.c3]); + assert_equals(tTree.c1.assignedSlot, tTree.s3); + assert_equals(tTree.c2.assignedSlot, tTree.s2); + assert_equals(tTree.c3.assignedSlot, tTree.s3); +}, 'Order and assignment of nodes are preserved during multiple assignment in a row.'); + +test(() => { + let tTree = createTestTree(test_assign); + + // tTree.c4 is invalid for tTree.host slot assignment. + // No exception should be thrown here. + tTree.s1.assign(tTree.c1, tTree.c4, tTree.c2); + + // All observable assignments should skip c4. + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + assert_equals(tTree.c4.assignedSlot, null); + + // Moving c4 into place should reveal the assignment. + tTree.host.append(tTree.c4); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c4, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + assert_equals(tTree.c4.assignedSlot, tTree.s1); + + // Moving c4 into a different shadow host and back should + // also not break the assignment. + tTree.host4.append(tTree.c4) + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c4.assignedSlot, null); + tTree.host.append(tTree.c4); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c4, tTree.c2]); + assert_equals(tTree.c4.assignedSlot, tTree.s1); +}, 'Assigning invalid nodes should be allowed.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c2, tTree.c3); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]); + + tTree.host4.append(tTree.s1); + assert_array_equals(tTree.s1.assignedNodes(), []); +}, 'Moving a slot to a new host, the slot loses its previously assigned slottables.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c2, tTree.c3); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]); + + tTree.shadow_root.insertBefore(tTree.s2, tTree.s1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]); + assert_array_equals(tTree.s2.assignedNodes(), []); + + tTree.shadow_root.insertBefore(tTree.s4, tTree.s1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]); + assert_array_equals(tTree.s4.assignedNodes(), []); + + tTree.ns1.append(tTree.s1); + assert_array_equals(tTree.s1.assignedNodes(), []); +}, 'Moving a slot\'s tree order position within a shadow host has no impact on its assigned slottables.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c2, tTree.c3); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2, tTree.c3]); + + tTree.host4.append(tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c2, tTree.c3]); + assert_array_equals(tTree.s4.assignedNodes(), []); + assert_equals(tTree.c1.assignedSlot, null); + + tTree.s4.assign(tTree.c1); + assert_array_equals(tTree.s4.assignedNodes(), [tTree.c1]); + assert_equals(tTree.c1.assignedSlot, tTree.s4); +}, 'Appending slottable to different host, it loses slot assignment. It can be re-assigned within a new host.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); + + tTree.shadow_root4.insertBefore(tTree.s1, tTree.s4); + assert_array_equals(tTree.s1.assignedNodes(), []); + // Trigger slot assignment on previous shadow root. + assert_array_equals(tTree.s2.assignedNodes(), []); + + tTree.shadow_root.insertBefore(tTree.s1, tTree.s2); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); +}, 'Previously assigned node should not be assigned if slot moved to a new shadow root. The node is re-assigned when moved back.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c1, tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1]); + + tTree.s1.assign(tTree.c1, tTree.c1, tTree.c2, tTree.c2, tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); +}, 'Assignment with the same node in parameters should be ignored, first one wins.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c2, tTree.c3); + tTree.s1.remove(); + + assert_equals(tTree.c1.assignedSlot, null); + assert_equals(tTree.c2.assignedSlot, null); + assert_equals(tTree.c3.assignedSlot, null); +}, 'Removing a slot from DOM resets its slottable\'s slot assignment.'); + +test(() => { + let tTree = createTestTree(test_assign); + + const isolatedDocNode = document.implementation.createHTMLDocument("").body; + isolatedDocNode.appendChild(tTree.c1); + const isolatedDocNode2 = document.implementation.createHTMLDocument("").body; + isolatedDocNode2.appendChild(tTree.s1); + + tTree.s1.assign(tTree.c1, tTree.c2); + assert_array_equals(tTree.s1.assignedNodes(), [], 's1 not inside shadow root'); + assert_equals(tTree.c1.assignedSlot, null); + assert_equals(tTree.c2.assignedSlot, null); + + tTree.shadow_root.appendChild(tTree.s1); + tTree.host.appendChild(tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); +}, 'Nodes can be assigned even if slots or nodes aren\'t in the same tree.'); + +test(() => { + let tTree = createTestTree(test_assign); + + tTree.s1.assign(tTree.c1, tTree.c2); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); + + const isolatedDocNode = document.implementation.createHTMLDocument("").body; + isolatedDocNode.appendChild(tTree.c1); + const isolatedDocNode2 = document.implementation.createHTMLDocument("").body; + isolatedDocNode2.appendChild(tTree.s1); + + assert_array_equals(tTree.s1.assignedNodes(), [], 's1 not inside shadow root'); + assert_equals(tTree.c1.assignedSlot, null); + assert_equals(tTree.c2.assignedSlot, null); + + tTree.shadow_root.appendChild(tTree.s1); + tTree.host.appendChild(tTree.c1); + assert_array_equals(tTree.s1.assignedNodes(), [tTree.c1, tTree.c2]); + assert_equals(tTree.c1.assignedSlot, tTree.s1); + assert_equals(tTree.c2.assignedSlot, tTree.s1); +}, 'Removing a node from the document does not break manually assigned slot linkage.'); + +test(() => { + const inputValues = [ + ['Attr', document.createAttribute('bar')], + ['Comment', document.createComment('bar')], + ['DocumentFragment', document.createDocumentFragment()], + ['DocumentType', document.implementation.createDocumentType('html', '', '')] + ]; + for (const [label, input] of inputValues) { + assert_throws_js(TypeError, () => { + const slot = document.createElement('slot'); + slot.assign(input); + }, label); + } +}, 'throw TypeError if the passed values are neither Element nor Text'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-assign-not-slotable-crash.html b/testing/web-platform/tests/shadow-dom/imperative-slot-assign-not-slotable-crash.html new file mode 100644 index 0000000000..a493eeaf00 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-assign-not-slotable-crash.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel=help href="https://crbug.com/1240783"> + +<p>This test passes if it does not crash.</p> + +<slot id=slot> +<object id=object> +<script> +onload = () => { + const nonSlotable = document.createProcessingInstruction(undefined, undefined); + document.getElementById('slot').assign(nonSlotable); + document.getElementById('object').appendChild(nonSlotable); +} +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-fallback-clear.html b/testing/web-platform/tests/shadow-dom/imperative-slot-fallback-clear.html new file mode 100644 index 0000000000..5f79b4f2fa --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-fallback-clear.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<title>Imperative Slot API: fallback should be cleared after slot assignment</title> +<link rel="author" href="mailto:xiaochengh@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1292292"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div> + <div id="host1"><span></span></div> + <div id="host2"><span></span></div> +</div> + +<script> +test(() => { + const host = document.getElementById('host1'); + const shadow = host.attachShadow({mode: 'open', slotAssignment: 'manual'}); + shadow.innerHTML = '<slot>fallback</slot>'; + host.offsetHeight; // Force layout + const slot = shadow.firstChild; + slot.assign(host.firstChild); + assert_array_equals(slot.assignedNodes(), [host.firstChild]); + assert_equals(host.offsetHeight, 0, 'Fallback content should not be rendered'); +}, 'Text node fallback should be cleared in a subsequently layout'); + +test(() => { + const host = document.getElementById('host2'); + const shadow = host.attachShadow({mode: 'open', slotAssignment: 'manual'}); + shadow.innerHTML = '<slot><span>fallback</span></slot>'; + host.offsetHeight; // Force layout + const slot = shadow.firstChild; + slot.assign(host.firstChild); + assert_array_equals(slot.assignedNodes(), [host.firstChild]); + assert_equals(host.offsetHeight, 0, 'Fallback content should not be rendered'); +}, 'Element fallback should be cleared in a subsequent layout'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-initial-fallback.html b/testing/web-platform/tests/shadow-dom/imperative-slot-initial-fallback.html new file mode 100644 index 0000000000..87b37aa827 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-initial-fallback.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>Imperative Slot API: intial fallback should be correctly set up</title> +<link rel="author" href="mailto:xiaochengh@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1292292"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<div> + <div id="host1"></div> + <div id="host2"></div> +</div> + +<script> +test(() => { + const host = document.getElementById('host1'); + const shadow = host.attachShadow({mode: 'open', slotAssignment: 'manual'}); + shadow.innerHTML = '<slot>fallback</slot>'; + const slot = shadow.firstChild; + assert_array_equals(slot.assignedNodes(), []); + assert_greater_than(host.offsetHeight, 0, 'Fallback content should be rendered'); +}, 'Unassigned imperative slot can render text node as the initial fallback'); + +test(() => { + const host = document.getElementById('host2'); + const shadow = host.attachShadow({mode: 'open', slotAssignment: 'manual'}); + shadow.innerHTML = '<slot><span>fallback</span></slot>'; + const slot = shadow.firstChild; + assert_array_equals(slot.assignedNodes(), []); + assert_greater_than(host.offsetHeight, 0, 'Fallback content should be rendered'); +}, 'Unassigned imperative slot can render element as the initial fallback'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001-ref.html b/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001-ref.html new file mode 100644 index 0000000000..98bc61ba4d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001-ref.html @@ -0,0 +1,5 @@ +<!doctype html> +<title>CSS Test Reference</title> +<div id="host"> + <span>two</span> +</div> diff --git a/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001.html b/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001.html new file mode 100644 index 0000000000..c6a579b70c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/imperative-slot-layout-invalidation-001.html @@ -0,0 +1,27 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Imperative slotting API: Invalidation on re-assignment</title> +<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1752694"> +<link rel=match href="imperative-slot-layout-invalidation-001-ref.html"> +<link rel=author href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel=author href="https://mozilla.org" title="Mozilla"> +<div id="host"> + <span>one</span> +</div> +<script> + let host = document.getElementById("host"); + + function assignFirstChild() { + host.shadowRoot.querySelector("slot").assign(host.firstElementChild); + } + + host.attachShadow({ mode: "open", slotAssignment: "manual" }).innerHTML = "<slot>Fallback</slot>"; + + assignFirstChild(); + + host.getBoundingClientRect(); + + host.insertAdjacentHTML("afterbegin", "<span>two</span>"); + + assignFirstChild(); +</script> diff --git a/testing/web-platform/tests/shadow-dom/innerHTML-setter.xhtml b/testing/web-platform/tests/shadow-dom/innerHTML-setter.xhtml new file mode 100644 index 0000000000..0122707e2a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/innerHTML-setter.xhtml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:bar="bar"> + <head> + <title>Test for Shadow DOM innerHTML setter in XML</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script> + <![CDATA[ + // We define our custom elements lazily so we don't mess + // with the DOM during parsing. + customElements.define("custom-el-1", + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + try { this.shadowRoot.innerHTML = "<span/>"; } catch (e) {} + } + }); + function defineElements() { + // We define our custom elements whose behavior involves + // ancestors of our parent lazily, because otherwise the + // constructor runs before the element is in the DOM and has + // the ancestors set up. + customElements.define("custom-el-2", + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + try { this.shadowRoot.innerHTML = "<span/>"; } catch (e) {} + } + }); + customElements.define("custom-el-with-prefix", + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + try { + this.shadowRoot.innerHTML = "<bar:span/>"; + } catch (e) { + // Test will fail due to us not having the kid + } + } + }); + } + ]]> + </script> + </head> + <body> + <custom-el-1 id="htmlDefault"/> + <span xmlns="foo" xmlns:html="http://www.w3.org/1999/xhtml"> + <html:custom-el-2 id="fooDefault"/> + </span> + <custom-el-with-prefix id="prefixTest"/> + <script> + <![CDATA[ + const htmlNS = "http://www.w3.org/1999/xhtml"; + test(() => { + var el = document.getElementById("htmlDefault"); + var kid = el.shadowRoot.firstChild; + assert_equals(kid.namespaceURI, htmlNS, + "Kid's namespace should be our default"); + }, "InnerHTML behavior on custom element in default XHTML namespace"); + + test(defineElements, "Setting up the custom elements"); + test(() => { + var el = document.getElementById("fooDefault"); + var kid = el.shadowRoot.firstChild; + assert_equals(kid.namespaceURI, "foo", + "Kid's namespace should be our default"); + }, "InnerHTML behavior on custom element in default 'foo' namespace"); + + test(() => { + var el = document.getElementById("prefixTest"); + var kid = el.shadowRoot.firstChild; + assert_equals(kid.namespaceURI, "bar", + "Kid's namespace should be based on ancestor prefix"); + }, "InnerHTML behavior with prefixes on custom element"); + ]]> + </script> + </body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/input-element-list.html b/testing/web-platform/tests/shadow-dom/input-element-list.html new file mode 100644 index 0000000000..b571534eb0 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/input-element-list.html @@ -0,0 +1,39 @@ +<!doctype html> +<meta charset=utf-8> +<title>Input.list</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id="testcontent"> + <input id="input" list="datalist"> +</div> +<script> + +test(() => { + assert_equals(document.getElementById('input').list, null); + var dl = document.createElement("datalist"); + dl.id = "datalist"; + document.getElementById("testcontent").appendChild(dl); + assert_equals(document.getElementById('input').list, dl); +}, "Input element's list attribute should point to the datalist element."); + + +test(() => { + var host = document.createElement("div"); + document.getElementById("testcontent").appendChild(host); + var sr = host.attachShadow({mode: "open"}); + var input = document.createElement("input"); + input.setAttribute("list", "datalist"); + sr.appendChild(input); + assert_equals(input.list, null); + + var dl = document.createElement("datalist"); + dl.id = "datalist"; + sr.appendChild(dl); + assert_equals(input.list, dl); + + dl.remove(); + assert_equals(input.list, null); +}, "Input element's list attribute should point to the datalist element in Shadow DOM."); + + +</script> diff --git a/testing/web-platform/tests/shadow-dom/input-type-radio.html b/testing/web-platform/tests/shadow-dom/input-type-radio.html new file mode 100644 index 0000000000..9c90fcf060 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/input-type-radio.html @@ -0,0 +1,73 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<input type="radio" name="group" id="lightRadio1"> +<input type="radio" name="group" id="lightRadio2"> +<div id="host"></div> +<script> + +test(() => { + var lightRadio1 = document.getElementById("lightRadio1"); + var lightRadio2 = document.getElementById("lightRadio2"); + + var host = document.getElementById("host"); + var sr = host.attachShadow({mode: "closed"}); + var shadowRadio1 = document.createElement("input"); + shadowRadio1.name = "group"; + shadowRadio1.id = "shadowRadio1"; + shadowRadio1.type = "radio"; + sr.appendChild(shadowRadio1); + var shadowRadio2 = document.createElement("input"); + shadowRadio2.name = "group"; + shadowRadio2.id = "shadowRadio2"; + shadowRadio2.type = "radio"; + sr.appendChild(shadowRadio2); + + assert_false(lightRadio1.checked); + assert_false(lightRadio2.checked); + assert_false(shadowRadio1.checked); + assert_false(shadowRadio2.checked); + + lightRadio1.click(); + assert_true(lightRadio1.checked); + assert_false(lightRadio2.checked); + assert_false(shadowRadio1.checked); + assert_false(shadowRadio2.checked); + + lightRadio2.click(); + assert_false(lightRadio1.checked); + assert_true(lightRadio2.checked); + assert_false(shadowRadio1.checked); + assert_false(shadowRadio2.checked); + + shadowRadio1.click(); + assert_false(lightRadio1.checked); + assert_true(lightRadio2.checked); + assert_true(shadowRadio1.checked); + assert_false(shadowRadio2.checked); + + shadowRadio2.click(); + assert_false(lightRadio1.checked); + assert_true(lightRadio2.checked); + assert_false(shadowRadio1.checked); + assert_true(shadowRadio2.checked); + + // Ensure radio groups work even when modifying shadow DOM. + shadowRadio2.remove(); + sr.appendChild(shadowRadio2); + shadowRadio2.click(); + assert_false(lightRadio1.checked); + assert_true(lightRadio2.checked); + assert_false(shadowRadio1.checked); + assert_true(shadowRadio2.checked); + + shadowRadio1.click(); + assert_false(lightRadio1.checked); + assert_true(lightRadio2.checked); + assert_true(shadowRadio1.checked); + assert_false(shadowRadio2.checked); +}, "input type=radio elements should form a group inside shadow DOM."); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/invalidate-shadow-dom-crash.html b/testing/web-platform/tests/shadow-dom/invalidate-shadow-dom-crash.html new file mode 100644 index 0000000000..ebbcabf131 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/invalidate-shadow-dom-crash.html @@ -0,0 +1,12 @@ +<style> +.c~*{} +</style> +<script> +document.addEventListener('DOMContentLoaded', () => { + b.select() + a.className = "c" +}) +</script> +<details> +<summary id="a"></summary> +<textarea id="b"> diff --git a/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots-ref.html b/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots-ref.html new file mode 100644 index 0000000000..1fc6dd5522 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots-ref.html @@ -0,0 +1,10 @@ +<!doctype html> +<title>Invalidation works properly across siblings on different slots</title> +<style> +.x { display: table-header-group; } +.x + * { color: green } +</style> +<details open=""> + <summary class="x" id="a">Main summary</summary> + <summary>Should be green</summary> +</details> diff --git a/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots.html b/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots.html new file mode 100644 index 0000000000..0846389a9e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/invalidate-sibling-different-slots.html @@ -0,0 +1,20 @@ +<!doctype html> +<title>Invalidation works properly across siblings on different slots</title> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1794720"> +<link rel="match" href="invalidate-sibling-different-slots-ref.html"> +<style> +.x { display: table-header-group; } +.x + * { color: green } +</style> +<details open=""> + <summary id="a">Main summary</summary> + <summary>Should be green</summary> +</details> +<script> +onload = function() { + let a = document.getElementById("a"); + a.getBoundingClientRect(); + a.setAttribute("class", "x") + a.getBoundingClientRect(); +}; +</script> diff --git a/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-assigned.html b/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-assigned.html new file mode 100644 index 0000000000..dfcac99da0 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-assigned.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<title>Layout using slot elements</title> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"/> +<link rel="help" href="https://dom.spec.whatwg.org/#shadow-tree-slots"> +<link rel="match" href="reference/empty.html"/> +<div id="host"></div> +<script> +const host = document.querySelector('#host'); +const sr = host.attachShadow({ mode: 'open' }); +sr.innerHTML = '<slot name=s1></slot>' +host.innerHTML = '<div id=d1 slot=s1></div>'; + +document.body.offsetLeft; +document.querySelector('#d1').setAttribute('slot', 's2'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-fallback.html b/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-fallback.html new file mode 100644 index 0000000000..7507f11ac1 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/layout-slot-no-longer-fallback.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<title>Layout using slot elements</title> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"/> +<link rel="help" href="https://dom.spec.whatwg.org/#shadow-tree-slots"> +<link rel="match" href="reference/empty.html"/> +<div id="host"></div> +<script> +const host = document.querySelector('#host'); +const sr = host.attachShadow({ mode: 'open' }); + +sr.innerHTML = '<slot><div id="fallback">Should not be displayed</div></slot>' + +document.body.offsetLeft; +host.appendChild(document.createElement('div')); +</script> diff --git a/testing/web-platform/tests/shadow-dom/leaktests/get-elements.html b/testing/web-platform/tests/shadow-dom/leaktests/get-elements.html new file mode 100644 index 0000000000..40fa9b6931 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/leaktests/get-elements.html @@ -0,0 +1,174 @@ +<!DOCTYPE html> +<html> +<head> +<meta name='author' title='Google' href='http://www.google.com'> +<meta name='assert' content='getElement* API in document should not leak any node in shadow tree.'> +<link rel='help' href='https://w3c.github.io/webcomponents/spec/shadow/'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +</head> +<body> + +<!-- This template will be filled in '#doc', '#host-open', and '#host-closed' below --> +<template id='domtree-template'> + <span id='foo'></span> + <div class='bar'></div> + <form name='baz'></form> + <my-element></my-element> +</template> + +<div id='doc'> + <div id='host-open'></div> + <div id='host-closed'></div> +</div> + +</body> +<script> +'use strict'; + +function fillTemplate(root, prefix) { + var tmpl = document.getElementById('domtree-template'); + root.appendChild(document.importNode(tmpl.content, true)); + for (var i = 0; i < root.childNodes.length; ++i) { + var el = root.childNodes[i]; + if (el.nodeType != 1) + continue; + el.setAttribute('label', prefix + el.tagName.toLowerCase()); + } + + root.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient')); +} + +// Construct subtree with 'doc-*' ids. +var doc = document.getElementById('doc'); +fillTemplate(doc, 'doc-'); + +// Construct shadow subtree with 'shadow-*' ids. +var hostOpen = document.getElementById('host-open'); +var shadowOpen = hostOpen.attachShadow({mode: 'open'}); +fillTemplate(shadowOpen, 'shadow-open-'); + +var hostClosed = document.getElementById('host-closed'); +var shadowClosed = hostClosed.attachShadow({mode: 'closed'}); +fillTemplate(shadowClosed, 'shadow-closed-'); + +test(function() { + // getElementById() (NonElementParentNode) + assert_equals(document.querySelectorAll('#foo').length, 1); + assert_equals(document.getElementById('foo').getAttribute('label'), 'doc-span'); + assert_equals(document.querySelector('#foo').getAttribute('label'), 'doc-span'); + + assert_equals(doc.querySelectorAll('#foo').length, 1); + assert_equals(doc.querySelector('#foo').getAttribute('label'), 'doc-span'); + + assert_equals(hostOpen.querySelectorAll('#foo').length, 0); + + assert_equals(shadowOpen.querySelectorAll('#foo').length, 1); + assert_equals(shadowOpen.getElementById('foo').getAttribute('label'), 'shadow-open-span'); + assert_equals(shadowOpen.querySelector('#foo').getAttribute('label'), 'shadow-open-span'); + + assert_equals(hostClosed.querySelectorAll('#foo').length, 0); + + assert_equals(shadowClosed.querySelectorAll('#foo').length, 1); + assert_equals(shadowClosed.getElementById('foo').getAttribute('label'), 'shadow-closed-span'); + assert_equals(shadowClosed.querySelector('#foo').getAttribute('label'), 'shadow-closed-span'); +}, 'getElementsById() should not leak nodes in shadow tree'); + +test(function() { + // getElementsByClassName() (Element, Document) + assert_equals(document.getElementsByClassName('bar').length, 1); + assert_equals(document.getElementsByClassName('bar')[0].getAttribute('label'), 'doc-div'); + assert_equals(document.getElementsByClassName('bar').length, 1); + assert_equals(document.getElementsByClassName('bar')[0].getAttribute('label'), 'doc-div'); + assert_equals(document.querySelectorAll('.bar').length, 1); + + assert_equals(doc.querySelectorAll('.bar').length, 1); + assert_equals(doc.getElementsByClassName('bar')[0].getAttribute('label'), 'doc-div'); + + assert_equals(hostOpen.querySelectorAll('.bar').length, 0); + + assert_equals(shadowOpen.querySelectorAll('.bar').length, 1); + assert_equals(shadowOpen.querySelectorAll('.bar')[0].getAttribute('label'), 'shadow-open-div'); + + assert_equals(hostClosed.querySelectorAll('.bar').length, 0); + + assert_equals(shadowClosed.querySelectorAll('.bar').length, 1); + assert_equals(shadowClosed.querySelectorAll('.bar')[0].getAttribute('label'), 'shadow-closed-div'); +}, 'getElementsByClassName() should not leak nodes in shadow tree'); + +test(function() { + // getElementsByName (Document) + assert_equals(document.getElementsByName('baz').length, 1); + assert_equals(document.getElementsByName('baz')[0].getAttribute('label'), 'doc-form'); + assert_equals(document.getElementsByName('baz').length, 1); + assert_equals(document.getElementsByName('baz')[0].getAttribute('label'), 'doc-form'); + assert_equals(document.querySelectorAll('[name=baz]').length, 1); + + assert_equals(doc.querySelectorAll('[name=baz]').length, 1); + + assert_equals(hostOpen.querySelectorAll('[name=baz]').length, 0); + assert_equals(shadowOpen.querySelectorAll('[name=baz]').length, 1); + assert_equals(shadowOpen.querySelectorAll('[name=baz]')[0].getAttribute('label'), 'shadow-open-form'); + + assert_equals(hostClosed.querySelectorAll('[name=baz]').length, 0); + assert_equals(shadowClosed.querySelectorAll('[name=baz]').length, 1); + assert_equals(shadowClosed.querySelectorAll('[name=baz]')[0].getAttribute('label'), 'shadow-closed-form'); +}, 'getElementsByName() should not leak nodes in shadow tree'); + +test(function() { + // getElementsByTagName (Element, Document) + assert_equals(document.getElementsByTagName('my-element').length, 1); + assert_equals(document.getElementsByTagName('my-element')[0].getAttribute('label'), 'doc-my-element'); + assert_equals(document.getElementsByTagName('my-element').length, 1); + assert_equals(document.getElementsByTagName('my-element')[0].getAttribute('label'), 'doc-my-element'); + assert_equals(document.querySelectorAll('my-element').length, 1); + + assert_equals(doc.querySelectorAll('my-element').length, 1); + assert_equals(doc.getElementsByTagName('my-element')[0].getAttribute('label'), 'doc-my-element'); + + assert_equals(hostOpen.querySelectorAll('my-element').length, 0); + // ShadowRoot isn't an Element, does not have getElementsByTagName(). + assert_equals(shadowOpen.querySelectorAll('my-element').length, 1); + assert_equals(shadowOpen.querySelectorAll('my-element')[0].getAttribute('label'), 'shadow-open-my-element'); + + assert_equals(hostClosed.querySelectorAll('my-element').length, 0); + assert_equals(shadowClosed.querySelectorAll('my-element').length, 1); + assert_equals(shadowClosed.querySelectorAll('my-element')[0].getAttribute('label'), 'shadow-closed-my-element'); +}, 'getElementsByTagName() should not leak nodes in shadow tree'); + +test(function() { + // getElementsByTagNameNS (Element, Document) + assert_equals(document.getElementsByTagName('lineargradient').length, 0); + assert_equals(document.getElementsByTagNameNS('*', 'lineargradient').length, 0); + assert_equals(document.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'lineargradient').length, 0); + assert_equals(document.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'lineargradient').length, 0); + + assert_equals(document.getElementsByTagName('linearGradient').length, 1); + assert_equals(document.getElementsByTagNameNS('*', 'linearGradient').length, 1); + assert_equals(document.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'linearGradient').length, 1); + assert_equals(document.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'linearGradient').length, 0); + + assert_equals(doc.getElementsByTagName('lineargradient').length, 0); + assert_equals(doc.getElementsByTagNameNS('*', 'lineargradient').length, 0); + assert_equals(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'lineargradient').length, 0); + assert_equals(doc.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'lineargradient').length, 0); + + assert_equals(doc.getElementsByTagName('linearGradient').length, 1); + assert_equals(doc.getElementsByTagNameNS('*', 'linearGradient').length, 1); + assert_equals(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'linearGradient').length, 1); + assert_equals(doc.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'linearGradient').length, 0); + + assert_equals(hostOpen.getElementsByTagName('linearGradient').length, 0); + assert_equals(hostOpen.getElementsByTagNameNS('*', 'linearGradient').length, 0); + assert_equals(hostOpen.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'linearGradient').length, 0); + assert_equals(hostOpen.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'linearGradient').length, 0); + + assert_equals(hostClosed.getElementsByTagName('linearGradient').length, 0); + assert_equals(hostClosed.getElementsByTagNameNS('*', 'linearGradient').length, 0); + assert_equals(hostClosed.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'linearGradient').length, 0); + assert_equals(hostClosed.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'linearGradient').length, 0); + + // ShadowRoot isn't an Element, does not have getElementsByTagNameNS(). +}, 'getElementsByTagNameNS() should not leak nodes in shadow tree'); +</script> +</html> diff --git a/testing/web-platform/tests/shadow-dom/leaktests/html-collection.html b/testing/web-platform/tests/shadow-dom/leaktests/html-collection.html new file mode 100644 index 0000000000..1ce2cf3440 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/leaktests/html-collection.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> +<meta name='author' title='Google' href='http://www.google.com'> +<meta name='assert' content='document attributes that returns HTMLCollection should not expose nodes in shadow tree.'> +<link rel='help' href='https://w3c.github.io/webcomponents/spec/shadow/'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +</head> +<body> +<template id='collection-template'> + <img> + <embed></embed> + <applet></applet> + <object type='application/x-java-applet'></object> + <a href='http://example.com'></a> + <a name='test'></a> + <form name='test'></form> + <script></script> +</template> +<div id='doc'></div> +<div id='host-open'></div> +<div id='host-closed'></div> +</body> +<script> +'use strict'; + +function fillTemplate(root, prefix) { + var tmpl = document.getElementById('collection-template'); + root.appendChild(document.importNode(tmpl.content, true)); + for (var i = 0; i < root.childNodes.length; ++i) { + var el = root.childNodes[i]; + if (el.nodeType != 1) + continue; + el.id = prefix + el.tagName.toLowerCase(); + } +} + +// Construct subtree with 'doc-*' ids. +var doc = document.getElementById('doc'); +fillTemplate(doc, 'doc-'); + +// Construct shadow subtree with 'shadow-*' ids. +var host = document.getElementById('host-open'); +var shadow = host.attachShadow({mode: 'open'}); +fillTemplate(shadow, 'shadow-open-'); + +host = document.getElementById('host-closed'); +shadow = host.attachShadow({mode: 'closed'}); +fillTemplate(shadow, 'shadow-closed-'); + +function testCollection(collection) { + var elements = document[collection]; + assert_greater_than(elements.length, 0, 'document.' + collection + ' should have at least 1 element.'); + for (var i = 0; i < elements.length; ++i) { + if (elements[i].id) { + assert_equals(elements[i].id.indexOf('shadow-'), -1, 'document.' + collection + ' should not contain elements in shadow tree.'); + } + } +} + +var testParams = [ + ['document.scripts should not contain shadow nodes', 'scripts'], + ['document.all should not contain shadow nodes', 'all'], + ['document.forms should not contain shadow nodes', 'forms'], + ['document.images should not contain shadow nodes', 'images'], + ['document.links should not contain shadow nodes', 'links'], + ['document.anchors should not contain shadow nodes', 'anchors'], + ['document.embeds should not contain shadow nodes', 'embeds'], + ['document.plugins should not contain shadow nodes', 'plugins']]; + +generate_tests(testCollection, testParams); + +test(() => { + assert_equals(document.applets.length, 0); +}, 'document.applets should not contain any nodes'); + +</script> +</html> diff --git a/testing/web-platform/tests/shadow-dom/leaktests/window-frames.html b/testing/web-platform/tests/shadow-dom/leaktests/window-frames.html new file mode 100644 index 0000000000..5ba2531ff2 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/leaktests/window-frames.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<meta name='author' title='Google' href='http://www.google.com'> +<meta name='assert' content='Shadow DOM should not leak via window.frames.'> +<link rel='help' href='https://w3c.github.io/webcomponents/spec/shadow/'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +</head> +<body> +<div id='log'></div> +<iframe src='about:blank' name='mainFrame1'></iframe> +<div id='host-open'></div> +<div id='host-closed'></div> +</body> +<script> +'use strict'; + +var host_open = document.getElementById('host-open'); +var root_open = host_open.attachShadow({mode: 'open'}); +root_open.innerHTML = '<iframe src="about:blank" name="shadowFrame1"></iframe>'; + +var host_closed = document.getElementById('host-closed'); +var root_closed = host_closed.attachShadow({mode: 'closed'}); +root_closed.innerHTML = '<iframe src="about:blank" name="shadowFrame2"></iframe>'; + +test(() => { + assert_equals(window.frames.length, 1, 'window.frames should return only frames in document.'); + assert_equals(window.frames[0].name, 'mainFrame1', 'window.frames[0] should be mainFrame1.'); + assert_equals(window.frames['mainFrame1'], window.frames[0], 'window.frames[\'mainFrame1\'] should be equal to mainFrame1.'); + assert_equals(window.frames['shadowFrame1'], undefined, 'shadowFrame1 should not leak.'); + assert_equals(window.frames['shadowFrame2'], undefined, 'shadowFrame2 should not leak.'); + +}, 'window.frames should not leak frames in Shadow DOM.'); +</script> +</html> diff --git a/testing/web-platform/tests/shadow-dom/nested-slot-remove-crash.html b/testing/web-platform/tests/shadow-dom/nested-slot-remove-crash.html new file mode 100644 index 0000000000..2ac3f109c7 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/nested-slot-remove-crash.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://crbug.com/1159328"> +<meta name="assert" content="The renderer should not crash."> + +<div id="host"></div> +<span>This test passes if the renderer does not crash.</span> + +<script> + const root = host.attachShadow({mode:"open"}); + root.innerHTML = ` + <slot id=outer1 name=outer> + <slot id=inner1 name=inner>Fallback</slot> + </slot> + `; + document.body.offsetTop; + // The renderer should not crash here: + root.querySelector("#outer1").remove(); + + root.innerHTML = ` + <slot id=outer2> + <slot id=inner2>Fallback</slot> + </slot> + `; + document.body.offsetTop; + // The renderer should not crash here: + root.querySelector("#outer2").remove(); + + root.innerHTML = ` + <slot id=outer3> + <slot id=inner3> + <slot id=way-inner3>Fallback</slot> + </slot> + </slot> + `; + document.body.offsetTop; + // The renderer should not crash here: + root.querySelector("#outer3").remove(); +</script> diff --git a/testing/web-platform/tests/shadow-dom/offsetParent-across-shadow-boundaries.html b/testing/web-platform/tests/shadow-dom/offsetParent-across-shadow-boundaries.html new file mode 100644 index 0000000000..5491e21213 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/offsetParent-across-shadow-boundaries.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="offsetParent should only return nodes that are shadow including ancestor"> +<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent"> +<link rel="help" href="https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/event-path-test-helpers.js"></script> +</head> +<body> +<div id="log"></div> +<div id="container" style="position: relative"></div> +<script> + +const container = document.getElementById('container'); + +function testOffsetParentInShadowTree(mode) { + test(function () { + const host = document.createElement('div'); + container.appendChild(host); + this.add_cleanup(() => host.remove()); + const shadowRoot = host.attachShadow({mode}); + shadowRoot.innerHTML = '<div id="relativeParent" style="position: relative; padding-left: 100px; padding-top: 70px;"><div id="target"></div></div>'; + const relativeParent = shadowRoot.getElementById('relativeParent'); + + assert_true(relativeParent instanceof HTMLDivElement); + const target = shadowRoot.getElementById('target'); + assert_equals(target.offsetParent, relativeParent); + assert_equals(target.offsetLeft, 100); + assert_equals(target.offsetTop, 70); + }, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode`); +} + +testOffsetParentInShadowTree('open'); +testOffsetParentInShadowTree('closed'); + +function testOffsetParentInNestedShadowTrees(mode) { + test(function () { + const outerHost = document.createElement('section'); + container.appendChild(outerHost); + this.add_cleanup(() => outerHost.remove()); + const outerShadow = outerHost.attachShadow({mode}); + outerShadow.innerHTML = '<section id="outerParent" style="position: absolute; top: 50px; left: 50px;"></section>'; + + const innerHost = document.createElement('div'); + outerShadow.firstChild.appendChild(innerHost); + const innerShadow = innerHost.attachShadow({mode}); + innerShadow.innerHTML = '<div id="innerParent" style="position: relative; padding-left: 60px; padding-top: 40px;"><div id="target"></div></div>'; + const innerParent = innerShadow.getElementById('innerParent'); + + const target = innerShadow.getElementById('target'); + assert_true(innerParent instanceof HTMLDivElement); + assert_equals(target.offsetParent, innerParent); + assert_equals(target.offsetLeft, 60); + assert_equals(target.offsetTop, 40); + + outerHost.remove(); + }, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode even when nested`); +} + +testOffsetParentInNestedShadowTrees('open'); +testOffsetParentInNestedShadowTrees('closed'); + +function testOffsetParentOnElementAssignedToSlotInsideOffsetParent(mode) { + test(function () { + const host = document.createElement('div'); + host.innerHTML = '<div id="target"></div>' + container.appendChild(host); + this.add_cleanup(() => host.remove()); + const shadowRoot = host.attachShadow({mode}); + shadowRoot.innerHTML = '<div style="position: relative; padding-left: 85px; padding-top: 45px;"><slot></slot></div>'; + const target = host.querySelector('#target'); + assert_equals(target.offsetParent, container); + assert_equals(target.offsetLeft, 85); + assert_equals(target.offsetTop, 45); + }, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in a shadow tree of ${mode} mode`); +} + +testOffsetParentOnElementAssignedToSlotInsideOffsetParent('open'); +testOffsetParentOnElementAssignedToSlotInsideOffsetParent('closed'); + +function testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents(mode) { + test(function () { + const host = document.createElement('div'); + host.innerHTML = '<div id="target" style="border:solid 1px blue;">hi</div>'; + const previousBlock = document.createElement('div'); + previousBlock.style.height = '12px'; + container.append(previousBlock, host); + this.add_cleanup(() => { container.innerHTML = ''; }); + const shadowRoot = host.attachShadow({mode}); + shadowRoot.innerHTML = '<section style="position: relative; margin-left: 20px; margin-top: 100px; background: #ccc"><div style="position: absolute; top: 10px; left: 10px;"><slot></slot></div></section>'; + const target = host.querySelector('#target'); + assert_equals(target.offsetParent, container); + assert_equals(target.offsetLeft, 30); + assert_equals(target.offsetTop, 122); + }, `offsetParent must skip multiple offset parents of an element when the context object is assigned to a slot in a shadow tree of ${mode} mode`); +} + +testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('open'); +testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('closed'); + +function testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees(mode) { + test(function () { + const outerHost = document.createElement('section'); + outerHost.innerHTML = '<div id="target"></div>'; + container.appendChild(outerHost); + this.add_cleanup(() => outerHost.remove()); + const outerShadow = outerHost.attachShadow({mode}); + outerShadow.innerHTML = '<section style="position: absolute; top: 40px; left: 50px;"><div id="innerHost"><slot></slot></div></section>'; + + const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode}); + innerShadow.innerHTML = '<div style="position: absolute; top: 200px; margin-left: 100px;"><slot></slot></div>'; + + const target = outerHost.querySelector('#target'); + assert_equals(target.offsetParent, container); + assert_equals(target.offsetLeft, 150); + assert_equals(target.offsetTop, 240); + outerHost.remove(); + }, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in nested shadow trees of ${mode} mode`); +} + +testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('open'); +testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('closed'); + +function testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent(mode) { + test(function () { + const outerHost = document.createElement('section'); + container.appendChild(outerHost); + this.add_cleanup(() => outerHost.remove()); + const outerShadow = outerHost.attachShadow({mode}); + outerShadow.innerHTML = '<div id="innerHost"><div id="target"></div></div>'; + + const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode}); + innerShadow.innerHTML = '<div style="position: absolute; top: 23px; left: 24px;"><slot></slot></div>'; + + const target = outerShadow.querySelector('#target'); + assert_equals(target.offsetParent, container); + assert_equals(target.offsetLeft, 24); + assert_equals(target.offsetTop, 23); + }, `offsetParent must find the first offset parent which is a shadow-including ancestor of the context object even some shadow tree of ${mode} mode did not have any offset parent`); +} + +testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('open'); +testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('closed'); + +function testOffsetParentOnUnassignedChild(mode) { + test(function () { + const host = document.createElement('section'); + host.innerHTML = '<div id="target"></div>'; + this.add_cleanup(() => host.remove()); + container.appendChild(host); + const shadowRoot = host.attachShadow({mode}); + shadowRoot.innerHTML = '<section style="position: absolute; top: 50px; left: 50px;">content</section>'; + const target = host.querySelector('#target'); + assert_equals(target.offsetParent, null); + assert_equals(target.offsetLeft, 0); + assert_equals(target.offsetTop, 0); + }, `offsetParent must return null on a child element of a shadow host for the shadow tree in ${mode} mode which is not assigned to any slot`); +} + +testOffsetParentOnUnassignedChild('open'); +testOffsetParentOnUnassignedChild('closed'); + +function testOffsetParentOnAssignedChildNotInFlatTree(mode) { + test(function () { + const outerHost = document.createElement('section'); + outerHost.innerHTML = '<div id="target"></div>'; + container.appendChild(outerHost); + this.add_cleanup(() => outerHost.remove()); + const outerShadow = outerHost.attachShadow({mode}); + outerShadow.innerHTML = '<div id="innerHost"><div style="position: absolute; top: 50px; left: 50px;"><slot></slot></div></div>'; + + const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode}); + innerShadow.innerHTML = '<div>content</div>'; + + const target = outerHost.querySelector('#target'); + assert_equals(target.offsetParent, null); + assert_equals(target.offsetLeft, 0); + assert_equals(target.offsetTop, 0); + }, `offsetParent must return null on a child element of a shadow host for the shadow tree in ${mode} mode which is not in the flat tree`); +} + +testOffsetParentOnAssignedChildNotInFlatTree('open'); +testOffsetParentOnAssignedChildNotInFlatTree('closed'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/offsetTop-offsetLeft-across-shadow-boundaries.html b/testing/web-platform/tests/shadow-dom/offsetTop-offsetLeft-across-shadow-boundaries.html new file mode 100644 index 0000000000..27b1faa171 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/offsetTop-offsetLeft-across-shadow-boundaries.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<style> +.box { + width: 10px; + height: 10px; +} +</style> + +<div> + <template shadowrootmode=open> + <style> + .box { + width: 10px; + height: 10px; + } + </style> + <div class=box></div> + <div style="position: relative"> + <div class=box></div> + <slot></slot> + </div> + </template> + <div class=box></div> + <div id=target1 style="position: absolute" class=box></div> +</div> + +<span> + <template shadowrootmode=open> + <style> + .box { + width: 10px; + height: 10px; + } + </style> + <span class=box></span> + <span style="position: relative"> + <span class=box></span> + <slot></slot> + </span> + </template> + <span class=box></span> + <span id=target2 style="position: absolute" class=box></span> +</span> + +<div> + <template shadowrootmode=open> + <style> + .box { + width: 10px; + height: 10px; + } + </style> + <div class=box></div> + <div style="position: relative"> + <div class=box></div> + <div> + <template shadowrootmode=open> + <style> + .box { + width: 10px; + height: 10px; + } + </style> + <div class=box></div> + <div style="position: relative"> + <div class=box></div> + <slot></slot> + </div> + </template> + <slot></slot> + </div> + </div> + </template> + <div class=box></div> + <div id=target3 style="position: absolute" class=box></div> +</div> + +<script> +test(() => { + assert_equals(target1.offsetTop, 38); +}, 'Verifies that HTMLElement.offsetTop accounts for shadow boundaries.'); + +test(() => { + assert_equals(target2.offsetLeft, 8); +}, 'Verifies that HTMLElement.offsetLeft accounts for shadow boundaries.'); + +test(() => { + assert_equals(target3.offsetTop, 88); +}, 'Verifies that HTMLElement.offsetTop accounts for shadow boundaries when nested in multiple shadow roots.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/reference/empty.html b/testing/web-platform/tests/shadow-dom/reference/empty.html new file mode 100644 index 0000000000..0e76edd65b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/reference/empty.html @@ -0,0 +1 @@ +<!DOCTYPE html> diff --git a/testing/web-platform/tests/shadow-dom/resources/Document-prototype-currentScript-helper.js b/testing/web-platform/tests/shadow-dom/resources/Document-prototype-currentScript-helper.js new file mode 100644 index 0000000000..c25693cdb9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/resources/Document-prototype-currentScript-helper.js @@ -0,0 +1 @@ +executeExternalScript(); diff --git a/testing/web-platform/tests/shadow-dom/resources/event-path-test-helpers.js b/testing/web-platform/tests/shadow-dom/resources/event-path-test-helpers.js new file mode 100644 index 0000000000..f0e8ec33ff --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/resources/event-path-test-helpers.js @@ -0,0 +1,92 @@ + +function dispatchEventWithEventLog(shadow, target, event) { + var eventPath = []; + var targets = []; + var relatedTargets = []; + var pathAtTargets = []; + + var attachedNodes = []; + for (var nodeKey in shadow) { + var startingNode = shadow[nodeKey]; + for (var node = startingNode; node; node = node.parentNode) { + if (attachedNodes.indexOf(node) >= 0) + continue; + attachedNodes.push(node); + node.addEventListener(event.type, (function (event) { + eventPath.push(this.label); + relatedTargets.push(event.relatedTarget ? event.relatedTarget.label : null); + + pathAtTargets.push(event.composedPath().map(function (node) { return node.label; })); + targets.push(event.target); + }).bind(node)); + } + } + + target.dispatchEvent(event); + + return {event: event, targets: targets, eventPath: eventPath, relatedTargets: relatedTargets, pathAtTargets: pathAtTargets}; +} + +/* +-SR: ShadowRoot -S: Slot +A ------------------------------- A-SR ++ B ------------ B-SR + A1 --- A1-SR + + C + B1 --- B1-SR + A2-S + A1a + + D --- D-SR + B1a + B1b --- B1b-SR + + D1 + B1c-S + B1b1 + + B1b2 +*/ +function createFixedTestTree(mode) { + var namedNodes = {}; + + function element(name) { + var element = document.createElement(name.indexOf('-S') > 0 ? 'slot' : 'div'); + element.label = name; + namedNodes[name] = element; + for (var i = 1; i < arguments.length; i++) { + var item = arguments[i]; + if (typeof(item) == 'function') + item(element); + else + element.appendChild(item); + } + return element; + } + + function shadow(name) { + var children = []; + for (var i = 1; i < arguments.length; i++) + children.push(arguments[i]); + return function (element) { + var shadowRoot = element.attachShadow({mode: mode}); + shadowRoot.label = name; + namedNodes[name] = shadowRoot; + for (var child of children) + shadowRoot.appendChild(child); + } + } + + var host = element('A', + shadow('A-SR', + element('A1', + shadow('A1-SR', + element('A1a'))), + element('A2-S') + ), + element('B', + shadow('B-SR', + element('B1', + shadow('B1-SR', + element('B1b', + shadow('B1b-SR', + element('B1b1'), + element('B1b2'))), + element('B1c-S')), + element('B1a'))), + element('C'), + element('D', + shadow('D-SR', + element('D1'))))); + + return namedNodes; +} diff --git a/testing/web-platform/tests/shadow-dom/resources/shadow-dom-utils.js b/testing/web-platform/tests/shadow-dom/resources/shadow-dom-utils.js new file mode 100644 index 0000000000..303d1a573d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/resources/shadow-dom-utils.js @@ -0,0 +1,95 @@ +"use strict"; + +function unit(f) { + return function () { + var ctx = newContext(); + try { + f(ctx); + } finally { + cleanContext(ctx); + } + } +} + +function step_unit(f, ctx, t) { + return function () { + var done = false; + try { + f(); + done = true; + } finally { + if (done) { + t.done(); + } + cleanContext(ctx); + } + } +} + +function assert_nodelist_contents_equal_noorder(actual, expected, message) { + assert_equals(actual.length, expected.length, message); + var used = []; + for (var i = 0; i < expected.length; i++) { + used.push(false); + } + for (i = 0; i < expected.length; i++) { + var found = false; + for (var j = 0; j < actual.length; j++) { + if (used[j] == false && expected[i] == actual[j]) { + used[j] = true; + found = true; + break; + } + } + if (!found) { + assert_unreached(message + ". Fail reason: element not found: " + expected[i]); + } + } +} + +//Example taken from http://www.w3.org/TR/shadow-dom/#event-retargeting-example +function createTestMediaPlayer(d) { + d.body.innerHTML = '' + + '<div id="player">' + + '<input type="checkbox" id="outside-control">' + + '<div id="player-shadow-host">' + + '</div>' + + '</div>'; + + var playerShadowRoot = d.querySelector('#player-shadow-host').attachShadow({mode: 'open'}); + playerShadowRoot.innerHTML = '' + + '<div id="controls">' + + '<button class="play-button">PLAY</button>' + + '<div tabindex="0" id="timeline">' + + '<div id="timeline-shadow-host">' + + '</div>' + + '</div>' + + '<div class="volume-slider-container" id="volume-slider-container">' + + '<div tabindex="0" class="volume-slider" id="volume-slider">' + + '<div id="volume-shadow-host">' + + '</div>' + + '</div>' + + '</div>' + + '</div>'; + + var timeLineShadowRoot = playerShadowRoot.querySelector('#timeline-shadow-host').attachShadow({mode: 'open'}); + timeLineShadowRoot.innerHTML = '<div class="slider-thumb" id="timeline-slider-thumb"></div>'; + + var volumeShadowRoot = playerShadowRoot.querySelector('#volume-shadow-host').attachShadow({mode: 'open'}); + volumeShadowRoot.innerHTML = '<div class="slider-thumb" id="volume-slider-thumb"></div>'; + + return { + 'playerShadowRoot': playerShadowRoot, + 'timeLineShadowRoot': timeLineShadowRoot, + 'volumeShadowRoot': volumeShadowRoot + }; +} + +//FIXME This call of initKeyboardEvent works for WebKit-only. +//See https://bugs.webkit.org/show_bug.cgi?id=16735 +// and https://bugs.webkit.org/show_bug.cgi?id=13368. Add check for browser here +function fireKeyboardEvent(doc, element, key) { + var event = doc.createEvent('KeyboardEvent'); + event.initKeyboardEvent("keydown", true, true, doc.defaultView, key, 0, false, false, false, false); + element.dispatchEvent(event); +} diff --git a/testing/web-platform/tests/shadow-dom/resources/shadow-dom.js b/testing/web-platform/tests/shadow-dom/resources/shadow-dom.js new file mode 100644 index 0000000000..3a4ad39d42 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/resources/shadow-dom.js @@ -0,0 +1,163 @@ +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); + } + } +} diff --git a/testing/web-platform/tests/shadow-dom/scroll-to-the-fragment-in-shadow-tree.html b/testing/web-platform/tests/shadow-dom/scroll-to-the-fragment-in-shadow-tree.html new file mode 100644 index 0000000000..b4ecc16a67 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/scroll-to-the-fragment-in-shadow-tree.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: The indicated part of the document should not match an element inside a shadow tree</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="An element inside a shadow tree should not be the indicated part of the document even if its ID is exactly equal to the decoded fragid or its name attribute is exactly equal to the fragid"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#scroll-to-the-fragment-identifier"> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<div id="testContainer"></div> +<script> + +var tests = [ + {test: async_test('The user agent scroll to the fragment when there is an element with an ID exactly equal to the decoded fragid'), + execute: testScrollingToElementInDocumentTree.bind(this, 'div')}, + {test: async_test('The user agent scroll to the fragment when there is an anchor element with a name attribute exactly equal to the decoded fragid'), + execute: testScrollingToElementInDocumentTree.bind(this, 'a')}, + + {test: async_test('The user agent should not scroll to an element with an ID exactly equal to the decoded fragid in an open shadow tree'), + execute: testScrollingToElementInShadowTree.bind(this, 'div', 'open')}, + {test: async_test('The user agent should not scroll to an element with an ID exactly equal to the decoded fragid in a closed shadow tree'), + execute: testScrollingToElementInShadowTree.bind(this, 'div', 'closed')}, + {test: async_test('The user agent should not scroll to an anchor element with a name attribute exactly equal to the decoded fragid in an open shadow tree'), + execute: testScrollingToElementInShadowTree.bind(this, 'a', 'open')}, + {test: async_test('The user agent should not scroll to an anchor element with a name attribute exactly equal to the decoded fragid in a closed shadow tree'), + execute: testScrollingToElementInShadowTree.bind(this, 'a', 'closed')}, + + {test: async_test('The user agent should scroll to an element with an ID exactly equal to the decoded fragid in the document tree' + + ' even if there was another element with the same ID inside an open shadow tree earlier in tree order'), + execute: testScrollingToElementInDocumentTreeAfterElementInShadowTreeWithSameID.bind(this, 'div', 'open')}, + {test: async_test('The user agent should scroll to an element with an ID exactly equal to the decoded fragid in the document tree' + + ' even if there was another element with the same ID inside a closed shadow tree earlier in tree order'), + execute: testScrollingToElementInDocumentTreeAfterElementInShadowTreeWithSameID.bind(this, 'div', 'closed')}, + {test: async_test('The user agent should scroll to an anchor element with a name attribute exactly equal to the decoded fragid in the document tree' + + ' even if there was another element with the same ID inside an open shadow tree earlier in tree order'), + execute: testScrollingToElementInDocumentTreeAfterElementInShadowTreeWithSameID.bind(this, 'a', 'open')}, + {test: async_test('The user agent should scroll to an anchor element with a name attribute exactly equal to the decoded fragid in the document tree' + + ' even if there was another element with the same ID inside a closed shadow tree earlier in tree order'), + execute: testScrollingToElementInDocumentTreeAfterElementInShadowTreeWithSameID.bind(this, 'a', 'closed')}, +]; + +function executeNextTest() +{ + window.scrollTo(0, 0); + + currentFragIdSuffix++; + var nextTest = tests.shift(); + if (!nextTest) + return; + setTimeout(function () { + nextTest.execute(nextTest.test); + }, 0); +} + +var testContainer = document.getElementById('testContainer'); +var currentFragIdSuffix = 0; + +function tallElementMarkup() +{ + return '<div style="height: ' + (window.innerHeight * 2) + 'px"><a href="#fragid' + currentFragIdSuffix + '">Go to fragment</a></div>'; +} + +function targetMarkup(elementType) +{ + return elementType == 'div' ? ('<div id="fragid' + currentFragIdSuffix + '">hello</div>') : ('<a name="fragid' + currentFragIdSuffix + '">hello</a>'); +} + +function clickFirstAnchorAndRunStep(test, step) +{ + setTimeout(function () { + testContainer.querySelector('a').click(); + setTimeout(function () { + test.step(step); + testContainer.innerHTML = ''; + test.done(); + executeNextTest(); + }, 0); + }, 0); +} + +function testScrollingToElementInDocumentTree(elementType, test) +{ + test.step(function () { + testContainer.innerHTML = tallElementMarkup() + targetMarkup(elementType); + assert_equals(window.pageYOffset, 0); + }); + clickFirstAnchorAndRunStep(test, function () { + assert_not_equals(window.pageYOffset, 0); + }); +} + +function testScrollingToElementInShadowTree(elementType, mode, test) +{ + test.step(function () { + testContainer.innerHTML = tallElementMarkup() + '<div id="host"></div>'; + var host = document.querySelector('#host'); + var shadowRoot = host.attachShadow({mode: mode}); + shadowRoot.innerHTML = targetMarkup(elementType); + assert_equals(window.pageYOffset, 0); + }); + clickFirstAnchorAndRunStep(test, function () { + assert_equals(window.pageYOffset, 0); + }); +} + +function testScrollingToElementInDocumentTreeAfterElementInShadowTreeWithSameID(elementType, mode, test) +{ + test.step(function () { + testContainer.innerHTML = tallElementMarkup() + '<div id="host"></div>' + tallElementMarkup() + targetMarkup(elementType); + var host = document.querySelector('#host'); + var shadowRoot = host.attachShadow({mode: mode}); + shadowRoot.innerHTML = targetMarkup(elementType); + assert_equals(window.pageYOffset, 0); + }); + clickFirstAnchorAndRunStep(test, function () { + assert_true(window.pageYOffset > testContainer.querySelector('#host').offsetTop); + }); +} + +executeNextTest(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/selection-collapse-and-extend.tentative.html b/testing/web-platform/tests/shadow-dom/selection-collapse-and-extend.tentative.html new file mode 100644 index 0000000000..36cc036b38 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/selection-collapse-and-extend.tentative.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<body> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Selection's collapse and extend should abort only when the node's root is not connected"> +<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-collapse"> +<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-extend"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="container"></div> +<script> + +test(() => { + const host = document.createElement('div'); + container.appendChild(host); + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = '<div contenteditable><p>hello, world</p></div>'; + const textNode = shadowRoot.querySelector('p').firstChild; + getSelection().removeAllRanges(); + getSelection().collapse(textNode, 5); + const ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, textNode); + assert_equals(ranges[0].startOffset, 5); + assert_equals(ranges[0].endContainer, textNode); + assert_equals(ranges[0].endOffset, 5); + assert_true(ranges[0].collapsed); +}, 'collapse can set selection to a node inside a shadow tree'); + +test(() => { + const host = document.createElement('div'); + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = '<div contenteditable><p>hello, world</p></div>'; + const textNode = shadowRoot.querySelector('p').firstChild; + getSelection().removeAllRanges(); + getSelection().collapse(textNode, 5); + const ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 0); +}, 'collapse abort steps when called with a disconnected node inside a shadow tree'); + +test(() => { + const host = document.createElement('div'); + container.appendChild(host); + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = '<div contenteditable><p>hello, world</p></div>'; + const textNode = shadowRoot.querySelector('p').firstChild; + getSelection().removeAllRanges(); + getSelection().collapse(textNode, 5); + let ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + getSelection().extend(textNode, 11); + ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, textNode); + assert_equals(ranges[0].startOffset, 5); + assert_equals(ranges[0].endContainer, textNode); + assert_equals(ranges[0].endOffset, 11); + assert_false(ranges[0].collapsed); +}, 'extend can set selection to a node inside a shadow tree'); + +test(() => { + const host = document.createElement('div'); + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = '<div contenteditable><p>hello, world</p></div>'; + const textNode = shadowRoot.querySelector('p').firstChild; + getSelection().removeAllRanges(); + getSelection().collapse(container, 0); + getSelection().extend(textNode, 5); + const ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container); + assert_equals(ranges[0].startOffset, 0); + assert_equals(ranges[0].endContainer, container); + assert_equals(ranges[0].endOffset, 0); + assert_true(ranges[0].collapsed); +}, 'extend abort steps when called with a disconnected node inside a shadow tree'); + +container.remove(); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/shadow-dom/selection-direction.tentative.html b/testing/web-platform/tests/shadow-dom/selection-direction.tentative.html new file mode 100644 index 0000000000..3a2512dcc7 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/selection-direction.tentative.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<body> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Selection's direction should return none, forwad, or backward"> +<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getcomposedrange"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="container"></div> +<script> + +test(() => { + getSelection().removeAllRanges(); + assert_equals(getSelection().direction, 'none'); +}, 'direction returns "none" when there is no selection'); + +test(() => { + container.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(container.firstChild, 0, container.firstChild, 5); + assert_equals(getSelection().direction, 'forward'); +}, 'direction returns "forward" when there is a forward-direction selection in the document tree'); + +test(() => { + container.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(container.firstChild, 4, container.firstChild, 3); + assert_equals(getSelection().direction, 'backward'); +}, 'direction returns "backward" when there is a backward-direction selection in the document tree'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 0, shadowRoot.firstChild, 5); + assert_equals(getSelection().direction, 'forward'); +}, 'direction returns "forward" when there is a forward selection in the shadow tree'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 5, shadowRoot.firstChild, 3); + assert_equals(getSelection().direction, 'backward'); +}, 'direction returns "backward" when there is a backward selection in the shadow tree'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 2); + assert_equals(getSelection().direction, 'forward'); +}, 'direction returns "forward" when there is a forward selection that crosses shadow boundaries'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 1); + assert_equals(getSelection().direction, 'backward'); +}, 'direction returns "backward" when there is a forward selection that crosses shadow boundaries'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/selection-getComposedRanges.tentative.html b/testing/web-platform/tests/shadow-dom/selection-getComposedRanges.tentative.html new file mode 100644 index 0000000000..983328693c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/selection-getComposedRanges.tentative.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<html> +<body> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="assert" content="Selection's getComposedRanges should return a sequence of static ranges"> +<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getcomposedrange"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="container"></div> +<script> + +test(() => { + getSelection().removeAllRanges(); + assert_array_equals(getSelection().getComposedRanges(), []); +}, 'getComposedRanges returns an empty sequence when there is no selection'); + +test(() => { + container.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(container.firstChild, 0, container.firstChild, 5); + const ranges = getSelection().getComposedRanges(); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container.firstChild); + assert_equals(ranges[0].startOffset, 0); + assert_equals(ranges[0].endContainer, container.firstChild); + assert_equals(ranges[0].endOffset, 5); +}, 'getComposedRanges returns a sequence with a static range when there is a forward-direction selection in the document tree'); + +test(() => { + container.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(container.firstChild, 4, container.firstChild, 3); + const ranges = getSelection().getComposedRanges(); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container.firstChild); + assert_equals(ranges[0].startOffset, 3); + assert_equals(ranges[0].endContainer, container.firstChild); + assert_equals(ranges[0].endOffset, 4); +}, 'getComposedRanges returns a sequence with a static range when there is a backward-direction selection in the document tree'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 0, shadowRoot.firstChild, 5); + const ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, shadowRoot.firstChild); + assert_equals(ranges[0].startOffset, 0); + assert_equals(ranges[0].endContainer, shadowRoot.firstChild); + assert_equals(ranges[0].endOffset, 5); +}, 'getComposedRanges returns a sequence with a static range pointing to a shadaw tree when there is a selection in the shadow tree and the shadow tree is specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 0, shadowRoot.firstChild, 5); + const ranges = getSelection().getComposedRanges(); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container); + assert_equals(ranges[0].startOffset, 1); + assert_equals(ranges[0].endContainer, container); + assert_equals(ranges[0].endOffset, 2); +}, 'getComposedRanges returns a sequence with a static range pointing to the shadow host when there is a selection in a shadow tree and the shadow tree is not specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 2); + const ranges = getSelection().getComposedRanges(); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container); + assert_equals(ranges[0].startOffset, 1); + assert_equals(ranges[0].endContainer, container); + assert_equals(ranges[0].endOffset, 2); +}, 'getComposedRanges a sequence with a static range pointing to the shadow host when there is a forward selection that crosses shadow boundaries and the shadow tree is not specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="host"></div>b'; + const shadowRoot = host.attachShadow({mode: 'closed'}); + shadowRoot.innerHTML = 'hello, world'; + getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 2); + const ranges = getSelection().getComposedRanges(shadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, shadowRoot.firstChild); + assert_equals(ranges[0].startOffset, 7); + assert_equals(ranges[0].endContainer, container); + assert_equals(ranges[0].endOffset, 2); +}, 'getComposedRanges a sequence with a static range that crosses shadow boundaries when there is a forward selection that crosses shadow boundaries and the shadow tree is specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="outerHost"></div>b'; + const outerShadowRoot = outerHost.attachShadow({mode: 'closed'}); + outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>'; + const innerHost = outerShadowRoot.getElementById('innerHost'); + const innerShadowRoot = innerHost.attachShadow({mode: 'closed'}); + innerShadowRoot.innerHTML = 'some text'; + getSelection().setBaseAndExtent(innerShadowRoot.firstChild, 5, innerShadowRoot.firstChild, 9); + const ranges = getSelection().getComposedRanges(); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, container); + assert_equals(ranges[0].startOffset, 1); + assert_equals(ranges[0].endContainer, container); + assert_equals(ranges[0].endOffset, 2); +}, 'getComposedRanges returns a sequence with a static range pointing to the outer shadow host when there is a selection in an inner shadow tree and no shadow tree is specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="outerHost"></div>b'; + const outerShadowRoot = outerHost.attachShadow({mode: 'closed'}); + outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>'; + const innerHost = outerShadowRoot.getElementById('innerHost'); + const innerShadowRoot = innerHost.attachShadow({mode: 'closed'}); + innerShadowRoot.innerHTML = 'some text'; + getSelection().setBaseAndExtent(innerShadowRoot.firstChild, 5, innerShadowRoot.firstChild, 9); + const ranges = getSelection().getComposedRanges(innerShadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, innerShadowRoot.firstChild); + assert_equals(ranges[0].startOffset, 5); + assert_equals(ranges[0].endContainer, innerShadowRoot.firstChild); + assert_equals(ranges[0].endOffset, 9); +}, 'getComposedRanges returns a sequence with a static range pointing to the inner shadow tree when there is a selection in an inner shadow tree and the inner shadow tree is specified as an argument'); + +test(() => { + container.innerHTML = 'a<div id="outerHost"></div>b'; + const outerShadowRoot = outerHost.attachShadow({mode: 'closed'}); + outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>'; + const innerHost = outerShadowRoot.getElementById('innerHost'); + const innerShadowRoot = innerHost.attachShadow({mode: 'closed'}); + innerShadowRoot.innerHTML = 'some text'; + getSelection().setBaseAndExtent(innerShadowRoot.firstChild, 5, innerShadowRoot.firstChild, 9); + const ranges = getSelection().getComposedRanges(outerShadowRoot); + assert_equals(ranges.length, 1); + assert_equals(ranges[0].startContainer, outerShadowRoot); + assert_equals(ranges[0].startOffset, 0); + assert_equals(ranges[0].endContainer, outerShadowRoot); + assert_equals(ranges[0].endOffset, 1); +}, 'getComposedRanges returns a sequence with a static range pointing to the outer shadow tree when there is a selection in an inner shadow tree and the outer shadow tree is specified as an argument'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/shadow-root-clonable.html b/testing/web-platform/tests/shadow-dom/shadow-root-clonable.html new file mode 100644 index 0000000000..a8d4a4ca51 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/shadow-root-clonable.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<title>Shadow root clonable flag</title> +<link rel='author' href='mailto:krosylight@mozilla.com'> +<link rel='author' href='mailto:masonf@chromium.org'> +<link rel='help' href='https://dom.spec.whatwg.org/#shadowroot-clonable'> +<link rel='help' href='https://github.com/whatwg/dom/issues/1249#issuecomment-1917772229'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> + +<body> +<script> +test(() => { + const div = document.createElement("div"); + const root = div.attachShadow({ mode: "open", clonable: true }); + root.innerHTML = '<input><div><span></span></div>'; + assert_true(root.clonable, "clonable attribute"); + + const clone = div.cloneNode(true); + const clonedRoot = clone.shadowRoot; + assert_true(clonedRoot.clonable, "clone gets the same clonable state"); + assert_equals(clonedRoot.children.length, 2, "children count"); + assert_equals(clonedRoot.children[0].localName, "input", "children content"); + assert_equals(clonedRoot.children[1].firstElementChild.localName, "span", "grandchildren content"); + + const shallowClone = div.cloneNode(false); + const shallowClonedRoot = shallowClone.shadowRoot; + assert_true(shallowClonedRoot.clonable, "clone gets the same clonable state"); + assert_equals(shallowClonedRoot.children.length, 2, "shallow clone still deep-clones the shadow root"); + assert_equals(shallowClonedRoot.children[0].localName, "input", "shadow children content"); + assert_equals(shallowClonedRoot.children[1].firstElementChild.localName, "span", "shadow grandchildren content"); +}, "attachShadow with clonable: true"); + +for (const clonable of [false, undefined]) { + test(() => { + const div = document.createElement("div"); + const root = div.attachShadow({ mode: "open", clonable }); + root.appendChild(document.createElement("input")); + assert_false(root.clonable, "clonable attribute"); + + const clone = div.cloneNode(true); + assert_true(!clone.shadowRoot, "shadow should not be cloned"); + }, `attachShadow with clonable: ${clonable}`); +} + +test(() => { + const div = document.createElement("div"); + div.setHTMLUnsafe('<div><template shadowrootmode=open><input></template></div>'); + const root = div.firstElementChild.shadowRoot; + assert_true(!!root); + assert_false(root.clonable, "clonable is *not* automatically true for declarative shadow root"); + + const clone = div.cloneNode(true); + const clonedRoot = clone.firstElementChild.shadowRoot; + assert_true(!clonedRoot,'no shadow root gets cloned'); +}, "declarative shadow roots do *not* get clonable: true automatically"); + +test(() => { + const div = document.createElement("div"); + div.setHTMLUnsafe('<div><template shadowrootmode=open shadowrootclonable><input></template></div>'); + const root = div.firstElementChild.shadowRoot; + assert_true(!!root); + assert_true(root.clonable, "clonable gets added when shadowrootclonable is present"); + + const clone = div.cloneNode(true); + const clonedRoot = clone.firstElementChild.shadowRoot; + assert_true(!!clonedRoot); + assert_equals(clonedRoot.children.length, 1, "children count"); + assert_equals(clonedRoot.children[0].localName, "input", "children content"); +}, "declarative shadow roots can opt in to clonable with shadowrootclonable"); +</script> + +<template id="test"> + <div id="host"> + <template shadowrootmode=open><input></template> + </div> +</template> + +<script> +test(() => { + const template = document.querySelector('#test'); + const root = template.content.querySelector('#host').shadowRoot; + assert_true(!!root); + const clone = template.content.cloneNode(true); + const clonedRoot = clone.querySelector('#host').shadowRoot; + assert_true(!clonedRoot,'no shadow root gets cloned'); +}, "declarative shadow roots inside templates do *not* get cloned automatically"); +</script> diff --git a/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units-ref.html b/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units-ref.html new file mode 100644 index 0000000000..d86fd234ca --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units-ref.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<div style="width: 100px; height: 100px; background: green;"></div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units.html b/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units.html new file mode 100644 index 0000000000..3f73f213ad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/shadow-style-invalidation-vw-units.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<link rel=match href="shadow-style-invalidation-vw-units-ref.html"> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +</head> +<body> +<script> + +const iframe = document.createElement('iframe'); +iframe.scrolling = 'no'; +iframe.style = 'border: none; width: 50px; height: 50px; overflow: hidden'; +iframe.src = `data:text/html,<!DOCTYPE html><style>head,html,body { padding: 0; margin: 0; }</style> +<div style="width: 100vw; height: 50vw; background: green"></div> +<div style="width: 100px; height: 100px; background: red;"><div id="host"></div></div> +<script>host.attachShadow({mode: 'closed'}).innerHTML = + '<style> div { width: 100vw; height: 50vw; background: green; }</style><div></div>'; +host.getBoundingClientRect(); +</sc` + `ript>`; +iframe.onload = () => { + iframe.style.width = '100px'; + iframe.style.height = '100px'; +} +document.body.appendChild(iframe); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/slot-dir-attach-child-crash.html b/testing/web-platform/tests/shadow-dom/slot-dir-attach-child-crash.html new file mode 100644 index 0000000000..3da70e3e14 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-dir-attach-child-crash.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<link rel=author href="mailto:myid.shin@igalia.com"> +<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1309343"> + +<datalist id="move"> + <option dir="rtl"> + <select id="select"></select> + </option> +</datalist> +<svg> + <tspan id="tspan1"/><use xlink:href="#tspan1"/> +</svg> +<script> + onload = () => { + document.getElementById("select").appendChild(document.createElement("ol")); + document.getElementById("tspan1").appendChild(move); + } +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-001-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-001-ref.html new file mode 100644 index 0000000000..5161f0df95 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-001-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Child node in host are default assigned to unnamed slot, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if there are two lines of text "A", "B" below.</p> + +<div> + <div>A</div> + <div>B</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-001.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-001.html new file mode 100644 index 0000000000..e6e8747c93 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-001.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Child nodes in host are default assigned to unnamed slot, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-001-ref.html"> + +<p>Test passes if there are two lines of text "A", "B" below.</p> + +<div id="host"> + <slot id="slot1">FAIL</slot> + <div id="A">A</div> + <div id="A">B</div> +</div> + +<script> +// Both divs A and B overwrite fallback contents of default slot +const shadowRoot = host.attachShadow({ mode: "open" }); +shadowRoot.appendChild(slot1); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-002-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-002-ref.html new file mode 100644 index 0000000000..2940bcebaf --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-002-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Assigned nodes overwrite fallback contents of slots, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if there are two lines of text "A", "B" below.</p> + +<div> + <div>A</div> + <div>B</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-002.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-002.html new file mode 100644 index 0000000000..16f2dea081 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-002.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Assigned nodes overwrite fallback contents of slots, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-002-ref.html"> + +<p>Test passes if there are two lines of text "A", "B" below.</p> + +<div id="host"> + <slot id="slot1" name="slot1">FAIL</slot> + <slot id="slot2" name="slot2">FAIL</slot> + <div id="A" slot="slot1">A</div> + <div id="B" slot="slot2">B</div> +</div> + +<script> +const shadowRoot = host.attachShadow({ mode: "open" }); +shadowRoot.appendChild(slot1); +shadowRoot.appendChild(slot2); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-003-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-003-ref.html new file mode 100644 index 0000000000..ad735ec81e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-003-ref.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Modify slot fallback contents, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if there are two lines of text "SLOT1", "A" below.</p> + +<div> + <div>SLOT1</div> + <div>A</div> +</div> + +<p>Test passes if there is one line of text "C" below.</p> + +<div> + <div>C</div> +</div> + +<p>Test passes if empty.</p> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-003.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-003.html new file mode 100644 index 0000000000..ab7b2af05a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-003.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Modify slot fallback contents, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-003-ref.html"> + +<p>Test passes if there are two lines of text "SLOT1", "A" below.</p> + +<div id="host1"><slot id="slot1">SLOT1</slot></div> + +<p>Test passes if there is one line of text "C" below.</p> + +<div id="host2"><slot id="slot2"> + <div id="B">FAIL</div> + <div id="C">C</div> +</slot></div> + +<p>Test passes if empty.</p> + +<div id="host3"> + <slot id="slot3"> + <div id="D">FAIL</div> + </slot> +</div> + +<script> +// Content added to existing fallback content will be rendered +const shadowRoot1 = host1.attachShadow({ mode: "open" }); +shadowRoot1.appendChild(slot1); +const A = document.createElement('div'); +A.innerText = 'A'; +shadowRoot1.getElementById('slot1').appendChild(A); + +// Remove some content from existing slot fallback will render the leftover fallback +const shadowRoot2 = host2.attachShadow({ mode: "open" }); +shadowRoot2.appendChild(slot2); +const B = shadowRoot2.getElementById('B'); +B.remove(); + +// // Remove all content from existing slot fallback will render no fallback +const shadowRoot3 = host3.attachShadow({ mode: "open" }); +shadowRoot3.appendChild(slot3); +const D = shadowRoot3.getElementById('D'); +D.remove(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-004-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-004-ref.html new file mode 100644 index 0000000000..85d8f6ef2b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-004-ref.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Remove assigned light nodes of a slot, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if there is one line of text "SLOT1" below.</p> + +<div> + <div>SLOT1</div> +</div> + +<p>Test passes if empty.</p> + +<p>Test passes if there is one line of text "SLOT3" below.</p> + +<div> + <div>SLOT3</div> +</div> + +<p>Test passes if there is one line of text "SLOT4" below.</p> + +<div> + <div>SLOT4</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-004.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-004.html new file mode 100644 index 0000000000..b7ca7144e4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-004.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Remove assigned light nodes of a slot, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-004-ref.html"> + +<p>Test passes if there is one line of text "SLOT1" below.</p> + +<div id="host1"> + <slot id="slot1" name="slot1">SLOT1</slot> + <div id="A" slot="slot1">FAIL</div> +</div> + +<p>Test passes if empty.</p> + +<div id="host2"> + <slot id="slot2" name="slot2"></slot> + <div id="B" slot="slot2">FAIL</div> +</div> + +<p>Test passes if there is one line of text "SLOT3" below.</p> + +<div id="host3"> + <slot id="slot3" name="slot3">SLOT3</slot> + <div id="C" slot="slot3">FAIL</div> +</div> + +<p>Test passes if there is one line of text "SLOT4" below.</p> + +<div id="host4"> + <slot id="slot4" name="slot4">SLOT4</slot> + <div id="D" slot="slot4">FAIL</div> +</div> + +<script> +// Remove a slot's assigned node should show fallback content +const shadowRoot1 = host1.attachShadow({ mode: "open" }); +shadowRoot1.appendChild(slot1); +A.remove(); + +// Remove a slot's assigned node with no fallback should show nothing +const shadowRoot2 = host2.attachShadow({ mode: "open" }); +shadowRoot2.appendChild(slot2); +B.remove(); + +// Remove the slot attribute to an assigned node should show fallback content +const shadowRoot3 = host3.attachShadow({ mode: "open" }); +shadowRoot3.appendChild(slot3); +C.removeAttribute('slot'); + +// Change the slot attribute to an assigned node to an unknown slot +// should show fallback content +const shadowRoot4 = host4.attachShadow({ mode: "open" }); +shadowRoot4.appendChild(slot4); +D.setAttribute('slot', 'invalid'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-005-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-005-ref.html new file mode 100644 index 0000000000..23bfc2ca53 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-005-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Remove slot with assigned node, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if empty.</p> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-005.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-005.html new file mode 100644 index 0000000000..37e383ac8c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-005.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Remove slot with assigned node, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-005-ref.html"> + +<p>Test passes if empty.</p> + +<div id="host"> + <slot id="slot1" name="slot1">FAIL</slot> + <div id="A" slot="slot1">FAIL</div> +</div> + +<script> +const shadowRoot = host.attachShadow({ mode: "open" }); +shadowRoot.appendChild(slot1); +const s1 = shadowRoot.getElementById('slot1'); +s1.remove(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-006-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-006-ref.html new file mode 100644 index 0000000000..85d53bf53b --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-006-ref.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Imperatively assigned node overwrites fallback contents, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> + +<p>Test passes if there are two lines of text "B", "A" below.</p> + +<div> + <div>B</div> + <div>A</div> +</div> + +<p>Test passes if there are two lines of text "D", "C" below.</p> + +<div> + <div>D</div> + <div>C</div> +</div> + +<p>Test passes if there are two lines of text "F", "E" below.</p> + +<div> + <div>F</div> + <div>E</div> +</div> + +<p>Test passes if there is one line of text "SLOT4" below.</p> + +<div> + <div>SLOT4</div> +</div> + +<p>Test passes if there is one line of text "SLOT5" below.</p> + +<div> + <div>SLOT5</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-006.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-006.html new file mode 100644 index 0000000000..4708ee7b44 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-006.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Imperatively assigned node overwrites fallback contents, when dynamically created."> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-006-ref.html"> + +<p>Test passes if there are two lines of text "B", "A" below.</p> + +<div id="host1"> + <slot id="slot1" name="slot1">FAIL</slot> +</div> +<div id="A">A</div> +<div id="B">B</div> + +<p>Test passes if there are two lines of text "D", "C" below.</p> + +<div id="host2"> + <slot id="slot2" name="slot2">FAIL</slot> +</div> +<div id="C">C</div> +<div id="D">D</div> + +<p>Test passes if there are two lines of text "F", "E" below.</p> + +<div id="host3"> + <slot id="slot3" name="slot3">FAIL</slot> +</div> +<div id="E">E</div> +<div id="F">F</div> + +<p>Test passes if there is one line of text "SLOT4" below.</p> + +<div id="host4"> + <slot id="slot4" name="slot4">SLOT4</slot> +</div> +<div id="G">FAIL</div> +<div id="H">FAIL</div> + +<p>Test passes if there is one line of text "SLOT5" below.</p> + +<div id="host5"> + <slot id="slot5" name="slot5">FAIL</slot> +</div> +<div id="I">FAIL</div> +<div id="J">FAIL</div> + +<script> +/* +1. Append nodes to document. +2. Assign nodes to slot. +*/ +const shadowRoot1 = host1.attachShadow({ mode: "open", slotAssignment: 'manual' }); +shadowRoot1.appendChild(slot1); +const s1 = shadowRoot1.getElementById('slot1'); +host1.append(A, B); +s1.assign(B, A); + +/* +1. Assign nodes to slot. +2. Append nodes to document. +*/ +const shadowRoot2 = host2.attachShadow({ mode: "open", slotAssignment: 'manual' }); +shadowRoot2.appendChild(slot2); +const s2 = shadowRoot2.getElementById('slot2'); +s2.assign(D, C); +host2.append(C, D); + +/* +1. Assign nodes to slot. +2. Change the fallback content. +3. Append nodes to document. +*/ +const shadowRoot3 = host3.attachShadow({ mode: "open", slotAssignment: 'manual' }); +shadowRoot3.appendChild(slot3); +const s3 = shadowRoot3.getElementById('slot3'); +s3.assign(F, E); +s3.innerText = 'FAIL'; +host3.append(E, F); + +/* +1. Append nodes to document. +2. Assign nodes to slot. +3. Remove nodes from document. +*/ +const shadowRoot4 = host4.attachShadow({ mode: "open", slotAssignment: 'manual' }); +shadowRoot4.appendChild(slot4); +const s4 = shadowRoot4.getElementById('slot4'); +host4.append(G, H); +s4.assign(H, G); +G.remove(); +H.remove(); + +/* +1. Append nodes to document. +2. Assign nodes to slot. +3. Change the fallback content. +4. Remove nodes from document. +*/ +const shadowRoot5 = host5.attachShadow({ mode: "open", slotAssignment: 'manual' }); +shadowRoot5.appendChild(slot5); +const s5 = shadowRoot5.getElementById('slot5'); +host5.append(I, J); +s5.assign(J, I); +s5.innerText = 'SLOT5'; +I.remove(); +J.remove(); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-007-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-007-ref.html new file mode 100644 index 0000000000..992b495273 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-007-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Basic slot fallback content, when dynamically created"> +<title>Shadow DOM: Slots and fallback contents</title> + +<div> + <div>B</div> + <div>E</div> + <div>SLOT2</div> + <div>C</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-007.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-007.html new file mode 100644 index 0000000000..1647111392 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-007.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Basic slot fallback content, when dynamically created"> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-007-ref.html"> + +<div id="host">FAIL + <slot id="slot1" name="slot1">FAIL</slot> + <slot id="slot2" name="slot2">SLOT2</slot> + <slot id="slot3" name="slot3"></slot> + <slot id="slot4" name="slot4"></slot> + + <div id="A">FAIL</div> + <div id="B" slot="slot1">B</div> + <div id="C" slot="slot3">C</div> + <div id="D" slot="slot-DNE">FAIL</div> + <div id="E" slot="slot1">E</div> + <div id="F">FAIL</div> +</div> + +<script> +const shadowRoot = host.attachShadow({ mode: "open" }); +shadowRoot.appendChild(slot1); +shadowRoot.appendChild(slot2); +shadowRoot.appendChild(slot3); +shadowRoot.appendChild(slot4); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-008-ref.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-008-ref.html new file mode 100644 index 0000000000..13a3d6b6c9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-008-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Complex slot fallback content, when dynamically created"> +<title>Shadow DOM: Slots and fallback contents</title> + +<div> + <div>A</div> + <div>D</div> + <div>B SLOT2 SLOT5</div> + <div>C</div> +</div> diff --git a/testing/web-platform/tests/shadow-dom/slot-fallback-content-008.html b/testing/web-platform/tests/shadow-dom/slot-fallback-content-008.html new file mode 100644 index 0000000000..3f93620bba --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slot-fallback-content-008.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8" > +<meta name="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<meta name="assert" content="Complex slot fallback content, when dynamically created"> +<title>Shadow DOM: Slots and fallback contents</title> +<link rel="match" href="slot-fallback-content-008-ref.html"> + +<div id="host">FAIL + <div id="host2">FAIL + <div id="A">A + <slot id="slot1" name="slot1">FAIL</slot> + </div> + <div id="B">B + <slot id="slot2" name="slot2">SLOT2 + <slot id="slot3" name="slot3">SLOT3</slot> + </slot> + </div> + <slot id="slot4" name="slot4" slot="slot1">FAIL</slot> + <slot id="slot5" name="slot5" slot="slot3">SLOT5 + <div id="C">C</div> + </slot> + </div> + <div id="D" slot="slot4">D</div> +</div> + +<script> +const shadowRoot2 = host2.attachShadow({ mode: "open" }); +shadowRoot2.appendChild(A); +shadowRoot2.appendChild(B); + +const shadowRoot = host.attachShadow({ mode: "open" }); +shadowRoot.appendChild(host2); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slotchange-customelements.html b/testing/web-platform/tests/shadow-dom/slotchange-customelements.html new file mode 100644 index 0000000000..b0cf93277d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slotchange-customelements.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: slotchange customelements</title> +<meta name="author" title="Surma" href="mailto:surma@google.com"> +<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<slots-in-constructor id="constructor-upgrade"><div></div></slots-in-constructor> +<slots-in-callback id="callback-upgrade"><div></div></slots-in-callback> +<script> +var calls = []; +class SlotsInConstructor extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: 'open'}); + this.shadowRoot.innerHTML = '<slot></slot>'; + var slot = this.shadowRoot.children[0]; + slot.addEventListener('slotchange', function() { + calls.push(this.id); + }.bind(this)); + } +} +customElements.define('slots-in-constructor', SlotsInConstructor); +class SlotsInCallback extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.attachShadow({mode: 'open'}); + this.shadowRoot.innerHTML = '<slot></slot>'; + var slot = this.shadowRoot.children[0]; + slot.addEventListener('slotchange', function() { + calls.push(this.id); + }.bind(this)); + } +} +customElements.define('slots-in-callback', SlotsInCallback); +</script> +<slots-in-constructor id="constructor-parser"><div></div></slots-in-constructor> +<slots-in-callback id="callback-parser"><div></div></slots-in-callback> +<script> +test(function () { + assert_true(calls.includes("constructor-parser")); + assert_true(calls.includes("callback-parser")); + assert_true(calls.includes("constructor-upgrade")); + assert_true(calls.includes("callback-upgrade")); +}, 'slotchange must fire on initialization of custom elements with slotted children'); +done(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/slotchange-event.html b/testing/web-platform/tests/shadow-dom/slotchange-event.html new file mode 100644 index 0000000000..f1d06297ff --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slotchange-event.html @@ -0,0 +1,602 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: slotchange event</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function treeName(mode, connectedToDocument) +{ + return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root ' + + (connectedToDocument ? '' : ' not') + ' in a document'; +} + +function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a default slot element inside ' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + + slot.addEventListener('slotchange', function (event) { + if (event.isFakeEvent) + return; + + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + + host.appendChild(document.createElement('span')); + host.appendChild(document.createElement('b')); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); + + host.appendChild(document.createElement('i')); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); + + host.appendChild(document.createTextNode('hello')); + + var fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + slot.dispatchEvent(fakeEvent); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' + + ' event if there was a synthetic slotchange event fired'); + }); + test.done(); + }, 1); + }, 1); + }, 1); +} + +testAppendingSpanToShadowRootWithDefaultSlot('open', true); +testAppendingSpanToShadowRootWithDefaultSlot('closed', true); +testAppendingSpanToShadowRootWithDefaultSlot('open', false); +testAppendingSpanToShadowRootWithDefaultSlot('closed', false); + +function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a named slot element inside' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.name = 'someSlot'; + + slot.addEventListener('slotchange', function (event) { + if (event.isFakeEvent) + return; + + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + var b = document.createElement('b'); + b.slot = 'someSlot'; + host.appendChild(b); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); + + var i = document.createElement('i'); + i.slot = 'someSlot'; + host.appendChild(i); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); + + var em = document.createElement('em'); + em.slot = 'someSlot'; + host.appendChild(em); + + var fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + slot.dispatchEvent(fakeEvent); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' + + ' event if there was a synthetic slotchange event fired'); + }); + test.done(); + }, 1); + + }, 1); + }, 1); +} + +testAppendingSpanToShadowRootWithNamedSlot('open', true); +testAppendingSpanToShadowRootWithNamedSlot('closed', true); +testAppendingSpanToShadowRootWithNamedSlot('open', false); +testAppendingSpanToShadowRootWithNamedSlot('closed', false); + +function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument) +{ + var test = async_test('slotchange event must not fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when another slot\'s assigned nodes change'); + + var host; + var defaultSlotEventCount = 0; + var namedSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + var defaultSlot = document.createElement('slot'); + defaultSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); + }); + defaultSlotEventCount++; + }); + + var namedSlot = document.createElement('slot'); + namedSlot.name = 'slotName'; + namedSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); + }); + namedSlotEventCount++; + }); + + shadowRoot.appendChild(defaultSlot); + shadowRoot.appendChild(namedSlot); + + host.appendChild(document.createElement('span')); + + assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a named slot after the assigned nodes change on a default slot'); + + var span = document.createElement('span'); + span.slot = 'slotName'; + host.appendChild(span); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must not be fired on a default slot after the assigned nodes change on a named slot'); + assert_equals(namedSlotEventCount, 1, + 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); + }); + test.done(); + }, 1); + }, 1); +} + +testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true); +testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true); +testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false); +testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false); + +function testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted' + + ' and must not fire when the shadow host was mutated after the slot was removed inside ' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + eventCount++; + }); + + host.appendChild(document.createElement('span')); + shadowRoot.appendChild(slot); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, + 'slotchange must be fired on a slot element if there is assigned nodes when the slot was inserted'); + host.removeChild(host.firstChild); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, + 'slotchange must be fired after the assigned nodes change on a slot while the slot element was in the tree'); + slot.parentNode.removeChild(slot); + host.appendChild(document.createElement('span')); + }); + + setTimeout(function () { + assert_equals(eventCount, 2, + 'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed'); + test.done(); + }, 1); + }, 1); + }, 1); +} + +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', true); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', true); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', false); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', false); + +function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' even if the slot was removed immediately after the assigned nodes were mutated'); + + var host; + var slot; + var anotherSlot; + var slotEventCount = 0; + var anotherSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.name = 'someSlot'; + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + slotEventCount++; + }); + + anotherSlot = document.createElement('slot'); + anotherSlot.name = 'someSlot'; + anotherSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element'); + }); + anotherSlotEventCount++; + }); + + shadowRoot.appendChild(slot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + shadowRoot.removeChild(slot); + shadowRoot.appendChild(anotherSlot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + shadowRoot.removeChild(anotherSlot); + + assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(slotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); + assert_equals(anotherSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); + }); + test.done(); + }, 1); +} + +testSlotchangeFiresOnTransientlyPresentSlot('open', true); +testSlotchangeFiresOnTransientlyPresentSlot('closed', true); +testSlotchangeFiresOnTransientlyPresentSlot('open', false); +testSlotchangeFiresOnTransientlyPresentSlot('closed', false); + +function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when innerHTML modifies the children of the shadow host'); + + var host; + var defaultSlot; + var namedSlot; + var defaultSlotEventCount = 0; + var namedSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + defaultSlot = document.createElement('slot'); + defaultSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); + }); + defaultSlotEventCount++; + }); + + namedSlot = document.createElement('slot'); + namedSlot.name = 'someSlot'; + namedSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); + }); + namedSlotEventCount++; + }); + shadowRoot.appendChild(namedSlot); + shadowRoot.appendChild(defaultSlot); + host.innerHTML = 'foo <b>bar</b>'; + + assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = 'baz'; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 2, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = ''; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 3, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = '<b slot="someSlot">content</b>'; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 3, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + assert_equals(namedSlotEventCount, 1, + 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); + host.innerHTML = ''; + }); + setTimeout(function () { + test.step(function () { + // FIXME: This test would fail in the current implementation because we can't tell + // whether a text node was removed in AllChildrenRemoved or not. +// assert_equals(defaultSlotEventCount, 3, +// 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + assert_equals(namedSlotEventCount, 2, + 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); + }); + test.done(); + }, 1); + }, 1); + }, 1); + }, 1); + }, 1); +} + +testSlotchangeFiresOnInnerHTML('open', true); +testSlotchangeFiresOnInnerHTML('closed', true); +testSlotchangeFiresOnInnerHTML('open', false); +testSlotchangeFiresOnInnerHTML('closed', false); + +function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when nested slots\'s contents change'); + + var outerHost; + var innerHost; + var outerSlot; + var innerSlot; + var outerSlotEventCount = 0; + var innerSlotEventCount = 0; + + test.step(function () { + outerHost = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(outerHost); + + var outerShadow = outerHost.attachShadow({'mode': mode}); + outerShadow.appendChild(document.createElement('span')); + outerSlot = document.createElement('slot'); + outerSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element'); + }); + outerSlotEventCount++; + }); + + innerHost = document.createElement('div'); + innerHost.appendChild(outerSlot); + outerShadow.appendChild(innerHost); + + var innerShadow = innerHost.attachShadow({'mode': mode}); + innerShadow.appendChild(document.createElement('span')); + innerSlot = document.createElement('slot'); + innerSlot.addEventListener('slotchange', function (event) { + event.stopPropagation(); + test.step(function () { + if (innerSlotEventCount === 0) { + assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the inner slot element at 1st slotchange'); + } else if (innerSlotEventCount === 1) { + assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the outer slot element at 2nd sltochange'); + } + }); + innerSlotEventCount++; + }); + innerShadow.appendChild(innerSlot); + + outerHost.appendChild(document.createElement('span')); + + assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(outerSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed'); + assert_equals(innerSlotEventCount, 2, + 'slotchange must be fired on a slot element and must bubble'); + }); + test.done(); + }, 1); +} + +testSlotchangeFiresWhenNestedSlotChange('open', true); +testSlotchangeFiresWhenNestedSlotChange('closed', true); +testSlotchangeFiresWhenNestedSlotChange('open', false); +testSlotchangeFiresWhenNestedSlotChange('closed', false); + +function testSlotchangeFiresAtEndOfMicroTask(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire at the end of current microtask after mutation observers are invoked inside ' + + treeName(mode, connectedToDocument) + ' when slots\'s contents change'); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + }); + + var element = document.createElement('div'); + + new MutationObserver(function () { + test.step(function () { + assert_equals(eventCount, 0, 'slotchange event must not be fired before mutation records are delivered'); + }); + host.appendChild(document.createElement('span')); + element.setAttribute('title', 'bar'); + }).observe(element, {attributes: true, attributeFilter: ['id']}); + + new MutationObserver(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange event must be fired during a single compound microtask'); + }); + }).observe(element, {attributes: true, attributeFilter: ['title']}); + + element.setAttribute('id', 'foo'); + host.appendChild(document.createElement('div')); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, + 'a distinct slotchange event must be enqueued for changes made during a mutation observer delivery'); + }); + test.done(); + }, 0); +} + +testSlotchangeFiresAtEndOfMicroTask('open', true); +testSlotchangeFiresAtEndOfMicroTask('closed', true); +testSlotchangeFiresAtEndOfMicroTask('open', false); +testSlotchangeFiresAtEndOfMicroTask('closed', false); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/slotchange.html b/testing/web-platform/tests/shadow-dom/slotchange.html new file mode 100644 index 0000000000..9c4b81a156 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slotchange.html @@ -0,0 +1,305 @@ +<!DOCTYPE html> +<title>Shadow DOM: slotchange Events</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="host1"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +function arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +} +function doneIfSlotChange(slots, expectedAssignedNodes, test) { + let matched = Array(slots.length).fill(false); + for (let i=0; i<slots.length; i++) { + slots[i].addEventListener('slotchange', test.step_func((e) => { + matched[i] = arraysEqual(slots[i].assignedNodes(), expectedAssignedNodes[i]); + if (matched.every(v => v)) { + test.done(); + } + })) + } +} + +async_test((test) => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + let d1 = document.createElement('div'); + d1.setAttribute('slot', 'slot1'); + + doneIfSlotChange([n.s1], [[n.c1, d1]], test); + + n.host1.appendChild(d1); +}, 'slotchange event: Append a child to a host.'); + +async_test((test) => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + doneIfSlotChange([n.s1], [[]], test); + + n.c1.remove(); +}, 'slotchange event: Remove a child from a host.'); + +async_test((test) => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + n.c1.remove(); + + doneIfSlotChange([n.s1], [[]], test); +}, 'slotchange event: Remove a child before adding an event listener.'); + +async_test((test) => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + doneIfSlotChange([n.s1], [[]], test); + + n.c1.setAttribute('slot', 'slot-none'); +}, 'slotchange event: Change slot= attribute to make it un-assigned.'); + +async_test((test) => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + doneIfSlotChange([n.s1], [[]], test); + + n.s1.setAttribute('name', 'slot-none'); +}, 'slotchange event: Change slot\'s name= attribute so that none is assigned.'); +</script> + +<div id="test2"> + <div id="host1"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c2" slot="slot2"></div> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test2); + removeWhiteSpaceOnlyTextNodes(n.test2); + + doneIfSlotChange([n.s1], [[n.c2]], test); + + n.c2.setAttribute('slot', 'slot1'); +}, 'slotchange event: Change slot= attribute to make it assigned.'); + +async_test((test) => { + let n = createTestTree(test2); + removeWhiteSpaceOnlyTextNodes(n.test2); + + doneIfSlotChange([n.s1], [[n.c2]], test); + + n.s1.setAttribute('name', 'slot2'); +}, 'slotchange event: Change slot\'s name= attribute so that a node is assigned to the slot.'); +</script> + +<div id="test_fallback"> + <div id="host1"> + <template data-mode="open"> + <slot id="s1"></slot> + </template> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test_fallback); + removeWhiteSpaceOnlyTextNodes(n.test_fallback); + + doneIfSlotChange([n.s1], [[]], test); + + n.s1.appendChild(document.createElement('div')); +}, 'slotchange event: Change fallback content - assignedNodes still empty.'); +</script> + +<div id="test_fallback2"> + <div id="host1"> + <template data-mode="open"> + <slot id="s1"> + <div id="f1"></div> + </slot> + </template> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test_fallback2); + removeWhiteSpaceOnlyTextNodes(n.test_fallback2); + + doneIfSlotChange([n.s1], [[]], test); + + n.f1.remove(); +}, 'slotchange event: Remove a fallback content - assignedNodes still empty.'); +</script> + +<div id="test_fallback3"> + <div id="host1"> + <template data-mode="open"> + <slot id="s2"> + <slot id="s1"> + <div id="f1"></div> + </slot> + </slot> + </template> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test_fallback3); + removeWhiteSpaceOnlyTextNodes(n.test_fallback3); + + doneIfSlotChange([n.s1, n.s2], [[],[]], test); + + n.s1.appendChild(document.createElement('div')); +}, 'slotchange event: Add a fallback content to nested slots - assignedNodes still empty.'); + +async_test((test) => { + let n = createTestTree(test_fallback3); + removeWhiteSpaceOnlyTextNodes(n.test_fallback3); + + doneIfSlotChange([n.s1, n.s2], [[],[]], test); + + n.f1.remove(); +}, 'slotchange event: Remove a fallback content from nested slots - assignedNodes still empty.'); +</script> + +<div id="test3"> + <div id="host1"> + <template id="shadowroot" data-mode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test3); + removeWhiteSpaceOnlyTextNodes(n.test3); + + doneIfSlotChange([n.s1], [[]], test); + + let slot = document.createElement('slot'); + slot.setAttribute('name', 'slot1'); + n.shadowroot.insertBefore(slot, n.s1); +}, "slotchange event: Insert a slot before an existing slot."); +</script> + +<div id="test4"> + <div id="host1"> + <template id="shadowroot" data-mode="open"> + <slot id="s1" name="slot1"></slot> + <slot id="s2" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test4); + removeWhiteSpaceOnlyTextNodes(n.test4); + + doneIfSlotChange([n.s2], [[n.c1]], test); + + n.s1.remove(); +}, "slotchange event: Remove a preceding slot."); +</script> + +<div id="test5"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + doneIfSlotChange([n.s1, n.s2], [[],[n.s1]], test); + + n.c1.remove(); +}, "slotchange event: A slot is assigned to another slot."); +</script> + +<div id="test6"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test6); + removeWhiteSpaceOnlyTextNodes(n.test6); + + doneIfSlotChange([n.s2], [[]], test); + + n.s1.remove(); +}, "slotchange event: Slotchange should be fired if assigned nodes are changed."); +</script> + +<div id="test7"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + </div> +</div> + +<script> +async_test((test) => { + let n = createTestTree(test7); + removeWhiteSpaceOnlyTextNodes(n.test7); + + let d1 = document.createElement('div'); + d1.setAttribute('slot', 'slot1'); + doneIfSlotChange([n.s1, n.s2], [[d1],[n.s1]], test); + + n.host1.appendChild(d1); +}, "slotchange event: Child content is added to nested slots."); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/shadow-dom/slots-fallback-in-document.html b/testing/web-platform/tests/shadow-dom/slots-fallback-in-document.html new file mode 100644 index 0000000000..846f3e03a0 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slots-fallback-in-document.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>Shadow DOM: Slots and fallback contents in Document tree</title> +<meta name="author" title="Takayoshi Kochi" href="mailto:kochi@google.com"> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="host"> + <template data-mode="open"> + <slot id="innerSlot"></slot> + </template> + <slot id="slot"><div id="fallback">This is fallback content</div></slot> + </div> +</div> + +<script> +'use strict'; + +let n1 = createTestTree(test1); +removeWhiteSpaceOnlyTextNodes(n1.test1); + +test(() => { + assert_array_equals(n1.innerSlot.assignedNodes(), [n1.slot]); + assert_array_equals(n1.innerSlot.assignedNodes({ flatten: true }), [n1.slot]); +}, 'Children of a slot in a document tree should not be counted in flattened ' + + 'assigned nodes.'); +</script> + +<div id="test2"> + <div id="host"> + <template data-mode="open"> + <div id="innerHost"> + <template data-mode="open"> + <slot id="innerSlot"></slot> + </template> + <slot id="slot"><div id="fallback">This is fallback content</div></slot> + </div> + </template> + </div> +</div> + +<script> +'use strict'; + +let n2 = createTestTree(test2); +removeWhiteSpaceOnlyTextNodes(n2.test2); + +test(() => { + assert_array_equals(n2.innerSlot.assignedNodes(), [n2.slot]); + assert_array_equals(n2.innerSlot.assignedNodes({ flatten: true }), + [n2.fallback]); +}, 'Slot fallback content in shadow tree should be counted in flattened ' + + 'assigned nodes.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/slots-fallback.html b/testing/web-platform/tests/shadow-dom/slots-fallback.html new file mode 100644 index 0000000000..e705e18f4a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slots-fallback.html @@ -0,0 +1,253 @@ +<!DOCTYPE html> +<title>Shadow DOM: Slots and fallback contents</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test1"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"> + <div id="f1"></div> + </slot> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test1); + removeWhiteSpaceOnlyTextNodes(n.test1); + + assert_equals(n.f1.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.f1]); +}, 'Slots fallback: Basic.'); + +test(() => { + let n = createTestTree(test1); + + assert_array_equals(n.s1.assignedElements(), []); + assert_array_equals(n.s1.assignedElements({ flatten: true }), [n.f1]); +}, 'Slots fallback: Basic, elements only.'); +</script> + +<div id="test2"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"> + <slot id="s2" name="slot2"> + <div id="f1"></div> + </slot> + </slot> + </template> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test2); + removeWhiteSpaceOnlyTextNodes(n.test2); + + assert_equals(n.f1.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_array_equals(n.s2.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.f1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.f1]); +}, 'Slots fallback: Slots in Slots.'); + +test(() => { + let n = createTestTree(test2); + + assert_array_equals(n.s1.assignedElements(), []); + assert_array_equals(n.s2.assignedElements(), []); + + assert_array_equals(n.s1.assignedElements({ flatten: true }), [n.f1]); + assert_array_equals(n.s2.assignedElements({ flatten: true }), [n.f1]); +}, 'Slots fallback: Slots in Slots, elements only.'); +</script> + +<div id="test3"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"> + <slot id="s2" name="slot2"> + <div id="f1"></div> + </slot> + </slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test3); + removeWhiteSpaceOnlyTextNodes(n.test3); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.f1.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.f1]); +}, 'Slots fallback: Fallback contents should not be used if a node is assigned.'); +</script> + +<div id="test4"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"> + <slot id="s2" name="slot2"> + <div id="f1"></div> + </slot> + </slot> + </template> + <div id="c1" slot="slot2"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test4); + removeWhiteSpaceOnlyTextNodes(n.test4); + + assert_equals(n.c1.assignedSlot, n.s2); + assert_equals(n.f1.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_array_equals(n.s2.assignedNodes(), [n.c1]); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); +}, 'Slots fallback: Slots in Slots: Assigned nodes should be used as fallback contents of another slot'); +</script> + +<div id="test5"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s4" name="slot4"> + <slot id="s3" name="slot3"> + <div id="f3"></div> + </slot> + <div id="f4"></div> + </slot> + </template> + <slot id="s2" name="slot2" slot="slot3"> + <slot id="s1" name="slot1"> + <div id="f1"></div> + </slot> + <div id="f2"></div> + </slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), []); + assert_array_equals(n.s3.assignedNodes(), [n.s2]); + assert_array_equals(n.s4.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1, n.f2]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.c1, n.f2]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [n.c1, n.f2, n.f4]); +}, 'Slots fallback: Complex case.'); + +test(() => { + let n = createTestTree(test5); + + assert_array_equals(n.s1.assignedElements(), [n.c1]); + assert_array_equals(n.s2.assignedElements(), []); + assert_array_equals(n.s3.assignedElements(), [n.s2]); + assert_array_equals(n.s4.assignedElements(), []); + + assert_array_equals(n.s1.assignedElements({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedElements({ flatten: true }), [n.c1, n.f2]); + assert_array_equals(n.s3.assignedElements({ flatten: true }), [n.c1, n.f2]); + assert_array_equals(n.s4.assignedElements({ flatten: true }), [n.c1, n.f2, n.f4]); +}, 'Slots fallback: Complex case, elements only.'); + +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + let d1 = document.createElement('div'); + n.s2.appendChild(d1); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1, n.f2, d1]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.c1, n.f2, d1]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [n.c1, n.f2, d1, n.f4]); +}, 'Slots fallback: Mutation. Append fallback contents.'); + +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + n.f2.remove(); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [n.c1, n.f4]); +}, 'Slots fallback: Mutation. Remove fallback contents.'); + +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + let d2 = document.createElement('div'); + d2.setAttribute('slot', 'slot2'); + n.host1.appendChild(d2); + + assert_array_equals(n.s2.assignedNodes(), [d2]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [d2]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [d2]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [d2, n.f4]); +}, 'Slots fallback: Mutation. Assign a node to a slot so that fallback contens are no longer used.'); + +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + n.c1.remove(); + + assert_array_equals(n.s1.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.f1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.f1, n.f2]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.f1, n.f2]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [n.f1, n.f2, n.f4]); +}, 'Slots fallback: Mutation. Remove an assigned node from a slot so that fallback contens will be used.'); + +test(() => { + let n = createTestTree(test5); + removeWhiteSpaceOnlyTextNodes(n.test5); + + n.s1.remove(); + + assert_array_equals(n.s1.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [], + 'fall back contents should be empty because s1 is not in a shadow tree.'); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.f2]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.f2]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), [n.f2, n.f4]); +}, 'Slots fallback: Mutation. Remove a slot which is a fallback content of another slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/slots-outside-shadow-dom.html b/testing/web-platform/tests/shadow-dom/slots-outside-shadow-dom.html new file mode 100644 index 0000000000..a6fa2b27f7 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slots-outside-shadow-dom.html @@ -0,0 +1,16 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id="host1"><slot>foo</slot></div> +<script> + +test(() => { + var host1 = document.getElementById("host1"); + var sr = host1.attachShadow({mode: "open"}); + sr.innerHTML = "<slot></slot>"; + assert_array_equals(sr.firstChild.assignedNodes({ flatten: true }), [host1.firstChild]); +}, "Light DOM slot element should be in flattened assignedNodes"); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/slots.html b/testing/web-platform/tests/shadow-dom/slots.html new file mode 100644 index 0000000000..948d389298 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slots.html @@ -0,0 +1,526 @@ +<!DOCTYPE html> +<title>Shadow DOM: Slots and assignments</title> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/shadow-dom.js"></script> + +<div id="test_basic"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_basic); + removeWhiteSpaceOnlyTextNodes(n.test_basic); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_array_equals(n.s1.assignedNodes(), [n.c1]); +}, 'Slots: Basic.'); + +test(() => { + let n = createTestTree(test_basic); + + assert_array_equals(n.s1.assignedElements(), [n.c1]); +}, 'Slots: Basic, elements only.'); +</script> + +<div id="test_basic_closed"> + <div id="host"> + <template data-mode="closed"> + <slot id="s1" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_basic_closed); + removeWhiteSpaceOnlyTextNodes(n.test_basic_closed); + + assert_equals(n.c1.assignedSlot, null); + assert_array_equals(n.s1.assignedNodes(), [n.c1]); +}, 'Slots: Slots in closed.'); + +test(() => { + let n = createTestTree(test_basic_closed); + + assert_array_equals(n.s1.assignedElements(), [n.c1]); +}, 'Slots: Slots in closed, elements only.'); +</script> + +<div id="test_slot_not_in_shadow"> + <slot id="s1"></slot> +</div> + +<script> +test(() => { + let n = createTestTree(test_slot_not_in_shadow); + removeWhiteSpaceOnlyTextNodes(n.test_slot_not_in_shadow); + + assert_array_equals(n.s1.assignedNodes(), []); +}, 'Slots: Slots not in a shadow tree.'); + +test(() => { + let n = createTestTree(test_slot_not_in_shadow); + + assert_array_equals(n.s1.assignedElements(), []); +}, 'Slots: Slots not in a shadow tree, elements only.'); +</script> + +<div id="test_slot_not_in_shadow_2"> + <slot id="s1"> + <div id="c1"></div> + </slot> + <slot id="s2"> + <div id="c2"></div> + <slot id="s3"> + <div id="c3_1"></div> + <div id="c3_2"></div> + </slot> + </slot> +</div> + +<script> +test(() => { + let n = createTestTree(test_slot_not_in_shadow_2); + removeWhiteSpaceOnlyTextNodes(n.test_slot_not_in_shadow_2); + + assert_equals(n.c1.assignedSlot, null); + assert_equals(n.c2.assignedSlot, null); + assert_equals(n.c3_1.assignedSlot, null); + assert_equals(n.c3_2.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_array_equals(n.s2.assignedNodes(), []); + assert_array_equals(n.s3.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), []); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), []); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), []); +}, 'Slots: Distributed nodes for Slots not in a shadow tree.'); +</script> + +<div id="test_slot_name_matching"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + <slot id="s2" name="slot2"></slot> + <slot id="s3" name="xxx"></slot> + </template> + <div id="c1" slot="slot1"></div> + <div id="c2" slot="slot2"></div> + <div id="c3" slot="yyy"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_slot_name_matching); + removeWhiteSpaceOnlyTextNodes(n.test_slot_name_matching); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.c2.assignedSlot, n.s2); + assert_equals(n.c3.assignedSlot, null); +}, 'Slots: Name matching'); +</script> + +<div id="test_no_direct_host_child"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + <slot id="s2" name="slot1"></slot> + </template> + <div id="c1" slot="slot1"></div> + <div id="c2" slot="slot1"></div> + <div> + <div id="c3" slot="slot1"></div> + </div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_no_direct_host_child); + removeWhiteSpaceOnlyTextNodes(n.test_no_direct_host_child); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.c2.assignedSlot, n.s1); + assert_equals(n.c3.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), [n.c1, n.c2]); +}, 'Slots: No direct host child.'); +</script> + +<div id="test_default_slot"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"></slot> + <slot id="s2"></slot> + <slot id="s3"></slot> + </template> + <div id="c1"></div> + <div id="c2" slot=""></div> + <div id="c3" slot="foo"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_default_slot); + removeWhiteSpaceOnlyTextNodes(n.test_default_slot); + + assert_equals(n.c1.assignedSlot, n.s2); + assert_equals(n.c2.assignedSlot, n.s2); + assert_equals(n.c3.assignedSlot, null); +}, 'Slots: Default Slot.'); +</script> + +<div id="test_slot_in_slot"> + <div id="host"> + <template data-mode="open"> + <slot id="s1" name="slot1"> + <slot id="s2" name="slot2"></slot> + </slot> + </template> + <div id="c1" slot="slot2"></div> + <div id="c2" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_slot_in_slot); + removeWhiteSpaceOnlyTextNodes(n.test_slot_in_slot); + + assert_equals(n.c1.assignedSlot, n.s2); + assert_equals(n.c2.assignedSlot, n.s1); +}, 'Slots: Slot in Slot does not matter in assignment.'); +</script> + +<div id="test_slot_is_assigned_to_slot"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_slot_is_assigned_to_slot); + removeWhiteSpaceOnlyTextNodes(n.test_slot_is_assigned_to_slot); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.s1.assignedSlot, n.s2); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), [n.s1]); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); +}, 'Slots: Slot is assigned to another slot'); +</script> + +<div id="test_open_closed"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="closed"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_open_closed); + removeWhiteSpaceOnlyTextNodes(n.test_open_closed); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.s1.assignedSlot, null, + 'A slot in a closed shadow tree should not be accessed via assignedSlot'); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), [n.s1]); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); +}, 'Slots: Open > Closed.'); +</script> + +<div id="test_closed_closed"> + <div id="host1"> + <template data-mode="closed"> + <div id="host2"> + <template data-mode="closed"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_closed_closed); + removeWhiteSpaceOnlyTextNodes(n.test_closed_closed); + + assert_equals(n.c1.assignedSlot, null, + 'A slot in a closed shadow tree should not be accessed via assignedSlot'); + assert_equals(n.s1.assignedSlot, null, + 'A slot in a closed shadow tree should not be accessed via assignedSlot'); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), [n.s1]); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); +}, 'Slots: Closed > Closed.'); +</script> + +<div id="test_closed_open"> + <div id="host1"> + <template data-mode="closed"> + <div id="host2"> + <template data-mode="open"> + <slot id="s2" name="slot2"></slot> + </template> + <slot id="s1" name="slot1" slot="slot2"></slot> + </div> + </template> + <div id="c1" slot="slot1"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_closed_open); + removeWhiteSpaceOnlyTextNodes(n.test_closed_open); + + assert_equals(n.c1.assignedSlot, null, + 'A slot in a closed shadow tree should not be accessed via assignedSlot'); + assert_equals(n.s1.assignedSlot, n.s2); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), [n.s1]); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c1]); +}, 'Slots: Closed > Open.'); +</script> + +<div id="test_complex"> + <div id="host1"> + <template data-mode="open"> + <div id="host2"> + <template data-mode="open"> + <slot id="s5" name="slot5"></slot> + <slot id="s6" name="slot6"></slot> + <slot id="s7"></slot> + <slot id="s8" name="slot8"></slot> + </template> + <slot id="s1" name="slot1" slot="slot5"></slot> + <slot id="s2" name="slot2" slot="slot6"></slot> + <slot id="s3"></slot> + <slot id="s4" name="slot4" slot="slot-none"></slot> + <div id="c5" slot="slot5"></div> + <div id="c6" slot="slot6"></div> + <div id="c7"></div> + <div id="c8" slot="slot-none"></div> + </div> + </template> + <div id="c1" slot="slot1"></div> + <div id="c2" slot="slot2"></div> + <div id="c3"></div> + <div id="c4" slot="slot-none"></div> + </div> +</div> + +<script> +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + assert_equals(n.c1.assignedSlot, n.s1); + assert_equals(n.c2.assignedSlot, n.s2); + assert_equals(n.c3.assignedSlot, n.s3); + assert_equals(n.c4.assignedSlot, null); + + assert_equals(n.s1.assignedSlot, n.s5); + assert_equals(n.s2.assignedSlot, n.s6); + assert_equals(n.s3.assignedSlot, n.s7); + assert_equals(n.s4.assignedSlot, null); + + assert_equals(n.c5.assignedSlot, n.s5); + assert_equals(n.c6.assignedSlot, n.s6); + assert_equals(n.c7.assignedSlot, n.s7); + assert_equals(n.c8.assignedSlot, null); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + assert_array_equals(n.s2.assignedNodes(), [n.c2]); + assert_array_equals(n.s3.assignedNodes(), [n.c3]); + assert_array_equals(n.s4.assignedNodes(), []); + assert_array_equals(n.s5.assignedNodes(), [n.s1, n.c5]); + assert_array_equals(n.s6.assignedNodes(), [n.s2, n.c6]); + assert_array_equals(n.s7.assignedNodes(), [n.s3, n.c7]); + assert_array_equals(n.s8.assignedNodes(), []); + + assert_array_equals(n.s1.assignedNodes({ flatten: true }), [n.c1]); + assert_array_equals(n.s2.assignedNodes({ flatten: true }), [n.c2]); + assert_array_equals(n.s3.assignedNodes({ flatten: true }), [n.c3]); + assert_array_equals(n.s4.assignedNodes({ flatten: true }), []); + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c1, n.c5]); + assert_array_equals(n.s6.assignedNodes({ flatten: true }), [n.c2, n.c6]); + assert_array_equals(n.s7.assignedNodes({ flatten: true }), [n.c3, n.c7]); + assert_array_equals(n.s8.assignedNodes({ flatten: true }), []); +}, 'Slots: Complex case: Basi line.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + let d1 = document.createElement('div'); + d1.setAttribute('slot', 'slot1'); + n.host1.appendChild(d1); + + assert_array_equals(n.s1.assignedNodes(), [n.c1, d1]); + assert_equals(d1.assignedSlot, n.s1); + + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c1, d1, n.c5]); +}, 'Slots: Mutation: appendChild.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.c1.setAttribute('slot', 'slot-none'); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_equals(n.c1.assignedSlot, null); + + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c5]); +}, 'Slots: Mutation: Change slot= attribute 1.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.c1.setAttribute('slot', 'slot2'); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_array_equals(n.s2.assignedNodes(), [n.c1, n.c2]); + assert_equals(n.c1.assignedSlot, n.s2); + + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c5]); + assert_array_equals(n.s6.assignedNodes({ flatten: true }), [n.c1, n.c2, n.c6]); +}, 'Slots: Mutation: Change slot= attribute 2.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.c4.setAttribute('slot', 'slot1'); + + assert_array_equals(n.s1.assignedNodes(), [n.c1, n.c4]); + assert_equals(n.c4.assignedSlot, n.s1); + + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c1, n.c4, n.c5]); +}, 'Slots: Mutation: Change slot= attribute 3.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.c1.remove(); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_equals(n.c1.assignedSlot, null); + + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c5]); +}, 'Slots: Mutation: Remove a child.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + let slot = document.createElement('slot'); + slot.setAttribute('name', 'slot1'); + n.host2.appendChild(slot); + + assert_array_equals(slot.assignedNodes(), []); +}, 'Slots: Mutation: Add a slot: after.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + let slot = document.createElement('slot'); + slot.setAttribute('name', 'slot1'); + n.host2.insertBefore(slot, n.s1); + + assert_array_equals(slot.assignedNodes(), [n.c1]); + assert_equals(n.c1.assignedSlot, slot); + + assert_array_equals(n.s7.assignedNodes(), [slot, n.s3, n.c7]); + assert_array_equals(n.s7.assignedNodes({ flatten: true }), [n.c1, n.c3, n.c7]); +}, 'Slots: Mutation: Add a slot: before.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.s1.remove(); + + assert_array_equals(n.s1.assignedNodes(), []); + assert_equals(n.c1.assignedSlot, null); + + assert_array_equals(n.s5.assignedNodes(), [n.c5]); + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c5]); +}, 'Slots: Mutation: Remove a slot.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.s1.setAttribute('name', 'slot2'); + + assert_array_equals(n.s1.assignedNodes(), [n.c2]); + assert_equals(n.c1.assignedSlot, null); + assert_equals(n.c2.assignedSlot, n.s1); + + assert_array_equals(n.s5.assignedNodes(), [n.s1, n.c5]); + assert_array_equals(n.s5.assignedNodes({ flatten: true }), [n.c2, n.c5]); +}, 'Slots: Mutation: Change slot name= attribute.'); + +test(() => { + let n = createTestTree(test_complex); + removeWhiteSpaceOnlyTextNodes(n.test_complex); + + n.s1.setAttribute('slot', 'slot6'); + + assert_array_equals(n.s1.assignedNodes(), [n.c1]); + + assert_array_equals(n.s5.assignedNodes(), [n.c5]); + assert_array_equals(n.s6.assignedNodes(), [n.s1, n.s2, n.c6]); + assert_array_equals(n.s6.assignedNodes({ flatten: true }), [n.c1, n.c2, n.c6]); +}, 'Slots: Mutation: Change slot slot= attribute.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/README b/testing/web-platform/tests/shadow-dom/untriaged/README new file mode 100644 index 0000000000..5b7572bda4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/README @@ -0,0 +1,2 @@ +This directory contains tests that have been written before the slot proposal had been adopted by the spec. +These tests need to be triaged, fixed, and merged back into shadow-dom directory. diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/attributes/test-006.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/attributes/test-006.html new file mode 100644 index 0000000000..bfbfde36e2 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/attributes/test-006.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_02_01_06</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-aware-attributes"> +<meta name="assert" content="Extensions to Element Interface: shadowRoot of type ShadowRoot"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + + var d = newHTMLDocument(); + + var host = d.createElement('div'); + d.body.appendChild(host); + + assert_equals(host.shadowRoot, null, 'attribute shadowRoot must return null if no shadow tree is accesible'); + + +}, 'A_10_02_01_06_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-001.html new file mode 100644 index 0000000000..c1c44980e1 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-001.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_02_02_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-aware-methods"> +<meta name="assert" content="Extensions to Element Interface: attachShadow method creates new instance of Shadow root object"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + + var d = newHTMLDocument(); + + var host = d.createElement('div'); + d.body.appendChild(host); + + var s = host.attachShadow({mode: 'open'}); + + assert_true(s instanceof ShadowRoot, 'attachShadow() method should create new instance ' + + 'of ShadowRoot object'); + +}, 'A_10_02_02_01_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-002.html new file mode 100644 index 0000000000..f7e1595a01 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/extensions-to-element-interface/methods/test-002.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_02_02_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-aware-methods"> +<meta name="assert" content="Extensions to Element Interface: attachShadow method"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + host.appendChild(span); + + var s = host.attachShadow({mode: 'open'}); + + // span should become invisible as shadow root content + assert_equals(span.offsetTop, 0, 'attachShadow() method should establish ' + + 'the context object as the shadow host of the ShadowRoot object'); + +}), 'A_10_02_02_02_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/activeElement-confirm-return-null.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/activeElement-confirm-return-null.html new file mode 100644 index 0000000000..417995d75e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/activeElement-confirm-return-null.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Confirm activeElement return null</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Masaya Iseki" href="mailto:iseki.m.aa@gmail.com"> +<link rel="help" href="http://w3c.github.io/webcomponents/spec/shadow/#attributes"> +<meta name="assert" content="ShadowRoot Object: confirm activeElement return null"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + assert_equals(s.activeElement, null, 'activeElement attribute of the ShadowRoot must return null if there\'s no focused element'); + +}), 'confirm activeElement return null'); + + +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var inp = d.createElement('input'); + d.body.appendChild(inp); + + inp.focus(); + + assert_equals(s.activeElement, null, 'activeElement attribute of the ShadowRoot must return null if there\'s no focused element in the shadow tree'); + +}), 'confirm activeElement return null when there is other element in body'); + + +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var inp = d.createElement('input'); + d.body.appendChild(inp); + + var inp2 = d.createElement('input'); + s.appendChild(inp2); + + inp.focus(); + + assert_equals(s.activeElement, null, 'activeElement attribute of the ShadowRoot must return null if there\'s no focused element in the shadow tree'); + +}), 'confirm activeElement return null when focus on the element in the outer shadow tree'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-007.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-007.html new file mode 100644 index 0000000000..d8868f5695 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-007.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_03_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: readonly attribute Element? activeElement; actual value"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('id', 'shRoot'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpId'); + inp.setAttribute('value', 'Some text'); + s.appendChild(inp); + + inp.focus(); + + assert_true(s.activeElement != null, 'Point 1: activeElement attribute of the ShadowRoot ' + + 'must return the currently focused element in the shadow tree'); + assert_equals(s.activeElement.tagName, 'INPUT', 'Point 2: activeElement attribute of the ShadowRoot ' + + 'must return the currently focused element in the shadow tree'); + +}), 'A_10_01_01_03_01_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-009.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-009.html new file mode 100644 index 0000000000..219c578266 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-009.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_04_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: innerHTML of type DOMString; Test getter"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + assert_equals(s.innerHTML.toLowerCase(), '<span>some text</span>', + 'Wrong value of ShadowRoot innerHTML attribute'); + +}), 'A_10_01_01_04_01_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-010.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-010.html new file mode 100644 index 0000000000..4207065867 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-010.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_04_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: innerHTML of type DOMString; Test setter"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + s.innerHTML = '<input type="text"><div>new text</div>'; + + assert_equals(s.innerHTML.toLowerCase(), '<input type="text"><div>new text</div>', + 'Wrong value of ShadowRoot innerHTML attribute'); + +}), 'A_10_01_01_04_02_T01_01'); + + +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.setAttribute('id', 'spanId'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + s.innerHTML = '<input type="text" id="inputId"><div id="divId">new text</div>'; + + assert_equals(s.querySelector('#spanId'), null, 'Point 1:innerHTML attribute must replace all content of ' + + 'the ShadowRoot object'); + + assert_true(s.querySelector('#inputId') != null, 'Point 2:innerHTML attribute must replace all content of ' + + 'the ShadowRoot object'); + assert_equals(s.querySelector('#inputId').getAttribute('id'), 'inputId', + 'Point 3:innerHTML attribute must replace all content of the ShadowRoot object'); + + assert_true(s.querySelector('#divId') != null, 'Point 3:innerHTML attribute must replace all content of ' + + 'the ShadowRoot object'); + assert_equals(s.querySelector('#divId').getAttribute('id'), 'divId', + 'Point 4:innerHTML attribute must replace all content of the ShadowRoot object'); +}), 'A_10_01_01_04_02_T01_02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-011.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-011.html new file mode 100644 index 0000000000..74c2c9877f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-011.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_05_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: styleSheets of type StyleSheetList, readonly"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + assert_true(s.styleSheets != null, 'ShadowRoot styleSheets attribute shouldn\'t be null'); + assert_equals(s.styleSheets.length, 0, 'attribute must return the shadow root style sheets only'); + +}), 'A_10_01_01_05_01_T01'); + + +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var style = d.createElement('style'); + s.appendChild(style); + + assert_true(s.styleSheets != null, 'ShadowRoot styleSheets attribute shouldn\'t be null'); + assert_equals(s.styleSheets.length, 1, 'attribute must return the shadow root style sheets'); + +}), 'A_10_01_01_05_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-012.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-012.html new file mode 100644 index 0000000000..af06e4fafb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-012.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_06</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: The nodeType attribute of a ShadowRoot instance must return DOCUMENT_FRAGMENT_NODE"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + assert_equals(s.nodeType, 11, 'The nodeType attribute of a ShadowRoot ' + + 'instance must return DOCUMENT_FRAGMENT_NODE'); + +}), 'A_10_01_01_06_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-013.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-013.html new file mode 100644 index 0000000000..3f754c0381 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-013.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_01_07</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-attributes"> +<meta name="assert" content="ShadowRoot Object: The nodeName attribute of a ShadowRoot instance must return "#document-fragment"."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + assert_equals(s.nodeName, '#document-fragment', 'The nodeName attribute of a ShadowRoot instance ' + + 'must return "#document-fragment".'); + +}), 'A_10_01_01_07_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-001.html new file mode 100644 index 0000000000..6093313a0e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-001.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_02_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-methods"> +<meta name="assert" content="ShadowRoot Object: HTMLElement getElementById(DOMString elementId) method"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + d.body.appendChild(el); + + var s = el.attachShadow({mode: 'open'}); + + var child = d.createElement('span'); + child.setAttribute('id', 'span_id'); + s.appendChild(child); + + assert_true(s.getElementById('span_id') != null, 'Point 1: ShadowRoot getElementById() ' + + 'method should return child element'); + assert_equals(s.getElementById('span_id').getAttribute('id'), 'span_id', 'Point 2: ' + + 'ShadowRoot getElementById() method should return child element'); + +}, 'A_10_01_02_01_T01'); + + + +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + d.body.appendChild(el); + + var s = el.attachShadow({mode: 'open'}); + + assert_true(s.getElementById('span_id') == null, ' ShadowRoot getElementById() ' + + 'method should return null if matching element not found'); + +}, 'A_10_01_02_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-004.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-004.html new file mode 100644 index 0000000000..dc087680dd --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-004.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_02_04</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-methods"> +<meta name="assert" content="ShadowRoot Object: Selection? getSelection() method"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + var range = d.createRange(); + range.setStart(span.firstChild, 0); + range.setEnd(span.firstChild, 3); + + var selection = s.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + var sl = s.getSelection(); + assert_equals(sl.toString(), 'Som', 'The getSelection() method of the shadow root object must return ' + + 'the current selection in this shadow tree'); + +}), 'A_10_01_02_04_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html new file mode 100644 index 0000000000..cf10846cc9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_02_06_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-methods"> +<meta name="assert" content="ShadowRoot Object: Element? elementFromPoint(float x, float y) method"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + d.body.appendChild(el); + + try { + el.elementFromPoint(1, 1); + assert_true(false, 'TypeMismatchError should be thrown'); + } catch(e) { + assert_true(e instanceof TypeError, 'Wrong error type'); + } + +}, 'A_10_01_02_06_01_T01'); + +// Added test for checking if elementFromPoint() method is existing on Shadowroot. +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + + var s = el.attachShadow({mode: 'open'}); + d.body.appendChild(el); + + if (typeof(s) == 'undefined' || typeof (s.elementFromPoint(1, 1)) != 'object') { + assert_true(false, 'Shadowroot doesn\'t have elementFromPoint() method.' ); + } + +}, 'A_10_01_02_06_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-007.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-007.html new file mode 100644 index 0000000000..371c5fa851 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-007.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_02_06_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-root-methods"> +<meta name="assert" content="ShadowRoot Object: Element? elementFromPoint(float x, float y) method"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + d.body.appendChild(el); + + var s = el.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + assert_equals(s.elementFromPoint(-1, 1), null, 'If x argument of elementFromPoint(x, y) is less ' + + 'than zero then method shold return null'); + +}, 'A_10_01_02_06_02_T01'); + + +test(function () { + + var d = newHTMLDocument(); + + var el = d.createElement('div'); + d.body.appendChild(el); + + var s = el.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + assert_equals(s.elementFromPoint(1, -1), null, 'If y argument of elementFromPoint(x, y) is less ' + + 'than zero then method shold return null'); + +}, 'A_10_01_02_06_02_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-010.html b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-010.html new file mode 100644 index 0000000000..8e11f739ce --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-010.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_10_01_02_09</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="https://dom.spec.whatwg.org/#dom-node-clonenode"> +<meta name="assert" content="If context object is a shadow root, throw a NotSupportedError exception."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../../html/resources/common.js"></script> +<script src="../../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + try { + s.cloneNode(); + assert_true(false, 'Invoking the cloneNode() method on a ShadowRoot instance must always ' + + 'throw a NotSupportedError (code 9) exception.'); + } catch (e) { + assert_equals(e.code, 9, 'Wrong exception type'); + assert_equals(e.name, 'NotSupportedError', 'Wrong exception name'); + } +}), 'A_10_01_02_09_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-002.html new file mode 100644 index 0000000000..663337526a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-002.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_05_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#event-dispatch"> +<meta name="assert" content="Event Dispatch: The MouseEvent relatedTarget attribute must return the adjusted related target"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_05_02_T01 = async_test('A_05_05_02_T01'); + +A_05_05_02_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var invoked = false; + + roots = createTestMediaPlayer(d); + + //expected result of what relative target should be see + //see at http://www.w3.org/TR/shadow-dom/#event-retargeting-example + + //For #volume-shadow-root adjusted related target #volume-shadow-root + roots.volumeShadowRoot.addEventListener('mouseover', + A_05_05_02_T01.step_func(function(event) { + invoked = true; + assert_equals(event.relatedTarget, roots.volumeShadowRoot, + 'Wrong relatedTarget'); + }), false); + + + + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("mouseover", true, false, window, + 0, 10, 10, 10, 10, false, false, false, false, 0, roots.volumeShadowRoot); + + roots.volumeShadowRoot.querySelector('#volume-slider-thumb').dispatchEvent(evt); + assert_true(invoked, 'Event listener was not invoked'); + A_05_05_02_T01.done(); +})); + +//TODO (sgrekhov) Add tests for other nodes from http://www.w3.org/TR/shadow-dom/#event-retargeting-example +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-003.html new file mode 100644 index 0000000000..bd81521018 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/event-dispatch/test-003.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_05_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#event-path-trimming"> +<meta name="assert" content="Event Path Trimming: In cases where both relatedTarget and target of a trusted event are part of the same shadow tree, the conforming UAs must stop events at the shadow root to avoid the appearance of spurious mouseover and mouseout events firing from the same node."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_05_03_T01 = async_test('A_05_05_03_T01'); + +A_05_05_03_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + s.id = 'shadow'; + + var input1 = d.createElement('input'); + input1.setAttribute('id', 'input1'); + s.appendChild(input1); + + var input2 = d.createElement('input'); + input2.setAttribute('id', 'input2'); + s.appendChild(input2); + + input1.addEventListener('focusin', A_05_05_03_T01.step_func(function(event) { + assert_equals(event.composed, true); + assert_equals(event.composedPath().length, 7); + assert_equals(event.composedPath()[0].id, 'input1'); + assert_equals(event.composedPath()[1].id, 'shadow'); + assert_equals(event.composedPath()[2].id, 'host'); + assert_equals(event.composedPath()[3].tagName, 'BODY'); + assert_equals(event.composedPath()[4].tagName, 'HTML'); + assert_equals(event.composedPath()[5], d); + assert_equals(event.composedPath()[6], ctx.iframes[0].contentWindow); + }), false); + + input1.addEventListener('focusout', A_05_05_03_T01.step_func(function(event) { + assert_equals(event.composed, true); + }), false); + + input2.addEventListener('focusin', A_05_05_03_T01.step_func(function(event) { + assert_equals(event.composedPath().length, 2); + assert_equals(event.composedPath()[0].id, 'input2'); + assert_equals(event.composedPath()[1].id, 'shadow'); + A_05_05_03_T01.done(); + }), false); + + // Expected event path for #input1: + // <input>, #shadow-root, <div>, <body>, <html>, #document, window + input1.focus(); + + // Causes a "focusin" event, from #input1 to #input2 + // In this case, original relatedTarget is #input1, and original target + // is #input2. + // It should be viewed outside the shadow as "target == relatedTarget" + // after event retargeting, therefore, event.composedPath() above the shadow + // host will be trimmed. + // Expected event path for #input2: + // <input>, #shadow-root + input2.focus(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-001.html new file mode 100644 index 0000000000..4ad3ac5ce9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-001.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_01_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#event-retargeting"> +<meta name="assert" content="Event Retargeting:test that event.target is retargeted when event crosses shadow boundary and vice versa"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_01_01_T1 = async_test('A_05_01_01_T1'); + +A_05_01_01_T1.step(function () { + var iframe = document.createElement('iframe'); + iframe.src = '../../resources/blank.html'; + document.body.appendChild(iframe); + + iframe.onload = A_05_01_01_T1.step_func(function () { + + try { + var d = iframe.contentDocument; + var div = d.createElement('div'); + d.body.appendChild(div); + + var s = div.attachShadow({mode: 'open'}); + + var div2 = d.createElement('div'); + s.appendChild(div2); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpid'); + div2.appendChild(inp); + + div2.addEventListener('click', A_05_01_01_T1.step_func(function (event) { + assert_equals(event.target.tagName, 'INPUT', 'Information about target of the event that ' + + 'doesn\'t cross the shadow boundaries should not be adjusted'); + }), false); + + var event = d.createEvent('HTMLEvents'); + event.initEvent ("click", true, false); + inp.dispatchEvent(event); + } finally { + iframe.parentNode.removeChild(iframe); + } + A_05_01_01_T1.done(); + }); +}); + + + +var A_05_01_01_T2 = async_test('A_05_01_01_T2'); + +A_05_01_01_T2.step(function () { + var iframe = document.createElement('iframe'); + iframe.src = '../../resources/blank.html'; + document.body.appendChild(iframe); + + iframe.onload = A_05_01_01_T2.step_func(function () { + + try { + var d = iframe.contentDocument; + + var div = d.createElement('div'); + d.body.appendChild(div); + + var s = div.attachShadow({mode: 'open'}); + + var div2 = d.createElement('div'); + s.appendChild(div2); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpid'); + div2.appendChild(inp); + + div.addEventListener('click', A_05_01_01_T2.step_func(function (event) { + assert_equals(event.target.tagName, 'DIV', 'Information about event target crossing ' + + 'the shadow boundaries should be adjusted'); + }), false); + + var event = d.createEvent('HTMLEvents'); + event.initEvent ("click", true, false); + inp.dispatchEvent(event); + } finally { + iframe.parentNode.removeChild(iframe); + } + A_05_01_01_T2.done(); + }); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-003.html new file mode 100644 index 0000000000..23ee9ad723 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/event-retargeting/test-003.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_01_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#event-retargeting"> +<meta name="assert" content="Event Retargeting:Event retargeting for fallback content"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_01_03_T01 = async_test('A_05_01_03_T01'); + +A_05_01_03_T01.step(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + d.body.innerHTML = '' + + '<div id="main">' + + '<div id="shadow-root">' + + '<span>1</span>' + + '<span>2</span>' + + '<span>3</span>' + + '</div>' + + '</div>'; + + var ul = d.querySelector('#shadow-root'); + var s = ul.attachShadow({mode: 'open'}); + + //make shadow subtree + var div = document.createElement('div'); + div.innerHTML = '<slot name="shadow"><span id="flbk">Fallback item</span></slot>'; + s.appendChild(div); + + d.body.addEventListener('click', A_05_01_03_T01.step_func(function (event) { + assert_equals(event.target.getAttribute('id'), 'shadow-root', 'Information about ' + + 'event target crossing the shadow boundaries should be adjusted for the fallback ' + + 'content'); + }), false); + + var event = d.createEvent('HTMLEvents'); + event.initEvent ("click", true, false); + s.querySelector('#flbk').dispatchEvent(event); + + A_05_01_03_T01.done(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-001.html new file mode 100644 index 0000000000..ce21769e3a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-001.html @@ -0,0 +1,313 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_03_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#retargeting-focus-events"> +<meta name="assert" content="Retargeting focus events:The focus, DOMFocusIn, blur, and DOMFocusOut events must be treated in the same way as events with a relatedTarget, where the corresponding node that is losing focus as a result of target gaining focus or the node that is gaining focus, and thus causing the blurring of target acts as the related target"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//blur and focus events are not bubbling. So this test tests only DOMFocusIn and DOMFocusOut +//which do bubble + +//test DOMFocusOut event +var A_05_03_01_T01 = async_test('A_05_03_01_T01'); + +A_05_03_01_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + s.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + d.body.appendChild(inp2); + + s.addEventListener('DOMFocusOut', A_05_03_01_T01.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Inside shadow tree: Wrong target'); + }), false); + + d.body.addEventListener('DOMFocusOut', A_05_03_01_T01.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'host', 'Inside shadow tree: Wrong target'); + }), false); + + inp1.focus(); + inp2.focus(); + + A_05_03_01_T01.done(); +})); + + +//test DOMFocusIn event +var A_05_03_01_T02 = async_test('A_05_03_01_T02'); + +A_05_03_01_T02.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + s.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + d.body.appendChild(inp2); + + inp2.focus(); + + s.addEventListener('DOMFocusIn', A_05_03_01_T02.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Inside shadoe tree: Wrong target'); + }), false); + + d.body.addEventListener('DOMFocusIn', A_05_03_01_T02.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'host', 'Outside shadow tree: Wrong target'); + }), false); + + inp1.focus(); + + A_05_03_01_T02.done(); +})); + + +//gaining and loosing focus elements are in the same tree. +//DOMFocusIn event should be stopped at shadow boundary +var A_05_03_01_T03 = async_test('A_05_03_01_T03'); + + +A_05_03_01_T03.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + s.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + s.appendChild(inp2); + + inp1.focus(); + + d.body.addEventListener('DOMFocusIn', A_05_03_01_T03.step_func(function(event) { + assert_true(false, 'Event should be stopped at Shadow boundary'); + }), false); + + inp2.focus(); + + A_05_03_01_T03.done(); +})); + + + + +//gaining and loosing focus elements are in the same tree. +//DOMFocusOut event should be stopped at shadow boundary +var A_05_03_01_T04 = async_test('A_05_03_01_T04'); + +A_05_03_01_T04.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + s.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + s.appendChild(inp2); + + inp1.focus(); + + d.body.addEventListener('DOMFocusOut', A_05_03_01_T04.step_func(function(event) { + assert_true(false, 'Event should be stopped at Shadow boundary'); + }), false); + + inp2.focus(); + + A_05_03_01_T04.done(); +})); + + + + +//Retargeting shouldn't occur for DOM tree nodes distributed +//among insertion point. Check DOMFocusOut +var A_05_03_01_T05 = async_test('A_05_03_01_T05'); + +A_05_03_01_T05.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + inp1.setAttribute('slot', 'slot1'); + host.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + inp2.setAttribute('slot', 'slot2'); + host.appendChild(inp2); + + var inp3 = d.createElement('input'); + inp3.setAttribute('id', 'inp3'); + inp3.setAttribute('type', 'checkbox'); + inp3.setAttribute('slot', 'slot1'); + host.appendChild(inp3); + + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var shadowDiv = document.createElement('div'); + shadowDiv.innerHTML = '<slot name="slot1"></slot>'; + s.appendChild(shadowDiv); + + //element outside the shadow tree + var inp4 = d.createElement('input'); + inp4.setAttribute('id', 'inp4'); + inp4.setAttribute('type', 'checkbox'); + inp4.setAttribute('slot', 'slot1'); + d.body.appendChild(inp4); + + inp1.focus(); + + s.addEventListener('DOMFocusOut', A_05_03_01_T05.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Inside shadow tree: ' + + 'Event for nodes, distributed ' + + 'agains insertion points shouldn\'t be retargeted'); + }), false); + + + d.body.addEventListener('DOMFocusOut', A_05_03_01_T05.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Outside shadow tree: ' + + 'Event for nodes, distributed ' + + 'agains insertion points shouldn\'t be retargeted'); + }), false); + + inp4.focus(); + + A_05_03_01_T05.done(); +})); + + +//Retargeting shouldn't occur for DOM tree nodes distributed +//among insertion points. Check DOMFocusIn +var A_05_03_01_T06 = async_test('A_05_03_01_T06'); + +A_05_03_01_T06.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + var inp1 = d.createElement('input'); + inp1.setAttribute('id', 'inp1'); + inp1.setAttribute('type', 'checkbox'); + inp1.setAttribute('slot', 'slot1'); + host.appendChild(inp1); + + var inp2 = d.createElement('input'); + inp2.setAttribute('id', 'inp2'); + inp2.setAttribute('type', 'checkbox'); + inp2.setAttribute('slot', 'slot2'); + host.appendChild(inp2); + + var inp3 = d.createElement('input'); + inp3.setAttribute('id', 'inp3'); + inp3.setAttribute('type', 'checkbox'); + inp3.setAttribute('slot', 'slot1'); + host.appendChild(inp3); + + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var shadowDiv = document.createElement('div'); + shadowDiv.innerHTML = '<slot name="slot1"></slot>'; + s.appendChild(shadowDiv); + + //element outside the shadow tree + var inp4 = d.createElement('input'); + inp4.setAttribute('id', 'inp4'); + inp4.setAttribute('type', 'checkbox'); + inp4.setAttribute('slot', 'slot1'); + d.body.appendChild(inp4); + + inp4.focus(); + + s.addEventListener('DOMFocusIn', A_05_03_01_T06.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Inside shadow tree: ' + + 'Event for nodes, distributed ' + + 'agains insertion points shouldn\'t be retargeted'); + }), false); + + + d.body.addEventListener('DOMFocusIn', A_05_03_01_T05.step_func(function(event) { + assert_equals(event.target.getAttribute('id'), 'inp1', 'Outside shadow tree: ' + + 'Event for nodes, distributed ' + + 'agains insertion points shouldn\'t be retargeted'); + }), false); + + inp1.focus(); + + A_05_03_01_T06.done(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-002.html new file mode 100644 index 0000000000..9c0eb23ae4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-002.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_03_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#retargeting-focus-events"> +<meta name="assert" content="Retargeting focus events:The blur event must be treated in the same way as events with a relatedTarget, where the node that is gaining focus causing the blurring of target acts as the related target"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//test blur event +var A_05_03_02_T01 = async_test('A_05_03_02_T01'); + + +A_05_03_02_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var invoked = false; + + var roots = createTestMediaPlayer(d); + + roots.playerShadowRoot.querySelector('.volume-slider').focus(); + + //expected result of what relative target should be see + //see at http://www.w3.org/TR/shadow-dom/#event-retargeting-example + + //For #volume-slider relative target is #volume-slider + roots.playerShadowRoot.querySelector('.volume-slider').addEventListener('blur', + A_05_03_02_T01.step_func(function(event) { + invoked = true; + assert_equals(event.target.getAttribute('id'), 'volume-slider', + 'Wrong target'); + }), false); + + // move focus out of shadow tree. blur should be fired + d.querySelector('#outside-control').focus(); + + assert_true(invoked, 'Event listener was not invoked'); + + A_05_03_02_T01.done(); +})); + + +//TODO (sgrekhov) add test for the case when related target differs from the +//node on which event listener is invoked +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-003.html new file mode 100644 index 0000000000..55fdde1307 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-focus-events/test-003.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_03_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#retargeting-focus-events"> +<meta name="assert" content="Retargeting focus events:The focus event must be treated in the same way as events with a relatedTarget, where the corresponding node that is losing focus as a result of target gaining focus or the node that is gaining focus"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//test focus event +var A_05_03_03_T01 = async_test('A_05_03_03_T01'); + + +A_05_03_03_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var invoked = false; + + var roots = createTestMediaPlayer(d); + + d.querySelector('#outside-control').focus(); + + //expected result of what relative target should be see + //see at http://www.w3.org/TR/shadow-dom/#event-retargeting-example + + //For #volume-slider relative target is #volume-slider + roots.playerShadowRoot.querySelector('.volume-slider').addEventListener('focus', + A_05_03_03_T01.step_func(function(event) { + invoked = true; + assert_equals(event.target.getAttribute('id'), 'volume-slider', + 'Wrong target'); + }), false); + + roots.playerShadowRoot.querySelector('.volume-slider').focus(); + + assert_true(invoked, 'Event listener was not invoked'); + + A_05_03_03_T01.done(); +})); + + +//TODO (sgrekhov) add test for the case when related target differs from the +//node on which event listener is invoked +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-001.html new file mode 100644 index 0000000000..5b0ed3fddd --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-001.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_02_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-201305214#retargeting-related-target"> +<meta name="assert" content="Retargeting relatedTarget:Event retargeting is a process of computing relative targets for each ancestor of the node at which the event is dispatched. A relative target is a DOM node that most accurately represents the target of a dispatched event at a given ancestor while maintaining the upper boundary encapsulation."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_02_01_T01 = async_test('A_05_02_01_T1'); + +A_05_02_01_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:100%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var div1 = d.createElement('div'); + div1.setAttribute('style', 'height:40px; width:100%'); + div1.setAttribute('id', 'div1'); + s.appendChild(div1); + + var div2 = d.createElement('div'); + div2.setAttribute('style', 'height:40px; width:100%'); + div2.setAttribute('id', 'div2'); + s.appendChild(div2); + + s.addEventListener('mouseover', A_05_02_01_T01.step_func(function(event) { + assert_equals(event.relatedTarget.getAttribute('id'), 'div1', 'Wrong relatedTarget'); + }), false); + + d.body.addEventListener('mouseover', A_05_02_01_T01.step_func(function(event) { + assert_true(false, 'Event must be stopped at Shadow boundary'); + }), false); + + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("mouseover", true, false, window, + 0, 10, 10, 10, 10, false, false, false, false, 0, div1); + + div2.dispatchEvent(evt); + + + A_05_02_01_T01.done(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-002.html new file mode 100644 index 0000000000..3a8ea47e76 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-002.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_02_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-201305214#retargeting-related-target"> +<meta name="assert" content="Retargeting relatedTarget:For a given node, the relatedTarget must be changed to its ancestor (or self) that is in the same shadow tree as the node"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_02_02_T01 = async_test('A_05_02_02_T01'); + +A_05_02_02_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var div1 = d.createElement('div'); + div1.setAttribute('style', 'height:100%; width:100%'); + div1.setAttribute('id', 'div1'); + s.appendChild(div1); + + var div2 = d.createElement('div'); + div2.setAttribute('style', 'height:100%; width:100%'); + div2.setAttribute('id', 'div2'); + d.body.appendChild(div2); + + d.body.addEventListener('mouseover', A_05_02_02_T01.step_func(function(event) { + assert_equals(event.relatedTarget.getAttribute('id'), 'host', 'Wrong related target'); + }), false); + + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("mouseover", true, false, window, + 0, 10, 10, 10, 10, false, false, false, false, 0, div1); + + div2.dispatchEvent(evt); + + A_05_02_02_T01.done(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-003.html new file mode 100644 index 0000000000..900a3ac7fe --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/retargeting-relatedtarget/test-003.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_02_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#event-relatedtarget-retargeting"> +<meta name="assert" content="The value of the Event object's relatedTarget attribute must be the result of the retargeting algorithm with the event's currentTarget and relatedTarget as input. The result is called a relative related target."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_02_03_T01 = async_test('A_05_02_03_T01'); + +A_05_02_03_T01.step(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('style', 'height:50%; width:100%'); + host.setAttribute('id', 'host'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var div1 = d.createElement('div'); + div1.setAttribute('style', 'height:100%; width:100%'); + div1.setAttribute('id', 'div1'); + s.appendChild(div1); + + host.addEventListener('mouseover', A_05_02_03_T01.step_func(function(event) { + assert_unreached('Event listeners shouldn\'t be invoked if relative target and relative related target are the same'); + }), false); + + var evt = new MouseEvent("mouseover", + { relatedTarget: div1, relatedTargetScoped: true }); + div1.dispatchEvent(evt); + + A_05_02_03_T01.done(); +})); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/events/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/events/test-001.html new file mode 100644 index 0000000000..b150f282f4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/events/test-001.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_05_00_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#events"> +<meta name="assert" content="Events:The mutation event types must never be dispatched in a shadow DOM subtree."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_05_00_01_T1 = async_test('A_05_00_01_T1'); + +A_05_00_01_T1.step(function () { + var iframe = document.createElement('iframe'); + iframe.src = '../resources/blank.html'; + document.body.appendChild(iframe); + + iframe.onload = A_05_00_01_T1.step_func(function () { + + try { + var d = iframe.contentDocument; + + var div = d.createElement('div'); + d.body.appendChild(div); + + var s = div.attachShadow({mode: 'open'}); + + var div2 = d.createElement('div'); + s.appendChild(div2); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpid'); + div2.appendChild(inp); + + div2.addEventListener('DOMAttrModified', A_05_00_01_T1.step_func(function (event) { + assert_true(false, 'The mutation event types must never be dispatched in a shadow DOM subtree'); + }), false); + /* + var attr = inp.getAttributeNode ("value"); + var event = d.createEvent('MutationEvent'); + event.initMutationEvent ("DOMAttrModified", true, true, attr, null, 'new value', "value", MutationEvent.MODIFICATION); + inp.dispatchEvent(event); + */ + inp.value = 'new value'; + inp.setAttribute ("newAttr" , "firstValue"); + inp.setAttribute ("newAttr" , "secondValue"); + inp.removeAttribute ("newAttr"); + } finally { + iframe.parentNode.removeChild(iframe); + } + A_05_00_01_T1.done(); + }); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-001.html new file mode 100644 index 0000000000..2783e5f15e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-001.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_08_02_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#html-forms"> +<meta name="assert" content="HTML Elements in shadow trees: Form elements and form-associated elements in shadow tree are not accessible using document DOM object's tree accessors"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//test form-associated elements +test(function () { + var d = newHTMLDocument(); + + var form = d.createElement('form'); + form.setAttribute('id', 'form_id'); + d.body.appendChild(form); + + var div = d.createElement('div'); + d.body.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + + var el = d.createElement(tagName); + el.setAttribute('form', 'form_id'); + el.setAttribute('id', tagName + '_id'); + s.appendChild(el); + + assert_equals(d.querySelector('#' + tagName + '_id'), null, 'Form-associated element ' + tagName + + ' in shadow tree must not be accessible using owner\'s document tree accessors'); + }); +}, 'A_08_02_01_T01'); + + +//test form elements +test(function () { + var d = newHTMLDocument(); + + var form = d.createElement('form'); + d.body.appendChild(form); + + var div = d.createElement('div'); + form.appendChild(div); + s = div.attachShadow({mode: 'open'}); + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + + var el = d.createElement(tagName); + el.setAttribute('id', tagName + '_id'); + s.appendChild(el); + + assert_equals(d.querySelector('#' + tagName + '_id'), null, 'Form element ' + tagName + + ' in shadow tree must not be accessible using owner\'s document tree accessors'); + }); +}, 'A_08_02_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-002.html new file mode 100644 index 0000000000..597d985bee --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-002.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_08_02_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#html-forms"> +<meta name="assert" content="HTML Elements in shadow trees: Form elements and form-associated elements in shadow tree must be accessible using shadow tree accessors"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//test form-associated elements +test(function () { + var d = newHTMLDocument(); + + var form = d.createElement('form'); + form.setAttribute('id', 'form_id'); + d.body.appendChild(form); + + var div = d.createElement('div'); + d.body.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + + var el = d.createElement(tagName); + el.setAttribute('form', 'form_id'); + el.setAttribute('id', tagName + '_id'); + s.appendChild(el); + + assert_true(s.querySelector('#' + tagName + '_id') != null, 'Form-associated element ' + tagName + + ' in shadow tree must be accessible shadow tree accessors'); + assert_equals(s.querySelector('#' + tagName + '_id').getAttribute('id'), tagName + '_id', + 'Form-associated element ' + tagName + ' in shadow tree must be accessible shadow tree accessors'); + }); +}, 'A_08_02_02_T01'); + + +//test form elements +test(function () { + var d = newHTMLDocument(); + + var form = d.createElement('form'); + d.body.appendChild(form); + + var div = d.createElement('div'); + form.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + + var el = d.createElement(tagName); + el.setAttribute('id', tagName + '_id'); + s.appendChild(el); + + assert_true(s.querySelector('#' + tagName + '_id') != null, 'Form-associated element ' + tagName + + ' in shadow tree must be accessible shadow tree accessors'); + assert_equals(s.querySelector('#' + tagName + '_id').getAttribute('id'), tagName + '_id', + 'Form element ' + tagName + ' in shadow tree must be accessible shadow tree accessors'); + }); +}, 'A_08_02_02_T02'); + + +//test distributed form elements +test(function () { + var d = newHTMLDocument(); + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + + var form = d.createElement('form'); + d.body.appendChild(form); + + var div = d.createElement('div'); + form.appendChild(div); + + var el = d.createElement(tagName); + el.setAttribute('id', tagName + '_id'); + el.setAttribute('slot', tagName + '_slot'); + div.appendChild(el); + + var s = div.attachShadow({mode: 'open'}); + s.innerHTML = '<slot name="' + tagName + '_slot"></slot>'; + + assert_true(s.querySelector('#' + tagName + '_id') == null, 'Distributed form-associated element ' + tagName + + ' in shadow tree must not be accessible shadow tree accessors'); + }); +}, 'A_08_02_02_T03'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-003.html new file mode 100644 index 0000000000..96c1027473 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/html-forms/test-003.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_08_02_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="https://w3c.github.io/webcomponents/spec/shadow/#inertness-of-html-elements-in-a-shadow-tree"> +<meta name="assert" content="HTML Elements in shadow trees: form should not submit elements in shadow tree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_08_02_03_T01 = async_test('A_08_02_03_T01'); + +var checkIframeContent = A_08_02_03_T01.step_func(function () { + // remember value to check before cleaning the context (it'll destroy the iframe) + var valueToCheck = A_08_02_03_T01.iframe.contentWindow.document.URL; + cleanContext(A_08_02_03_T01.ctx); + + assert_true(valueToCheck.indexOf('inp1=value1') > 0, + 'html form should submit all of its fields'); + + // Form data crossing shadow boundary should not be submitted. + // https://github.com/w3c/webcomponents/issues/65 + assert_equals(valueToCheck.indexOf('inp2=value2'), -1, + 'html form should not submit fields in the shadow tree'); + + A_08_02_03_T01.done(); +}); + + +A_08_02_03_T01.step(function () { + + A_08_02_03_T01.ctx = newContext(); + var d = newRenderedHTMLDocument(A_08_02_03_T01.ctx); + + //create iframe + var iframe = document.createElement('iframe'); + A_08_02_03_T01.iframe = iframe; + + iframe.src = '../../resources/blank.html'; + iframe.setAttribute('name', 'targetIframe'); + + // create form + var form = d.createElement('form'); + form.setAttribute('target', 'targetIframe'); + form.setAttribute('method', 'GET'); + form.setAttribute('action', '../../resources/blank.html'); + d.body.appendChild(form); + + // create shadow root + var root = d.createElement('div'); + form.appendChild(root); + var s = root.attachShadow({mode: 'open'}); + + var input1 = d.createElement('input'); + input1.setAttribute('type', 'text'); + input1.setAttribute('name', 'inp1'); + input1.setAttribute('value', 'value1'); + form.appendChild(input1); + + var input2 = d.createElement('input'); + input2.setAttribute('type', 'text'); + input2.setAttribute('name', 'inp2'); + input2.setAttribute('value', 'value2'); + s.appendChild(input2); + + // Wait for the first 'load' event for blank.html. + iframe.onload = A_08_02_03_T01.step_func(() => { + // Wait for the second 'load' event for the submission. + iframe.onload = checkIframeContent; + form.submit(); + }); + d.body.appendChild(iframe); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-001.html new file mode 100644 index 0000000000..f8dd40ea2a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-001.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_08_01_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#inert-html-elements"> +<meta name="assert" content="HTML Elements in shadow trees: base element must behave as inert, or not part of the document tree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_08_01_01_T01 = async_test('A_08_01_01_T01'); + +A_08_01_01_T01.checkIframeContent = A_08_01_01_T01.step_func(function () { + //remember value to check before cleaning the context (it'll destroy the iframe) + var valueToCheck = A_08_01_01_T01.iframe.contentWindow; + cleanContext(A_08_01_01_T01.ctx); + + assert_equals(valueToCheck, null, + 'base html element ih a shadow tree must beahve like inert one'); + + A_08_01_01_T01.done(); +}); + + +A_08_01_01_T01.step(function () { + + A_08_01_01_T01.ctx = newContext(); + var d = newRenderedHTMLDocument(A_08_01_01_T01.ctx); + + //create iframe + var iframe = document.createElement('iframe'); + + iframe.src = '../../resources/blank.html'; + iframe.setAttribute('name', 'targetIframe'); + d.body.appendChild(iframe); + + A_08_01_01_T01.iframe = iframe; + + // create a link + var link = d.createElement('a'); + link.setAttribute('href', '../../resources/bobs_page.html'); + link.innerHTML = 'the link'; + d.body.appendChild(link); + + //create Shadow root + var root = d.createElement('div'); + d.body.appendChild(root); + var s = root.attachShadow({mode: 'open'}); + + // create base element, set iframe as a target + var base = d.createElement('base'); + base.setAttribute('target', 'targetIframe'); + s.appendChild(base); + + //click the link + link.click(); + + //Expected: base should be inert therefore document d + // should be reloaded, so iframe context shouldn't be affected + + // set timeout to give the iframe time to load content + step_timeout(A_08_01_01_T01.checkIframeContent, 2000); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-002.html new file mode 100644 index 0000000000..397acd83e7 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/html-elements-in-shadow-trees/inert-html-elements/test-002.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_08_01_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#inert-html-elements"> +<meta name="assert" content="HTML Elements in shadow trees: link element must behave as inert not as part of the document tree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + var link = d.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + + //create Shadow root + var root = d.createElement('div'); + d.body.appendChild(root); + var s = root.attachShadow({mode: 'open'}); + + s.appendChild(link); + + assert_equals(d.styleSheets.length, 0, 'link element must behave as inert not as part of the document tree'); + + +}), 'A_08_01_02_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/resources/blank.html b/testing/web-platform/tests/shadow-dom/untriaged/resources/blank.html new file mode 100644 index 0000000000..5469aa6d0a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/resources/blank.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<head></head> +<body></body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/shadow-dom/untriaged/resources/bobs_page.html b/testing/web-platform/tests/shadow-dom/untriaged/resources/bobs_page.html new file mode 100644 index 0000000000..92dfb0d3c8 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/resources/bobs_page.html @@ -0,0 +1,35 @@ +<html> +<head> +</head> +<body> + <ul class='stories'> + <li id='li1'><a href='#1'>Link1</a></li> + <li id='li2' title="li2"><a href='#2'>Link 2</a></li> + <li id='li3' class='shadow'><a href='#3'>Link 3 Shadow</a></li> + <li id='li4' class='shadow2'><a href='#4'>Link 4 Shadow 2</a></li> + <li id='li5'><a id="a5" class="shadow" href='#5'>Link 5</a></li> + <li id='li6' class='shadow'><a href='#5'>Link 6 Shadow</a></li> + </ul> + <div id="divid" class='breaking'> + <span id='spandex'>Some text</span> + <ul id="ul2"> + <li id='li11'>Item 11</li> + <li id='li12'>Item 12</li> + <li id='li13' class='shadow'>Item 13 Shadow</li> + <li id='li14' class='shadow2'>Item 14 Shadow 2</li> + <li id='li15'>Item 15</li> + <li id='li16' class='shadow'>Item 16 Shadow</li> + </ul> + </div> + <div id="links-wrapper"> + <a href='#10' id='link10'>Link 10</a> + <a href='#11' id='link11'>Link 11</a> + </div> + <div id="inputs-wrapper"> + <input type='text' id='inp1' disabled/> + <input type='text' id='inp2'/> + <input type='checkbox' id='chb1' checked> + <input type='checkbox' id='chb2'> + </div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest-ref.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest-ref.html new file mode 100644 index 0000000000..3050cefe9d --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest-ref.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" > + <title>Shadow DOM Test Ref file - Tests nested shadow tree.</title> + <link rel="author" title="shingo.miyazawa" href="mailto:kumatronik@gmail.com" > + <script src="../../../../html/resources/common.js"></script> + <meta name="assert" content="nested shadow tree style is valid." > + <style> + #host { + width: 100px; + height: 100px; + background-color: red; + } + </style> + </head> + <body> + <p>The test passes if there is a green square. Test failed if there is a red square.</p> + <div id='host'> + <div id="sub" style="width: 100%;height:100%;"> + <div style="width:100%; height:100%;background-color: green;"></div> + </div> + </div> + </body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest.html new file mode 100644 index 0000000000..8d02227fcf --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/nested-shadow-trees/nested_tree_reftest.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" > + <title>Shadow DOM Test - Tests nested shadow tree.</title> + <link rel="match" href="nested_tree_reftest-ref.html" > + <link rel="author" title="shingo.miyazawa" href="mailto:kumatronik@gmail.com" > + <link rel="help" href="https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#nested-shadow-trees" > + <script src="../../../../html/resources/common.js"></script> + <meta name="assert" content="nested shadow tree style is valid." > + <style> + #host { + width: 100px; + height: 100px; + background-color: red; + } + </style> + </head> + <body> + <p>The test passes if there is a green square. Test failed if there is a red square.</p> + <div id='host'> + </div> + <script> + var shadowRoot = document.getElementById('host').attachShadow({mode: 'open'}); + shadowRoot.innerHTML = '<div id="sub" style="width: 100%;height:100%;"></div>'; + var nestedRoot = shadowRoot.getElementById('sub').attachShadow({mode: 'open'}); + nestedRoot.innerHTML = '<div style="width:100%; height:100%;background-color: green;"></div>'; + </script> + </body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001-ref.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001-ref.html new file mode 100644 index 0000000000..c74cf99e70 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001-ref.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Shadow DOM Test: Basic reprojection (reference)</title> +<link rel="author" title="Anna Ogawa" href="mailto:anna.ogawa.0219@gmail.com"> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<style> +.pass { color: green; } +</style> +</head> +<body> +<p>You should see green text saying "Apple" and "Orange" below.</p> +<div id="host"> + <div id="host2"> + <div>Hello a Shadow Root2.</div> + <div> + Hello a Shadow Root. + <div class="pass">Apple.</div> + <div class="pass">Orange.</div> + <div>Banana.</div> + </div> + </div> +</div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001.html new file mode 100644 index 0000000000..e2f5212b9a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/reprojection/reprojection-001.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Shadow DOM Test - Tests a reprojection.</title> +<link rel="match" href="reprojection-001-ref.html"> +<link rel="author" title="Anna Ogawa" href="mailto:anna.ogawa.0219@gmail.com"> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#reprojection"> +<meta name="assert" content="a node is distributed into more than one insertion point."> +<script src="../../../../html/resources/common.js"></script> +<style> +.pass { color: green; } +</style> +</head> +<body> +<p>You should see green text saying "Apple" and "Orange" below.</p> +<div id="host"> + <div class="pass">Apple.</div> + <div class="pass">Orange.</div> +</div> +<script> + var shadowRoot = host.attachShadow({mode: 'open'}); + shadowRoot.innerHTML = '<div id="host2">Hello a Shadow Root.<slot></slot><div>Banana.</div></div>'; + var host2 = shadowRoot.getElementById("host2"); + var shadowRoot2 = host2.attachShadow({mode: 'open'}); + shadowRoot2.innerHTML = '<div>Hello a Shadow Root2.</div><div><slot></slot></div>'; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001-ref.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001-ref.html new file mode 100644 index 0000000000..619f337853 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001-ref.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Basic shadow root (reference)</title> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<style> +p { color: black; } +div { color: green; } +</style> +</head> +<body> +<p>You should see green text saying "PASS" below.</p> +<div>PASS</div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001.html new file mode 100644 index 0000000000..99cacded05 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-001.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Basic shadow root</title> +<link rel="match" href="shadow-root-001-ref.html"> +<link rel="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-trees"> +<meta name="assert" content="When a shadow root is attached, the shadow tree is rendered."> +<script src="../../../html/resources/common.js"></script> +<style> +p { color: black; } +* { color: red; } +</style> +</head> +<body> +<p>You should see green text saying "PASS" below.</p> +<div id="host">FAIL</div> +<script> +var shadowRoot = window.host.attachShadow({mode: 'open'}); +shadowRoot.innerHTML = + '<style>#pass { color: green; }</style>\n' + + '<div id="pass">PASS</div>'; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002-ref.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002-ref.html new file mode 100644 index 0000000000..eda41b9c04 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002-ref.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Basic distribution (reference)</title> +<link rel="author" title="Anna Ogawa" href="mailto:anna.ogawa.0219@gmail.com"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<style> +p { color: black; } +div { color: green; } +</style> +</head> +<body> +<p> +You should see four lines of green text "A", "B", "C" and "D" below, +in this order. +</p> +<div>A</div> +<div>B</div> +<div>C</div> +<div>D</div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002.html new file mode 100644 index 0000000000..a26e817aae --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/shadow-root-002.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Shadow DOM Test: Basic distribution</title> +<link rel="match" href="shadow-root-002-ref.html"> +<link rel="author" title="Anna Ogawa" href="mailto:anna.ogawa.0219@gmail.com"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#shadow-trees"> +<meta name="assert" content="On distribution, content element is replaced with the shadow host's children."> +<script src="../../../html/resources/common.js"></script> +<style> +p { color: black; } +.pass { color: green; } +* { color: red; } +</style> +</head> +<body> +<p> +You should see four lines of green text "A", "B", "C" and "D" below, +in this order. +</p> +<div id="host"> +<div class="pass">B</div> +<div class="pass">C</div> +</div> +<script> +var shadowRoot = window.host.attachShadow({mode: 'open'}); + +shadowRoot.innerHTML = + '<style>\n' + + '.shadow-pass { color: green; }\n' + + '* { color: red; }\n' + + '</style>' + + '<div class="shadow-pass">A</div>\n' + + '<slot>FAIL</slot>' + + '<div class="shadow-pass">D</div>'; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-001.html new file mode 100644 index 0000000000..3007fafc54 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-001.html @@ -0,0 +1,218 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsulation: document's DOM tree accessors</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:a.semenov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The shadow nodes and named shadow elements are not accessible using shadow host's document DOM tree accessors."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// A document's "DOM tree accessors" include: +// (document.)head, title, body, images, embeds, plugins, links, forms, +// scripts, getElementsByName(), cssElementMap, and currentScript +// +// Of these, it is unclear how document.cssElementMap can be tested. +// Except for it, there is a test corresponding to each accessor. +// +// Additionally, there are obsolete accessors +// <http://www.whatwg.org/specs/web-apps/current-work/multipage/obsolete.html#other-elements,-attributes-and-apis>: +// (document.)anchors, applets, and all. +// +// and some accessors defined in the DOM specification (formerly known as +// "DOM Core") <http://dom.spec.whatwg.org/#interface-document>: +// (document.)documentElement, getElementsByTagName(), +// getElementsByTagNameNS(), getElementsByClassName(), and getElementById(). +// +// As it seems reasonable to have tests for these accessors, this file also +// includes tests for them, except for document.documentElement which is +// unclear whether we can test; the distribution process of Shadow DOM does not +// alter the host element, so the document element (e.g. <html>) cannot be +// replaced with an element in a shadow tree. + +// ---------------------------------------------------------------------------- +// Constants and utility functions + +// Place the same HTML content into both the host document and the shadow root. +// To differentiate these two, a class name is assigned to every element by +// populateTestContentToHostDocument() and populateTestContentToShadowRoot(). +var HTML_CONTENT = [ + '<head>', + '<title></title>', + '<link rel="help" href="#">', + '</head>', + '<body>', + '<p></p>', + '<a name="test-name"></a>', + '<a href="#"></a>', + '<area href="#">', + '<img src="#" alt="">', + '<embed></embed>', + '<form></form>', + '<script><' + '/script>', + '</body>' +].join('\n'); + +function addClassNameToAllElements(document, root, className) { + var nodeIterator = document.createNodeIterator( + root, NodeFilter.SHOW_ELEMENT, null); + var node; + while (node = nodeIterator.nextNode()) + node.className = className; +} + +function populateTestContentToHostDocument(document) { + document.documentElement.innerHTML = HTML_CONTENT; + addClassNameToAllElements(document, document.documentElement, 'host'); +} + +function populateTestContentToShadowRoot(shadowRoot) { + shadowRoot.innerHTML = HTML_CONTENT; + addClassNameToAllElements(shadowRoot.ownerDocument, shadowRoot, 'shadow'); +} + +function createDocumentForTesting() { + var doc = document.implementation.createHTMLDocument(''); + populateTestContentToHostDocument(doc); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + populateTestContentToShadowRoot(shadowRoot); + return doc; +} + +// Make sure the given HTMLCollection contains at least one elements and +// all elements have the class named "host". This function works well with +// HTMLCollection, HTMLAllCollection, and NodeList consisting of elements. +function assert_collection(collection) { + assert_true(collection.length > 0); + Array.prototype.forEach.call(collection, function (element) { + assert_equals(element.className, 'host'); + }); +} + +// ---------------------------------------------------------------------------- +// Tests for DOM tree accessors defined in HTML specification + +test(function () { + var doc = createDocumentForTesting(); + assert_equals(doc.head.className, 'host'); + assert_equals(doc.body.className, 'host'); +}, + '<head> and <body> in a shadow tree should not be accessible from ' + + 'owner document\'s "head" and "body" properties, respectively.' +); + +test(function () { + var doc = document.implementation.createHTMLDocument(''); + populateTestContentToHostDocument(doc); + + // Note: this test is originally written to replace document.documentElement + // with shadow contents, but among Shadow DOM V1 allowed elements body is the + // most approximate to it, though some test may make lesser sense. + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + populateTestContentToShadowRoot(shadowRoot); + + // Replace the content of <title> to distinguish elements in a host + // document and a shadow tree. + doc.getElementsByTagName('title')[0].textContent = 'Title of host document'; + shadowRoot.querySelector('title').textContent = + 'Title of shadow tree'; + + assert_equals(doc.title, 'Title of host document'); +}, + 'The content of title element in a shadow tree should not be accessible ' + + 'from owner document\'s "title" attribute.' +); + +function testHTMLCollection(accessor) { + var doc = createDocumentForTesting(); + assert_collection(doc[accessor]); +} + +generate_tests( + testHTMLCollection, + ['images', 'embeds', 'plugins', 'links', 'forms', 'scripts'].map( + function (accessor) { + return [ + 'Elements in a shadow tree should not be accessible from ' + + 'owner document\'s "' + accessor + '" attribute.', + accessor + ]; + })); + +test(function () { + var doc = createDocumentForTesting(); + assert_collection(doc.getElementsByName('test-name')); +}, + 'Elements in a shadow tree should not be accessible from owner ' + + 'document\'s getElementsByName() method.' +); + +// ---------------------------------------------------------------------------- +// Tests for obsolete accessors + +generate_tests( + testHTMLCollection, + ['anchors', 'all'].map( + function (accessor) { + return [ + 'Elements in a shadow tree should not be accessible from ' + + 'owner document\'s "' + accessor + '" attribute.', + accessor + ]; + })); + +// ---------------------------------------------------------------------------- +// Tests for accessors defined in DOM specification + +test(function () { + var doc = createDocumentForTesting(); + assert_collection(doc.getElementsByTagName('p')); +}, + 'Elements in a shadow tree should not be accessible from owner ' + + 'document\'s getElementsByTagName() method.' +); + +test(function () { + // Create a XML document. + var namespace = 'http://www.w3.org/1999/xhtml'; + var doc = document.implementation.createDocument(namespace, 'html'); + doc.documentElement.appendChild(doc.createElementNS(namespace, 'head')); + var body = doc.createElementNS(namespace, 'body'); + var pHost = doc.createElementNS(namespace, 'p'); + pHost.className = "host"; + body.appendChild(pHost); + doc.documentElement.appendChild(body); + + var shadowRoot = body.attachShadow({mode: 'open'}); + var pShadow = doc.createElementNS(namespace, 'p'); + pShadow.className = "shadow"; + shadowRoot.appendChild(pShadow); + + assert_collection(doc.getElementsByTagNameNS(namespace, 'p')); +}, + 'Elements in a shadow tree should not be accessible from owner ' + + 'document\'s getElementsByTagNameNS() method.' +); + +test(function () { + var doc = document.implementation.createHTMLDocument(''); + populateTestContentToHostDocument(doc); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + populateTestContentToShadowRoot(shadowRoot); + + shadowRoot.querySelectorAll('p')[0].id = 'test-id'; + assert_equals(doc.getElementById('test-id'), null); +}, + 'Elements in a shadow tree should not be accessible from owner ' + + 'document\'s getElementById() method.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-002.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-002.html new file mode 100644 index 0000000000..88a2efc2d3 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-002.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsulation: shadow root's DOM tree accessors</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The nodes are accessible using shadow root's DOM tree accessor methods."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function assert_singleton_node_list(nodeList, expectedNode) { + assert_equals(nodeList.length, 1); + assert_equals(nodeList[0], expectedNode); +} + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var image = doc.createElement('img'); + shadowRoot.appendChild(image); + + assert_singleton_node_list(shadowRoot.querySelectorAll('img'), image); +}, + 'Elements in a shadow tree should be accessible via shadow root\'s ' + + 'querySelectorAll() DOM tree accessor.' +); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var div = doc.createElement('div'); + div.className = 'div-class'; + shadowRoot.appendChild(div); + + assert_singleton_node_list( + shadowRoot.querySelectorAll('.div-class'), div); +}, + 'Elements with a specific class in a shadow tree should be accessible via' + + 'shadow root\'s querySelectorAll() DOM tree accessor.' +); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var div = doc.createElement('div'); + div.id = 'div-id'; + shadowRoot.appendChild(div); + + assert_equals(shadowRoot.getElementById('div-id'), div); +}, + 'Elements in a shadow tree should be accessible via shadow root\'s ' + + 'getElementById() DOM tree accessor.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-001.html new file mode 100644 index 0000000000..58c8adc087 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-001.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsuration on ownerDocument: basic tests</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The ownerDocument property of all nodes in shadow tree refers to the document of the shadow host."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + doc.body.innerHTML = '<div>A<div>B</div>C<div><span>D</span></div>E</div>'; + var nodeIterator = doc.createNodeIterator(doc.body, + NodeFilter.SHOW_ELEMENT, null); + var node; + while (node = nodeIterator.nextNode()) { + var shadowRoot = node.attachShadow({mode: 'open'}); + assert_equals(shadowRoot.ownerDocument, doc); + } +}, 'ownerDocument property of a shadow root should be the document of the ' + + 'shadow host, regardless of the location of the shadow host.'); + +test(function () { + var MAX_DEPTH = 16; + var doc = document.implementation.createHTMLDocument('Test'); + var tail = doc.body; + for (var depth = 1; depth <= MAX_DEPTH; ++depth) { + var div = doc.createElement('div'); + div.id = 'depth-' + depth; + tail.appendChild(div); + tail = div; + } + + for (var depth = 1; depth <= MAX_DEPTH; ++depth) { + var host = doc.getElementById('depth-' + depth); + var shadowRoot = host.attachShadow({mode: 'open'}); + assert_equals(shadowRoot.ownerDocument, doc, + 'ownerDocument mismatch for #depth-' + depth); + } +}, 'ownerDocument property of elements in a shadow tree should match ' + + 'the document of the shadow host, regardless of the element\'s location ' + + 'in a shadow tree.'); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var div = doc.createElement('div'); + shadowRoot.appendChild(div); + assert_equals(div.ownerDocument, doc); +}, 'Elements added to a shadow tree should automatically get a valid ' + + 'ownerDocument.'); + +test(function () { + var doc1 = document.implementation.createHTMLDocument('Test 1'); + var doc2 = document.implementation.createHTMLDocument('Test 2'); + var shadowRoot = doc1.body.attachShadow({mode: 'open'}); + var div = doc2.createElement('div'); + shadowRoot.appendChild(div); + assert_equals(div.ownerDocument, doc1); +}, 'ownerDocument property of an element in a shadow tree should be the ' + + 'document of the shadow host, even if the host element is created from ' + + 'another document.'); + +test(function () { + var doc1 = document.implementation.createHTMLDocument('Test 1'); + var doc2 = document.implementation.createHTMLDocument('Test 2'); + var shadowRoot = doc1.body.attachShadow({mode: 'open'}); + doc2.body.innerHTML = + '<div id="root">A<div>B</div>C<div><span>D</span></div>E</div>'; + shadowRoot.appendChild(doc2.getElementById('root')); + var nodeIterator = doc1.createNodeIterator( + shadowRoot.getElementById('root'), 0xFFFFFFFF, null); + var node; + while (node = nodeIterator.nextNode()) { + assert_equals(node.ownerDocument, doc1); + } +}, 'All children nodes of a shadow root get a valid ownerDocument when ' + + 'added to a shadow tree.'); + +test(function () { + var doc1 = document.implementation.createHTMLDocument('Test 1'); + var doc2 = document.implementation.createHTMLDocument('Test 2'); + var shadowRoot = doc1.body.attachShadow({mode: 'open'}); + doc2.body.innerHTML = '<div id="parent"><div id="child"></div></div>'; + shadowRoot.appendChild(doc2.getElementById('child')); + assert_equals(doc2.getElementById('parent').ownerDocument, doc2); +}, 'ownerDocument property of a node should remain the same, even if its ' + + 'child is adopted into a shadow tree.'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-002.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-002.html new file mode 100644 index 0000000000..3ef53fc5ab --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-002.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsuration on ownerDocument: with all HTML5 elements</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The ownerDocument property of all nodes in shadow tree refers to the document of the shadow host."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function testElement(elementName) { + var doc = document.implementation.createHTMLDocument('Test'); + var element = doc.createElement(elementName); + doc.body.appendChild(element); + var shadowRoot = element.attachShadow({mode: 'open'}); + HTML5_SHADOW_ALLOWED_ELEMENTS.forEach(function (name) { + shadowRoot.appendChild(doc.createElement(name)); + }); + + var iterator = doc.createNodeIterator(shadowRoot, 0xFFFFFFFF, null); + var node; + while (node = iterator.nextNode()) { + assert_equals(node.ownerDocument, doc); + } +} + +var testParameters = HTML5_SHADOW_ALLOWED_ELEMENTS.map(function (name) { + return [ + 'ownerDocument property of any elements in a shadow tree should ' + + 'match the document of the shadow host, when the host is a "' + + name + '" element.', + name + ]; +}); + +generate_tests(testElement, testParameters); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-001.html new file mode 100644 index 0000000000..f3ca2b786f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-001.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsulation: document's Selector APIs</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: Nodes in a shadow tree must not be accessible through selector APIs of owner document."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// Return a document containing the structure below: +// +// <body> - - - - - {shadow-root} +// | | +// | +-- <p class="test-class" id="test-id"> +// | +// +-- <p class="test-class" id="test-id"> +function createTestDocument() { + var doc = document.implementation.createHTMLDocument('Test'); + var pHost = doc.createElement('p'); + pHost.className = 'test-class'; + pHost.id = 'test-id'; + doc.body.appendChild(pHost); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var pShadow = doc.createElement('p'); + pShadow.className = 'test-class'; + pShadow.id = 'test-id'; + shadowRoot.appendChild(pShadow); + return { + doc: doc, + pHost: pHost, + pShadow: pShadow + }; +} + +test(function () { + var documentObject = createTestDocument(); + var doc = documentObject.doc; + var pHost = documentObject.pHost; + assert_equals(doc.querySelector('p'), pHost); + assert_equals(doc.querySelector('.test-class'), pHost); + assert_equals(doc.querySelector('#test-id'), pHost); +}, + 'Elements in a shadow tree should not be accessible from ' + + 'owner document\'s querySelector() method.' +); + +function assert_singleton_node_list(nodeList, expectedNode) { + assert_equals(nodeList.length, 1); + assert_equals(nodeList[0], expectedNode); +} + +test(function () { + var documentObject = createTestDocument(); + var doc = documentObject.doc; + var pHost = documentObject.pHost; + assert_singleton_node_list(doc.querySelectorAll('p'), pHost); + assert_singleton_node_list(doc.querySelectorAll('.test-class'), pHost); + assert_singleton_node_list(doc.querySelectorAll('#test-id'), pHost); +}, + 'Elements in a shadow tree should not be accessible from ' + + 'owner document\'s querySelectorAll() method.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-002.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-002.html new file mode 100644 index 0000000000..6167f57e4e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/selectors-api-002.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Upper-boundary encapsulation: shadow root's Selector APIs</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: Nodes in a shadow tree must be accessible through selector APIs of the shadow root."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// Return a document containing the structure below: +// +// <body> - - - - - {shadow-root} +// | | +// | +-- <p class="test-class" id="test-id"> +// | +// +-- <p class="test-class" id="test-id"> +function createTestDocument() { + var doc = document.implementation.createHTMLDocument('Test'); + var pHost = doc.createElement('p'); + pHost.className = 'test-class'; + pHost.id = 'test-id'; + doc.body.appendChild(pHost); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var pShadow = doc.createElement('p'); + pShadow.className = 'test-class'; + pShadow.id = 'test-id'; + shadowRoot.appendChild(pShadow); + return { + doc: doc, + shadowRoot: shadowRoot, + pHost: pHost, + pShadow: pShadow + }; +} + +test(function () { + var documentObject = createTestDocument(); + var shadowRoot = documentObject.shadowRoot; + var pShadow = documentObject.pShadow; + assert_equals(shadowRoot.querySelector('p'), pShadow); + assert_equals(shadowRoot.querySelector('.test-class'), pShadow); + assert_equals(shadowRoot.querySelector('#test-id'), pShadow); +}, + 'Elements in a shadow tree should be accessible from ' + + 'shadow root\'s querySelector() method.' +); + +function assert_singleton_node_list(nodeList, expectedNode) { + assert_equals(nodeList.length, 1); + assert_equals(nodeList[0], expectedNode); +} + +test(function () { + var documentObject = createTestDocument(); + var shadowRoot = documentObject.shadowRoot; + var pShadow = documentObject.pShadow; + assert_singleton_node_list(shadowRoot.querySelectorAll('p'), pShadow); + assert_singleton_node_list(shadowRoot.querySelectorAll('.test-class'), + pShadow); + assert_singleton_node_list(shadowRoot.querySelectorAll('#test-id'), + pShadow); +}, + 'Elements in a shadow tree should be accessible from ' + + 'shadow root\'s querySelectorAll() method.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/shadow-root-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/shadow-root-001.html new file mode 100644 index 0000000000..97f5c35717 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/shadow-root-001.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Shadow root's parentNode() and parentElement()</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The parentNode and parentElement attributes of the shadow root object must always return null."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + assert_equals(shadowRoot.parentNode, null); +}, 'The parentNode attribute of a shadow root must always return null.'); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + assert_equals(shadowRoot.parentElement, null); +}, 'The parentElement attribute of a shadow root must always return null.'); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var outerShadowRoot = doc.body.attachShadow({mode: 'open'}); + var div = doc.createElement('div'); + outerShadowRoot.appendChild(div); + var innerShadowRoot = div.attachShadow({mode: 'open'}); + assert_equals(innerShadowRoot.parentNode, null); +}, + 'The parentNode attribute of a shadow root must always return null, ' + + 'even if the shadow root is nested inside another shadow root.' +); + +test(function () { + var doc = document.implementation.createHTMLDocument('Test'); + var outerShadowRoot = doc.body.attachShadow({mode: 'open'}); + var div = doc.createElement('div'); + outerShadowRoot.appendChild(div); + var innerShadowRoot = div.attachShadow({mode: 'open'}); + assert_equals(innerShadowRoot.parentElement, null); +}, + 'The parentElement attribute of a shadow root must always return null, ' + + 'even if the shadow root is nested inside another shadow root.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-005.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-005.html new file mode 100644 index 0000000000..b82a9523a9 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-005.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_04_01_05</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation:The nodes with a unique id and named elements are not addressable from any attributes of elements in shadow host's document"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// check label.for attribute +test(function () { + var d = newHTMLDocument(); + var div = d.createElement('div'); + d.body.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + // node in shadow with id + var input = d.createElement('input'); + input.setAttribute('type', 'text'); + input.setAttribute('id', 'input_id'); + d.body.appendChild(input); + s.appendChild(input); + + // node in host with a reference to host element with id + var label = d.createElement('label'); + label.setAttribute('for', 'input_id'); + d.body.appendChild(label); + + assert_equals(label.control, null, 'Elements in shadow DOM must not be accessible from ' + + 'owner\'s document label.for attribute'); + +}, 'A_04_01_05_T01'); + +// check form associated elements +test(function () { + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + var d = newHTMLDocument(); + var div = d.createElement('div'); + d.body.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + var form = d.createElement('form'); + form.setAttribute('id', 'form_id'); + d.body.appendChild(form); + + var el = d.createElement(tagName); + el.setAttribute('form', 'form_id'); + d.body.appendChild(el); + + s.appendChild(form); + + assert_equals(el.form, null, 'Elements in shadow DOM must not be accessible from ' + + 'owner\'s document ' + tagName + '.form attribute'); + }); +}, 'A_04_01_05_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-007.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-007.html new file mode 100644 index 0000000000..0cfe169b44 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-007.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_04_01_07</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation:The nodes with a unique id and named elements are addressable from any attributes of elements in the same shadow DOM subtree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// check for label.control +test(function () { + var d = newHTMLDocument(); + var div = d.createElement('div'); + d.body.appendChild(div); + var s = div.attachShadow({mode: 'open'}); + + var input = d.createElement('input'); + input.setAttribute('type', 'text'); + input.setAttribute('id', 'input_id'); + d.body.appendChild(input); + + var label = d.createElement('label'); + label.setAttribute('for', 'input_id'); + s.appendChild(label); + s.appendChild(input); + + assert_equals(label.control, input, 'Elements in shadow DOM must be accessible from ' + + 'shadow document label.for attribute'); + +}, 'A_04_01_07_T01'); + +// check for elem.form associated elements +test(function () { + + HTML5_FORM_ASSOCIATED_ELEMENTS.forEach(function (tagName) { + d = newHTMLDocument(); + + var form = d.createElement('form'); + var el = d.createElement(tagName); + + d.body.appendChild(form); + d.body.appendChild(el); + + form.setAttribute('id', 'form_id'); + el.setAttribute('form', 'form_id'); + + div = d.createElement('div'); + d.body.appendChild(div); + + var s = div.attachShadow({mode: 'open'}); + s.appendChild(form); + s.appendChild(el); + + assert_equals(el.form, form, 'Elements in shadow DOM must be accessible from ' + + 'shadow document ' + tagName + '.form attribute'); + }); +}, 'A_04_01_07_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-009.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-009.html new file mode 100644 index 0000000000..f37f17bc1a --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-009.html @@ -0,0 +1,235 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_04_01_09</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:a.semenov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Mikhail Fursov" href="mailto:mfursov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: no nodes other than shadow root descendants are accessible with shadow root DOM tree accessor methods"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +var A_04_01_09 = new Object(); + +A_04_01_09.setupBlock = function (ctx, prefix, root) { + // create <div id='prefix+_id1' class='cls'><p class='cls'><div id='prefix+_id2' class='cls'></div></p></div> like structure + // where <p> will be used as shadow host element + + ctx[prefix + '_div1'] = ctx.d.createElement('div'); + ctx[prefix + '_div1'].setAttribute('id', prefix + '_id1'); + ctx[prefix + '_div1'].setAttribute('class', 'cls'); + + ctx[prefix + '_p1'] = ctx.d.createElement('p'); + ctx[prefix + '_p1'].setAttribute('class', 'cls'); + ctx[prefix + '_p1'].setAttribute('test', 'A_04_01_09'); + + ctx[prefix + '_div2'] = ctx.d.createElement('div'); + ctx[prefix + '_div2'].setAttribute('id', prefix + '_id2'); + ctx[prefix + '_div2'].setAttribute('class', 'cls'); + ctx[prefix + '_div2'].setAttribute('test', 'A_04_01_09'); + + root.appendChild(ctx[prefix + '_div1']); + ctx[prefix + '_div1'].appendChild(ctx[prefix + '_p1']); + ctx[prefix + '_p1'].appendChild(ctx[prefix + '_div2']); +}; + +A_04_01_09.setup = function () { + var ctx = {}; + + ctx.d = newHTMLDocument(); + A_04_01_09.setupBlock(ctx, 'd', ctx.d.body); + + ctx.s1 = ctx.d_p1.attachShadow({mode: 'open'}); + A_04_01_09.setupBlock(ctx, 's1', ctx.s1); + + ctx.s2 = ctx.s1_p1.attachShadow({mode: 'open'}); + A_04_01_09.setupBlock(ctx, 's2', ctx.s2); + + assert_true(ctx.d_div1 != null, 'setup:d_div1'); + assert_true(ctx.d_div2 != null, 'setup:d_div2'); + assert_true(ctx.s1_div1 != null, 'setup: s1_div1'); + assert_true(ctx.s1_div2 != null, 'setup: s1_div2'); + assert_true(ctx.s2_div1 != null, 'setup: s2_div1'); + assert_true(ctx.s2_div2 != null, 'setup: s2_div2'); + + return ctx; +}; + +//check querySelectorAll +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder( + ctx.s1.querySelectorAll('div'), [ctx.s1_div1, ctx.s1_div2], + 'nodes, other than shadow root descendants, should not be accessible with ' + + 'ShadowRoot.getElementsByTagName (s1)'); + + assert_nodelist_contents_equal_noorder( + ctx.s2.querySelectorAll('div'), [ctx.s2_div1, ctx.s2_div2], + 'nodes, other than shadow root descendants, should not be accessible with ' + + 'ShadowRoot.getElementsByTagName (s2)'); + +}, 'A_04_01_09_T01'); + +//check querySelectorAll for class +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder( + ctx.s1.querySelectorAll('.cls'), [ctx.s1_div1, ctx.s1_p1, ctx.s1_div2], + 'nodes, other than shadow root descendants, should not be accessible with ' + + 'ShadowRoot.getElementsByClassName (s1)'); + + assert_nodelist_contents_equal_noorder( + ctx.s2.querySelectorAll('.cls'), [ctx.s2_div1, ctx.s2_p1, ctx.s2_div2], + 'nodes, other than shadow root descendants, should not be accessible with ' + + 'ShadowRoot.getElementsByClassName (s2)'); + +}, 'A_04_01_09_T03'); + +// check querySelector for id +test(function () { + var ctx = A_04_01_09.setup(); + + assert_equals(ctx.d.querySelector('#s1_id1'), null, 'Expected no access to s1_div1 from d.querySelector()'); + assert_equals(ctx.d.querySelector('#s1_id2'), null, 'Expected no access to s1_div2 from d.querySelector()'); + assert_equals(ctx.d.querySelector('#s2_id1'), null, 'Expected no access to s2_div1 from d.querySelector()'); + assert_equals(ctx.d.querySelector('#s2_id2'), null, 'Expected no access to s2_div1 from d.querySelector()'); + + assert_equals(ctx.s1.querySelector('#d_id1'), null, 'Expected no access to d_div1 from s1.querySelector()'); + assert_equals(ctx.s1.querySelector('#d_id2'), null, 'Expected no access to d_div2 from s1.querySelector()'); + assert_equals(ctx.s2.querySelector('#d_id1'), null, 'Expected no access to d_div1 from s2.querySelector()'); + assert_equals(ctx.s2.querySelector('#d_id2'), null, 'Expected no access to d_div1 from s2.querySelector()'); + + assert_equals(ctx.d.querySelector('#d_id1'), ctx.d_div1, 'Expected access to d_div1 form d.querySelector()'); + assert_equals(ctx.d.querySelector('#d_id2'), ctx.d_div2, 'Expected access to d_div2 form d.querySelector()'); + assert_equals(ctx.s1.querySelector('#s1_id1'), ctx.s1_div1, 'Expected access to s1_div1 form s1.querySelector()'); + assert_equals(ctx.s1.querySelector('#s1_id2'), ctx.s1_div2, 'Expected access to s1_div2 form s1.querySelector()'); + assert_equals(ctx.s2.querySelector('#s2_id1'), ctx.s2_div1, 'Expected access to s2_div1 form s2.querySelector()'); + assert_equals(ctx.s2.querySelector('#s2_id2'), ctx.s2_div2, 'Expected access to s2_div2 form s2.querySelector()'); + + assert_equals(ctx.s1.querySelector('#s2_id1'), null, 'Expected no access to s2_div1 form s1.querySelector()'); + assert_equals(ctx.s1.querySelector('#s2_id2'), null, 'Expected no access to s2_div2 form s1.querySelector()'); + assert_equals(ctx.s2.querySelector('#s1_id1'), null, 'Expected no access to s1_div1 form s2.querySelector()'); + assert_equals(ctx.s2.querySelector('#s1_id2'), null, 'Expected no access to s1_div2 form s2.querySelector()'); + +}, 'A_04_01_09_T05'); + + +//check querySelector for element +test(function () { + var ctx = A_04_01_09.setup(); + + assert_equals(ctx.d.querySelector('p'), ctx.d_p1, 'Expected access to d_p1 from d.querySelector()'); + assert_equals(ctx.s1.querySelector('p'), ctx.s1_p1, 'Expected access to s1_p1 from s1.querySelector()'); + assert_equals(ctx.s2.querySelector('p'), ctx.s2_p1, 'Expected access to s2_p1 from s2.querySelector()'); + +}, 'A_04_01_09_T06'); + +// check querySelectorAll for element +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('p'), [ctx.d_p1], 'Expected access to d_p1 from d.querySelectorAll()'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('p'), [ctx.s1_p1], 'Expected access to s1_p1 s1.querySelectorAll'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('p'), [ctx.s2_p1], 'Expected access to s2_p1 from s2.querySelectorAll'); + +}, 'A_04_01_09_T07'); + +// check querySelectorAll for class +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('.cls'), [ctx.d_div1, ctx.d_p1, ctx.d_div2], 'd.querySelectorAll() return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('.cls'), [ctx.s1_div1, ctx.s1_p1, ctx.s1_div2], 's1.querySelectorAll() return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('.cls'), [ctx.s2_div1, ctx.s2_p1, ctx.s2_div2], 's2.querySelectorAll() return wrong result'); + +}, 'A_04_01_09_T08'); + +//check querySelectorAll with whildcat +test(function () { + var ctx = A_04_01_09.setup(); + + //assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('*'), [ctx.d_div1, ctx.d_p1, ctx.d_div2], 'd.querySelectorAll'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('*'), [ctx.s1_div1, ctx.s1_p1, ctx.s1_div2], 's1.querySelectorAll(\'*\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('*'), [ctx.s2_div1, ctx.s2_p1, ctx.s2_div2], 's2.querySelectorAll(\'*\') return wrong result'); + +}, 'A_04_01_09_T09'); + +//check querySelectorAll with attribute value +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('[test=A_04_01_09]'), [ctx.d_p1, ctx.d_div2], 'd.querySelectorAll(\'[test=A_04_01_09]\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('[test=A_04_01_09]'), [ctx.s1_p1, ctx.s1_div2], 's1.querySelectorAll(\'[test=A_04_01_09]\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('[test=A_04_01_09]'), [ctx.s2_p1, ctx.s2_div2], 's2.querySelectorAll(\'[test=A_04_01_09]\') return wrong result'); + +}, 'A_04_01_09_T10'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('div:first-child'), [ctx.d_div1, ctx.d_div2], 'd.querySelectorAll(\'div:first-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('div:first-child'), [ctx.s1_div1,ctx.s1_div2], 's1.querySelectorAll(\'div:first-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('div:first-child'), [ctx.s2_div1,ctx.s2_div2], 's2.querySelectorAll(\'div:first-child\') return wrong result'); + +}, 'A_04_01_09_T11'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('div:last-child'), [ctx.d_div1, ctx.d_div2], 'd.querySelectorAll(\'div:last-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('div:last-child'), [ctx.s1_div1, ctx.s1_div2], 's1.querySelectorAll(\'div:last-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('div:last-child'), [ctx.s2_div1, ctx.s2_div2], 's2.querySelectorAll(\'div:last-child\') return wrong result'); + +}, 'A_04_01_09_T12'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('p:only-child'), [ctx.d_p1], 'd.querySelectorAll(\'p:only-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('p:only-child'), [ctx.s1_p1], 's1.querySelectorAll(\'p:only-child\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('p:only-child'), [ctx.s2_p1], 's2.querySelectorAll(\'p:only-child\') return wrong result'); + +}, 'A_04_01_09_T13'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('div:empty'), [ctx.d_div2], 'd.querySelectorAll(\'div:empty\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('div:empty'), [ctx.s1_div2], 's1.querySelectorAll(\'div:empty\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('div:empty'), [ctx.s2_div2], 's2.querySelectorAll(\'div:empty\') return wrong result'); + +}, 'A_04_01_09_T14'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('p div'), [ctx.d_div2], 'd.querySelectorAll(\'p div\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('p div'), [ctx.s1_div2], 's1.querySelectorAll(\'p div\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('p div'), [ctx.s2_div2], 's2.querySelectorAll(\'p div\') return wrong result'); + +}, 'A_04_01_09_T15'); + +//check querySelectorAll with parent-child selection +test(function () { + var ctx = A_04_01_09.setup(); + + assert_nodelist_contents_equal_noorder(ctx.d.querySelectorAll('p > div'), [ctx.d_div2], 'd.querySelectorAll(\'p > div\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s1.querySelectorAll('p > div'), [ctx.s1_div2], 's1.querySelectorAll(\'p > div\') return wrong result'); + assert_nodelist_contents_equal_noorder(ctx.s2.querySelectorAll('p > div'), [ctx.s2_div2], 's2.querySelectorAll(\'p > div\') return wrong result'); + +}, 'A_04_01_09_T16'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-011.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-011.html new file mode 100644 index 0000000000..48ac1c7540 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/test-011.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_04_01_11</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation:The style sheets, represented by the shadow nodes are not accessible using shadow host document's CSSOM extensions"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +// check that <link> element added to head is not exposed +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + var initialStyleSheetsCount = d.styleSheets.length; + + var link = d.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + d.body.appendChild(link); + + //create Shadow root + var root = d.createElement('div'); + d.body.appendChild(root); + var s = root.attachShadow({mode: 'open'}); + + s.appendChild(link); + + assert_equals(d.styleSheets.length, initialStyleSheetsCount, 'stylesheet link elements in shadow DOM must not be ' + + 'exposed via the document.styleSheets collection'); + + +}), 'A_04_01_11_T2'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-001.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-001.html new file mode 100644 index 0000000000..6e4cbbd9b7 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-001.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Window object named properties: Frames</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:a.semenov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The nodes and named elements are not accessible from Window object named properties."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function () { + var host = document.createElement('div'); + try { + host.style.display = 'none'; + document.body.appendChild(host); + var shadowRoot = host.attachShadow({mode: 'open'}); + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.name = 'test-name'; + shadowRoot.appendChild(iframe); + assert_false('test-name' in window); + } finally { + if (host.parentNode) + host.parentNode.removeChild(host); + } +}, + 'An iframe element in a shadow tree should not be accessible from ' + + 'window\'s named properties with its "name" attribute value.' +); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-002.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-002.html new file mode 100644 index 0000000000..957e087e86 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-002.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Window object named properties: "name" attribute</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:a.semenov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The nodes and named elements are not accessible from Window object named properties."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function testNameAttribute(elementName) { + var doc = document.implementation.createHTMLDocument('Title'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var element = doc.createElement(elementName); + element.name = 'test-name'; + shadowRoot.appendChild(element); + assert_false('test-name' in window); +} + +var namedElements = [ + 'a', 'applet', 'area', 'embed', 'form', 'frameset', 'img', 'object' +]; + +var nameAttributeTestParameter = namedElements.map(function (elementName) { + return [ + '"' + elementName + '" element with name attribute in a shadow tree ' + + 'should not be accessible from window object\'s named property.', + elementName + ]; +}); + +generate_tests(testNameAttribute, nameAttributeTestParameter); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-003.html b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-003.html new file mode 100644 index 0000000000..00d3931266 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-003.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Window object named properties: "id" attribute</title> +<link rel="author" title="Aleksei Yu. Semenov" href="mailto:a.semenov@unipro.ru"> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="author" title="Yuta Kitamura" href="mailto:yutak@google.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#upper-boundary-encapsulation"> +<meta name="assert" content="Upper-boundary encapsulation: The nodes and named elements are not accessible from Window object named properties."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function testIDAttribute(elementName) { + var doc = document.implementation.createHTMLDocument('Title'); + var shadowRoot = doc.body.attachShadow({mode: 'open'}); + var element = doc.createElement(elementName); + element.id = 'test-id'; + shadowRoot.appendChild(element); + assert_false('test-id' in window); +} + +var idAttributeTestParameter = HTML5_ELEMENTS.map(function (elementName) { + return [ + '"' + elementName + '" element with id attribute in a shadow tree ' + + 'should not be accessible from window object\'s named property.', + elementName + ]; +}); + +generate_tests(testIDAttribute, idAttributeTestParameter); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001-ref.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001-ref.html new file mode 100644 index 0000000000..947c01a8f5 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001-ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test</title> +<link rel="author" title="Kazuhito Hokamura" href="mailto:k.hokamura@gmail.com"> +<style> +div { + width: 100px; + height: 100px; + background: green; +} +</style> +</head> +<body> +<p>Test passes if following box is green.</p> +<div></div> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001.html new file mode 100644 index 0000000000..99c130ef59 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/not-apply-in-shadow-root-001.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test - Tests CSS rules must not apply in a shadow root</title> +<link rel="match" href="not-apply-in-shadow-root-001-ref.html"> +<link rel="author" title="Kazuhito Hokamura" href="mailto:k.hokamura@gmail.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#styles"> +<meta name="assert" content="Styles: CSS rules declared in an enclosing tree must not apply in a shadow tree if apply-author-styles flag is not set."> +<script src="../../../html/resources/common.js"></script> +<style> +div { + width: 100px; + height: 100px; + background: green; +} +</style> +</head> +<body> +<p>Test passes if following box is green.</p> +<div id="shadow-host"></div> +<script> +var shadowHost = document.getElementById('shadow-host'); +var shadowRoot = shadowHost.attachShadow({mode: 'open'}); +var style = document.createElement('style'); +style.innerHTML = 'div { background: red }'; + +shadowRoot.appendChild(style); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-001.html new file mode 100644 index 0000000000..d773d248c1 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-001.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_06_00_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#styles"> +<meta name="assert" content="Styles: CSS rules declared in an enclosing tree must not apply in a shadow tree if apply-author-styles flag is set to false for this tree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../html/resources/common.js"></script> +<script src="../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//test apply-author-styles flag of ShadowRoot object (default value) +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + d.head.innerHTML = '<style>' + + '.invis {' + + 'display:none;' + + '}' + + '</style>'; + + var host = d.createElement('div'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var div1 = d.createElement('div'); + div1.innerHTML ='<span id="shd" class="invis">This is the shadow tree</span>'; + s.appendChild(div1); + + //apply-author-styles flag is false by default. Invisible style shouldn't be applied + assert_true(s.querySelector('#shd').offsetTop > 0, + 'CSS styles declared in enclosing tree must not be applied in a shadow tree ' + + 'if the apply-author-styles flag is set to false'); + + +}), 'A_06_00_01_T01'); + + +//test apply-author-styles flag of ShadowRoot object (set it) +test(unit(function (ctx) { + + var d = newRenderedHTMLDocument(ctx); + + d.head.innerHTML = '<style>' + + '.invis {' + + 'display:none;' + + '}' + + '</style>'; + + var host = d.createElement('div'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var div1 = d.createElement('div'); + div1.innerHTML ='<span id="shd" class="invis">This is the shadow tree</span>'; + s.appendChild(div1); + + //apply-author-styles flag is set to false. Invisible style shouldn't be applied + assert_true(s.querySelector('#shd').offsetTop > 0, + 'CSS styles declared in enclosing tree must not be applied in a shadow tree ' + + 'if the apply-author-styles flag is set to false'); + + +}), 'A_06_00_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/test-003.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-003.html new file mode 100644 index 0000000000..b5b2529861 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-003.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_06_00_03</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#styles"> +<meta name="assert" content="Styles: Each shadow root has an associated list of zero or more style sheets, named shadow root style sheets"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../html/resources/common.js"></script> +<script src="../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + var host = d.createElement('div'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + assert_equals(s.styleSheets.length, 0, 'There should be no style sheets'); +}), 'A_06_00_03_T01'); + + +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + var host = d.createElement('div'); + host.setAttribute('style', 'width:100px'); + d.body.appendChild(host); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + assert_equals(s.styleSheets.length, 0, 'There should be no style sheets'); +}), 'A_06_00_03_T02'); + +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + var host = d.createElement('div'); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var style = d.createElement('style'); + style.textContent = 'div {width: 50%;}'; + s.appendChild(style); + + d.body.appendChild(host); + assert_equals(s.styleSheets.length, 1, 'Style sheet is not accessible via styleSheets'); +}), 'A_06_00_03_T03'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/test-005.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-005.html new file mode 100644 index 0000000000..94dbb551dc --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-005.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_06_00_06</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#styles"> +<meta name="assert" content="Styles:CSS rules declared in a shadow root style sheets must not apply in the document tree,"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../html/resources/common.js"></script> +<script src="../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +//check querySelector method +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + d.body.innerHTML = + '<div>' + + '<span class="invis" id="theTreeSpan">This is an element in the document tree</span>' + + '</div>' + + '<div id="sr">' + + '</div>'; + + var host = d.querySelector('#sr'); + + //Shadow root to play with + var s = host.attachShadow({mode: 'open'}); + + var style = d.createElement('style'); + style.innerHTML ='.invis {display:none}'; + s.appendChild(style); + + var span = d.createElement('span'); + span.setAttribute('id', 'theShadowSpan'); + span.setAttribute('class', 'invis'); + s.appendChild(span); + + //theTreeSpan should be visible, theShadowSpan not + assert_true(d.querySelector('#theTreeSpan').offsetTop > 0, + 'CSS styles declared in shadow tree must not be applied to the elements ' + + 'in the document tree'); + + //theTreeSpan should be visible, theShadowSpan not + assert_equals(s.querySelector('#theShadowSpan').offsetTop, 0, + 'CSS styles declared in shadow tree must be applied to the element ' + + 'in the same shadow tree'); + +}), 'A_06_00_06_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/styles/test-008.html b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-008.html new file mode 100644 index 0000000000..4b810af1c8 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/styles/test-008.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_06_00_09</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#styles"> +<meta name="assert" content="Styles:the styles of the shadow host are inherited by the children of the shadow root"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../html/resources/common.js"></script> +<script src="../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + d.body.innerHTML = '' + + '<div id="shHost" style="font-size:10px">' + + '<span id="spn1">This is a shadow host child</span>' + + '</div>'; + + var host = d.querySelector('#shHost'); + + var s = host.attachShadow({mode: 'open'}); + + var div = d.createElement('div'); + div.innerHTML ='<span id="spn2">This is a shadow root child</span>'; + s.appendChild(div); + + assert_equals(d.querySelector('#spn1').offsetTop, 0, + 'Element should not be rendered'); + assert_true(s.querySelector('#spn2').offsetTop > 0, + 'Element should be rendered'); + + var oldHeight = s.querySelector('#spn2').offsetHeight; + + host.setAttribute('style', 'font-size:20px'); + + assert_true(s.querySelector('#spn2').offsetHeight > oldHeight, + 'Shadow host style must be aplied to the shadow root children'); + +}), 'A_06_00_09_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-001.html new file mode 100644 index 0000000000..9d4b026ad8 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-001.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_07_03_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#active-element"> +<meta name="assert" content="User Interaction: each shadow root must also have an activeElement property to store the value of the focused element in the shadow tree."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpId'); + inp.setAttribute('value', 'Some text'); + s.appendChild(inp); + + inp.focus(); + + assert_equals(s.activeElement.tagName, 'INPUT', 'Point 1:activeElement property of shadow root ' + + 'must return the value of the focused element in the shadow tree'); + assert_equals(s.activeElement.getAttribute('id'), 'inpId', 'Point 2:activeElement property of shadow root ' + + 'must return the value of the focused element in the shadow tree'); + +}), 'A_07_03_01_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-002.html new file mode 100644 index 0000000000..af3165afeb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/active-element/test-002.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_07_03_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#active-element"> +<meta name="assert" content="User Interaction: Document's activeElement property must be adjusted"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.setAttribute('id', 'shRoot'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var inp = d.createElement('input'); + inp.setAttribute('type', 'text'); + inp.setAttribute('id', 'inpId'); + inp.setAttribute('value', 'Some text'); + s.appendChild(inp); + + inp.focus(); + + assert_equals(d.activeElement.tagName, 'DIV', 'Point 1: document\'s activeElement property ' + + 'must return adjusted the value of the focused element in the shadow tree'); + assert_equals(d.activeElement.getAttribute('id'), 'shRoot', 'Point 2: document\'s activeElement property ' + + 'must return adjusted the value of the focused element in the shadow tree'); + +}), 'A_07_03_02_T01'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/editing/inheritance-of-content-editable-001.html b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/editing/inheritance-of-content-editable-001.html new file mode 100644 index 0000000000..c09832cd7e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/editing/inheritance-of-content-editable-001.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: Inheritance of contentEditable attribute</title> +<link rel="author" title="Moto Ishizawa" href="mailto:summerwind.jp@gmail.com"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#editing"> +<meta name="assert" content="User Interaction: Shadow trees must not be propagated contentEditable attribute from shadow host"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.contentEditable = "true"; + d.body.appendChild(host); + + var s = host.attachShadow({mode: 'open'}); + + assert_equals(host.contentEditable, "true"); + assert_equals(s.contentEditable, undefined); +}), 'contentEditable of shadow trees must be undefined when contentEditable attribute of shadow host is "true"'); + +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + host.contentEditable = "false"; + d.body.appendChild(host); + + var s = host.attachShadow({mode: 'open'}); + + assert_equals(host.contentEditable, 'false'); + assert_equals(s.contentEditable, undefined); +}), 'contentEditable of shadow trees must be undefined when contentEditable of shadow host is "false"'); + +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + d.body.contentEditable = "true"; + + var s = host.attachShadow({mode: 'open'}); + + assert_equals(host.contentEditable, 'inherit'); + assert_equals(s.contentEditable, undefined); +}), 'contentEditable of shadow trees must be undefined when contentEditable attribute of shadow host is "inherit"'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-001.html b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-001.html new file mode 100644 index 0000000000..3a234e8b81 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-001.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_07_01_01</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#ranges-and-selection"> +<meta name="assert" content="User Interaction: Selection, returned by the window.getSelection() method must never return a selection within a shadow tree"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + var range = d.createRange(); + range.setStart(span.firstChild, 0); + range.setEnd(span.firstChild, 3); + + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + var sl = window.getSelection(); + assert_equals(sl.toString(), '', 'window.getSelection() method must never return a selection ' + + 'within a shadow tree'); + +}), 'A_07_07_01_T01'); + + +// test distributed nodes +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + span.setAttribute('slot', 'span'); + host.appendChild(span); + + var s = host.attachShadow({mode: 'open'}); + s.innerHTML = '<slot name="span"></slot>'; + + var range = d.createRange(); + range.setStart(span.firstChild, 0); + range.setEnd(span.firstChild, 3); + + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + var sl = window.getSelection(); + assert_equals(sl.toString(), '', 'window.getSelection() method must never return a selection ' + + 'within a shadow tree'); + +}), 'A_07_07_01_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-002.html b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-002.html new file mode 100644 index 0000000000..55cd91e458 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/untriaged/user-interaction/ranges-and-selections/test-002.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM Test: A_07_01_02</title> +<link rel="author" title="Sergey G. Grekhov" href="mailto:sgrekhov@unipro.ru"> +<link rel="help" href="http://www.w3.org/TR/2013/WD-shadow-dom-20130514/#ranges-and-selection"> +<meta name="assert" content="User Interaction: The getSelection() method of the shadow root object must return the current selection in this shadow tree."> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../../../html/resources/common.js"></script> +<script src="../../../resources/shadow-dom-utils.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + var s = host.attachShadow({mode: 'open'}); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + s.appendChild(span); + + var range = d.createRange(); + range.setStart(span.firstChild, 0); + range.setEnd(span.firstChild, 3); + + var selection = d.defaultView.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + var sl = s.getSelection(); + assert_equals(sl.toString(), 'Som', 'The getSelection() method of the shadow root object must return ' + + 'the current selection in this shadow tree'); + +}), 'A_07_01_02_T01'); + + +//test distributed nodes +test(unit(function (ctx) { + var d = newRenderedHTMLDocument(ctx); + + var host = d.createElement('div'); + d.body.appendChild(host); + + var span = d.createElement('span'); + span.innerHTML = 'Some text'; + span.setAttribute('slot', 'slot'); + host.appendChild(span); + + var s = host.attachShadow({mode: 'open'}); + s.innerHTML = '<slot name="slot"></slot>'; + + var range = d.createRange(); + range.setStart(span.firstChild, 0); + range.setEnd(span.firstChild, 3); + + var selection = d.defaultView.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + var sl = s.getSelection(); + assert_equals(sl.toString(), 'Som', 'The getSelection() method of the shadow root object must return ' + + 'the current selection in this shadow tree'); + +}), 'A_07_07_02_T02'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/shadow-dom/user-agent-shadow-root-crash.html b/testing/web-platform/tests/shadow-dom/user-agent-shadow-root-crash.html new file mode 100644 index 0000000000..bd90b04f29 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/user-agent-shadow-root-crash.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="help" href="https://crbug.com/1205246"> +<meta name="assert" content="The renderer should not crash."> + +This test passes if it does not crash. + +<details> sample1 +<marquee dir="auto">marquee1</marquee> +<marquee dir="auto">marquee2</marquee> +<marquee dir="auto">marquee3</marquee> +<marquee dir="auto">marquee4</marquee> +<marquee dir="auto">marquee5</marquee> +<marquee dir="auto">marquee6</marquee> +<marquee dir="auto">marquee7</marquee> +<marquee dir="auto">marquee8</marquee> +<marquee dir="auto">marquee9</marquee> +</details> |