Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
XML Parser should invoke reactions when creating/inserting new custom…
… elements https://bugs.webkit.org/show_bug.cgi?id=188190 Reviewed by Chris Dumez. Based on a patch written by Frédéric Wang. Made our XML parser respect custom elements semantics. Namely, perform a microtask checkpoint before constructing a custom element and throw exceptions in document.open, document.write, document.writeln, document.close inside custom element constructors and reaction callbacks. Also fixed a bug that enqueueUpgradeInShadowIncludingTreeOrder and JSCustomElementInterface::upgradeElement were checking the identity of qualified names instead of whether they match or not (ignoring prefix string). * LayoutTests/fast/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser-expected.txt: Added. * LayoutTests/fast/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml: Added. * LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser-expected.txt: Added. * LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml: Added. * LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser-expected.txt: Added. * LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml: Added. * LayoutTests/imported/w3c/web-platform-tests/shadow-dom/innerHTML-setter-expected.txt: * LayoutTests/platform/win/TestExpectations: * Source/WebCore/bindings/js/JSCustomElementInterface.cpp: (WebCore::JSCustomElementInterface::upgradeElement): * Source/WebCore/dom/CustomElementRegistry.cpp: (WebCore::enqueueUpgradeInShadowIncludingTreeOrder): * Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp: (WebCore::XMLDocumentParser::startElementNs): Canonical link: https://commits.webkit.org/253122@main
- Loading branch information
Showing
11 changed files
with
453 additions
and
4 deletions.
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
.../custom-elements/perform-microtask-checkpoint-before-construction-xml-parser-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
PASS XML parser must perform a microtask checkpoint before constructing a custom element | ||
|
94 changes: 94 additions & 0 deletions
94
...ts/fast/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | ||
<head> | ||
<title>Custom Elements: create an element for a token must perform a microtask checkpoint</title> | ||
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" /> | ||
<meta name="assert" content="When the HTML parser creates an element for a token, it must perform a microtask checkpoint before invoking the constructor" /> | ||
<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" /> | ||
<meta name="help" content="https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint" /> | ||
<script src="../../resources/testharness.js"></script> | ||
<script src="../../resources/testharnessreport.js"></script> | ||
</head> | ||
<body> | ||
<div id="log"></div> | ||
<script> | ||
<![CDATA[ | ||
|
||
function create_window_in_test(test, doc) { | ||
return new Promise((resolve) => { | ||
let iframe = document.createElement('iframe'); | ||
blob = new Blob([doc], {type: 'application/xml'}); | ||
iframe.src = URL.createObjectURL(blob); | ||
iframe.onload = (event) => { | ||
let contentWindow = iframe.contentWindow; | ||
test.add_cleanup(() => iframe.remove()); | ||
resolve(contentWindow); | ||
}; | ||
document.body.appendChild(iframe); | ||
}); | ||
} | ||
|
||
async function construct_custom_element_in_parser(test, markup) | ||
{ | ||
const window = await create_window_in_test(test, `<?xml version="1.0" encoding="utf-8"?> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | ||
<body><script> | ||
class SomeElement extends HTMLElement { | ||
constructor() { | ||
super(); | ||
window.recordsListInConstructor = recordsList.map((records) => records.slice(0)); | ||
} | ||
} | ||
customElements.define('some-element', SomeElement); | ||
const recordsList = []; | ||
const observer = new MutationObserver((records) => { | ||
recordsList.push(records); | ||
}); | ||
observer.observe(document.body, {childList: true, subtree: true}); | ||
window.onload = () => { | ||
window.recordsListInDOMContentLoaded = recordsList.map((records) => records.slice(0)); | ||
} | ||
</scr` + `ipt>${markup}</body></html>`); | ||
return window; | ||
} | ||
|
||
promise_test(async function () { | ||
const contentWindow = await construct_custom_element_in_parser(this, '<b><some-element></some-element></b>'); | ||
const contentDocument = contentWindow.document; | ||
|
||
let recordsList = contentWindow.recordsListInConstructor; | ||
assert_true(Array.isArray(recordsList)); | ||
assert_equals(recordsList.length, 1); | ||
assert_true(Array.isArray(recordsList[0])); | ||
assert_equals(recordsList[0].length, 1); | ||
let record = recordsList[0][0]; | ||
assert_equals(record.type, 'childList'); | ||
assert_equals(record.target, contentDocument.body); | ||
assert_equals(record.previousSibling, contentDocument.querySelector('script')); | ||
assert_equals(record.nextSibling, null); | ||
assert_equals(record.removedNodes.length, 0); | ||
assert_equals(record.addedNodes.length, 1); | ||
assert_equals(record.addedNodes[0], contentDocument.querySelector('b')); | ||
|
||
recordsList = contentWindow.recordsListInDOMContentLoaded; | ||
assert_true(Array.isArray(recordsList)); | ||
assert_equals(recordsList.length, 2); | ||
assert_true(Array.isArray(recordsList[1])); | ||
assert_equals(recordsList[1].length, 1); | ||
record = recordsList[1][0]; | ||
assert_equals(record.type, 'childList'); | ||
assert_equals(record.target, contentDocument.querySelector('b')); | ||
assert_equals(record.previousSibling, null); | ||
assert_equals(record.nextSibling, null); | ||
assert_equals(record.removedNodes.length, 0); | ||
assert_equals(record.addedNodes.length, 1); | ||
assert_equals(record.addedNodes[0], contentDocument.querySelector('some-element')); | ||
}, 'XML parser must perform a microtask checkpoint before constructing a custom element'); | ||
|
||
]]> | ||
</script> | ||
</body> | ||
</html> |
13 changes: 13 additions & 0 deletions
13
...stom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
PASS document.open() must throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.close() must throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.write must throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.writeln must throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element | ||
PASS document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element | ||
|
147 changes: 147 additions & 0 deletions
147
...fast/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | ||
<head> | ||
<title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title> | ||
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" /> | ||
<meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token" /> | ||
<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" /> | ||
<meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter" /> | ||
<script src="../../resources/testharness.js"></script> | ||
<script src="../../resources/testharnessreport.js"></script> | ||
</head> | ||
<body> | ||
<div id="log"></div> | ||
<script> | ||
<![CDATA[ | ||
|
||
function create_window_in_test(test, doc, type) { | ||
return new Promise((resolve) => { | ||
let iframe = document.createElement('iframe'); | ||
blob = new Blob([doc], {type}); | ||
iframe.src = URL.createObjectURL(blob); | ||
iframe.onload = (event) => { | ||
let contentWindow = iframe.contentWindow; | ||
test.add_cleanup(() => iframe.remove()); | ||
resolve(contentWindow); | ||
}; | ||
document.body.appendChild(iframe); | ||
}); | ||
} | ||
|
||
async function construct_custom_element_in_parser(test, code) | ||
{ | ||
window.executed = false; | ||
window.exception = false; | ||
const content_window = await create_window_in_test(test, `<?xml version="1.0" encoding="utf-8"?> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | ||
<head> | ||
<script> | ||
<![CDATA[ | ||
let executed = false; | ||
let exception = null; | ||
class CustomElement extends window.HTMLElement { | ||
constructor() { | ||
super(); | ||
try { | ||
${code} | ||
} catch (error) { | ||
exception = error; | ||
} | ||
executed = true; | ||
} | ||
} | ||
customElements.define('some-element', CustomElement); | ||
]]` + `> | ||
</` + `script> | ||
</head> | ||
<body> | ||
<some-element></some-element> | ||
<script> | ||
top.executed = executed; | ||
top.exception = exception; | ||
</script> | ||
</body> | ||
</html>`, 'application/xml'); | ||
let content_document; | ||
try { | ||
content_document = content_window.document; | ||
} catch (error) { } | ||
assert_true(executed, 'Must synchronously instantiate a custom element'); | ||
return {window: content_window, document: content_document, exception}; | ||
} | ||
|
||
promise_test(async function () { | ||
const result = await construct_custom_element_in_parser(this, `document.open()`); | ||
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError'); | ||
}, 'document.open() must throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
const result = await construct_custom_element_in_parser(this, `document.open('text/html')`); | ||
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError'); | ||
}, 'document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window | ||
promise_test(async function () { | ||
let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data)); | ||
const url = top.location.href.substring(0, top.location.href.lastIndexOf('/')) + '/resources/navigation-destination.html'; | ||
const result = await construct_custom_element_in_parser(this, `document.open('${url}', '_self', '')`); | ||
assert_equals(result.exception, null); | ||
assert_equals(await load_promise, 'didNavigate'); | ||
}, 'document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
const result = await construct_custom_element_in_parser(this, `document.close()`); | ||
assert_not_equals(result.exception, null); | ||
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError'); | ||
}, 'document.close() must throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
const result = await construct_custom_element_in_parser(this, `document.write('<b>some text</b>')`); | ||
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError'); | ||
assert_equals(result.document.querySelector('b'), null, 'Must not insert new content'); | ||
assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content'); | ||
}, 'document.write must throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
const result = await construct_custom_element_in_parser(this, `document.writeln('<b>some text</b>')`); | ||
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError'); | ||
assert_equals(result.document.querySelector('b'), null, 'Must not insert new content'); | ||
assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content'); | ||
}, 'document.writeln must throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html'); | ||
const result = await construct_custom_element_in_parser(this, `top.another_window.document.open()`); | ||
assert_equals(result.exception, null); | ||
}, 'document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html'); | ||
const result = await construct_custom_element_in_parser(this, `top.another_window.document.open('text/html')`); | ||
assert_equals(result.exception, null); | ||
}, 'document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html'); | ||
const result = await construct_custom_element_in_parser(this, `top.another_window.document.close()`); | ||
assert_equals(result.exception, null); | ||
}, 'document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html'); | ||
const result = await construct_custom_element_in_parser(this, `top.another_window.document.write('<b>some text</b>')`); | ||
assert_equals(result.exception, null); | ||
assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>'); | ||
}, 'document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
promise_test(async function () { | ||
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html'); | ||
const result = await construct_custom_element_in_parser(this, `top.another_window.document.writeln('<b>some text</b>')`); | ||
assert_equals(result.exception, null); | ||
assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>'); | ||
}, 'document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element'); | ||
|
||
]]> | ||
</script> | ||
</body> | ||
</html> |
13 changes: 13 additions & 0 deletions
13
...stom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
PASS document.open() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.open("text/html") must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.open(URL) must NOT throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.close() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.write must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.writeln must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.open() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.open("text/html") of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.close() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.write of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
PASS document.writeln of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element | ||
|
Oops, something went wrong.