| <!DOCTYPE html> | |
| <head> | |
| <title>Test custom element callbacks with Sanitizer</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| </head> | |
| <body> | |
| <div></div> | |
| <script> | |
| window.constructed = 0; | |
| window.connected = 0; | |
| function cleanup() { | |
| window.constructed = 0; | |
| window.connected = 0; | |
| } | |
| customElements.define("x-probe", class extends HTMLElement { | |
| constructor() { super(); window.constructed++; } | |
| connectedCallback() { window.connected++; } | |
| }); | |
| // Test cases involving named custom elements. | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<x-probe></x-probe>"); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element that is removed during sanitization."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<x-probe></x-probe>", | |
| {sanitizer: {elements: ["x-probe"]}}); | |
| assert_equals(window.constructed, 1); | |
| assert_equals(window.connected, 1); | |
| }, "Custom element that is allowed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const div = document.createElement("div"); | |
| div.setHTML("<x-probe></x-probe>", {sanitizer: {elements: ["x-probe"]}}); | |
| assert_equals(window.constructed, 1, "constructed before setHTML() returns"); | |
| assert_equals(window.connected, 0); | |
| assert_equals(div.firstChild.constructor.name, "", "upgraded element is not HTMLElement"); | |
| }, "Custom element that is allowed, on a disconnected element."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const div = document.createElement("div"); | |
| div.setHTMLUnsafe("<x-probe></x-probe>"); | |
| assert_equals(window.constructed, 1, "constructed before setHTMLUnsafe() returns"); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element on a disconnected element, setHTMLUnsafe()."); | |
| for (const method of ["setHTML", "setHTMLUnsafe"]) { | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const div = document.createElement("div"); | |
| div[method]("<span><x-probe></x-probe></span>", | |
| {sanitizer: {elements: ["x-probe"], replaceWithChildrenElements: ["span"]}}); | |
| assert_equals(div.firstChild.localName, "x-probe"); | |
| assert_equals(window.constructed, 1, `constructed before ${method}() returns`); | |
| assert_equals(window.connected, 0); | |
| }, `Custom element inside an element that is replaced with its children, on a disconnected element, ${method}().`); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const target = document.body.firstElementChild; | |
| target[method]("<span><b><x-probe></x-probe></b></span>", | |
| {sanitizer: {elements: ["x-probe"], replaceWithChildrenElements: ["span", "b"]}}); | |
| assert_equals(target.firstChild.localName, "x-probe"); | |
| assert_equals(window.constructed, 1); | |
| assert_equals(window.connected, 1); | |
| }, `Custom element inside nested elements that are replaced with their children, ${method}().`); | |
| test(t => { | |
| const scopedLog = []; | |
| const registry = new CustomElementRegistry(); | |
| class ScopedProbe extends HTMLElement { | |
| constructor() { super(); scopedLog.push("scoped"); } | |
| } | |
| registry.define("x-probe", ScopedProbe); | |
| t.add_cleanup(cleanup); | |
| const host = document.createElement("div"); | |
| const shadow = host.attachShadow({mode: "open", customElementRegistry: registry}); | |
| shadow[method]("<span><x-probe></x-probe></span>", | |
| {sanitizer: {elements: ["x-probe"], replaceWithChildrenElements: ["span"]}}); | |
| const probe = shadow.firstChild; | |
| assert_equals(probe.localName, "x-probe"); | |
| assert_equals(probe.customElementRegistry, registry, "element uses the scoped registry"); | |
| assert_true(probe instanceof ScopedProbe, "upgraded with the scoped definition"); | |
| assert_array_equals(scopedLog, ["scoped"]); | |
| assert_equals(window.constructed, 0, "global definition is not used"); | |
| }, `Custom element inside an element that is replaced with its children uses the target's scoped registry, ${method}().`); | |
| } | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const div = document.createElement("div"); | |
| div.setHTMLUnsafe( | |
| "<span><template shadowrootmode=open><x-probe></x-probe></template></span>", | |
| {sanitizer: {removeElements: ["span"]}}); | |
| assert_equals(div.childNodes.length, 0); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element in a declarative shadow root on a removed host is not constructed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| const div = document.createElement("div"); | |
| div.setHTMLUnsafe("<span><x-probe></x-probe></span>", | |
| {sanitizer: {removeElements: ["span"]}}); | |
| assert_equals(div.childNodes.length, 0); | |
| assert_equals(window.constructed, 0); | |
| }, "Custom element inside a removed element is not constructed."); | |
| const attrLog = []; | |
| customElements.define("x-attrs", class extends HTMLElement { | |
| static observedAttributes = ["a", "b"]; | |
| constructor() { super(); attrLog.push("constructor"); } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| attrLog.push(`attributeChanged ${name} ${oldValue} ${newValue}`); | |
| } | |
| connectedCallback() { attrLog.push("connected"); } | |
| }); | |
| test(t => { | |
| attrLog.length = 0; | |
| const target = document.body.firstElementChild; | |
| target.setHTML('<x-attrs a="1" b="2"></x-attrs>', | |
| {sanitizer: {elements: ["x-attrs"], attributes: ["a"]}}); | |
| assert_array_equals(attrLog, | |
| ["constructor", "attributeChanged a null 1", "connected"]); | |
| assert_equals(target.firstChild.getAttribute("a"), "1"); | |
| assert_false(target.firstChild.hasAttribute("b")); | |
| }, "attributeChangedCallback only fires for attributes the sanitizer keeps."); | |
| // Test cases involving `is=`. | |
| customElements.define("x-div", class extends HTMLDivElement { | |
| constructor() { super(); window.constructed++; } | |
| connectedCallback() { window.connected++; } | |
| }, { extends: "div" }); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>"); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is not default-allowed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>", | |
| {sanitizer: {elements:["div"], removeAttributes:["is"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is removed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>", | |
| {sanitizer: {elements:["div", "x-div"], removeAttributes:["is"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is removed but the custom element would have been allowed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<x-probe is='x-div'></x-probe>", | |
| {sanitizer: {elements:["div", "x-probe"], removeAttributes:["is"]}}); | |
| assert_equals(window.constructed, 1); | |
| assert_equals(window.connected, 1); | |
| }, "Custom element with an is-attribute, where the is=-attribute is blocked."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>", | |
| {sanitizer: {elements:["p", "x-div"], attributes:["is", "data-attr"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is allowed, but the element is not."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>", | |
| {sanitizer: {removeElements:["div"], attributes:["is", "data-attr"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is allowed, but the element is removed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML("<div is='x-div'></div>", | |
| {sanitizer: {replaceWithChildrenElements:["div"], attributes:["is", "data-attr"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute, where is attribute is allowed, but the element is replaced."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTMLUnsafe("<div is='x-div'></div>", | |
| {sanitizer: "default"}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute with unsafe, where is attribute is not default-allowed."); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTML( | |
| "<div is='x-div'></div>", | |
| {sanitizer: {elements:["p", "div"], attributes:["is"]}}); | |
| assert_equals(window.constructed, 1); | |
| assert_equals(window.connected, 1); | |
| }, "Sanity check, that is= processing still works when allowed."); | |
| // Test cases involving `is=` with <html> or <script> | |
| customElements.define("x-script", class extends HTMLScriptElement { | |
| constructor() { super(); window.constructed++; } | |
| connectedCallback() { window.connected++; } | |
| }, {extends: "script"}); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTMLUnsafe( | |
| "<script is='x-script'>2+2</sc" + "ript>", | |
| {sanitizer: {elements: ["script"],removeAttributes:["is"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute on script element, where is attribute is not allowed."); | |
| customElements.define("x-html", class extends HTMLHtmlElement { | |
| constructor() { super(); window.constructed++; } | |
| connectedCallback() { window.connected++; } | |
| }, {extends: "html"}); | |
| test(t => { | |
| t.add_cleanup(cleanup); | |
| document.body.firstElementChild.setHTMLUnsafe( | |
| "<html is='x-html'></html>", | |
| {sanitizer: {elements: ["html"],removeAttributes:["is"]}}); | |
| assert_equals(window.constructed, 0); | |
| assert_equals(window.connected, 0); | |
| }, "Custom element via is-attribute on html element, where is attribute is not default-allowed."); | |
| </script> | |
| </body> |