Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions html/syntax/parsing/adoption-agency-reparenting-cycle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency: move that would create a cycle (furthest block contains common ancestor)</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// Isolated because it has crashed implementations.
//
// <div id=ca><b><p id=fb>: commonAncestor is #ca, furthestBlock is #fb. The
// inline script detaches #fb, then moves #ca *inside* #fb. The algorithm's
// step "Insert lastNode ... at the adjusted insertion location" then tries to
// insert #fb into #ca (now its own descendant) — a cycle. The prose has no
// guard; WebKit skips the move, dropping the subtree.
promise_test(async t => {
const iframe = await parseInIframe(t, `<div id=ca><b><p id=fb><script>window.fb=document.getElementById('fb');window.ca=document.getElementById('ca');document.body.appendChild(window.fb);window.fb.appendChild(window.ca)<\/script></b></div>`);
const w = iframe.contentWindow;
assert_equals(tree(iframe.contentDocument.body), ``,
"main document body is empty (the cycle-inducing move was dropped)");
assert_false(w.fb.isConnected, "furthest block ends up detached");
assert_false(w.ca.isConnected, "common ancestor ends up detached");
}, "Adoption agency move that would create a cycle (furthest block contains common ancestor)");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency: foster target is a Document with an element child</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// Isolated in case implementations crash.
//
// The common ancestor is a <tr> (a foster-parenting element), so the "Insert
// lastNode ... at the adjusted insertion location" step foster-parents relative
// to the last <table> on the stack. Script makes that <table> the document
// element of a second Document, so the foster target is a Document that already
// has an element child. lastNode (<div id=fb>) must be dropped on the floor, the
// same way an element inserted into such a Document is.
promise_test(async t => {
const iframe = await parseInIframe(t, `<table><tbody><tr><b><div id=fb><script>window.fb=document.getElementById('fb');window.other=document.implementation.createHTMLDocument('');window.other.documentElement.remove();window.other.appendChild(document.querySelector('table'))<\/script></b></table>`);
const w = iframe.contentWindow;
assert_equals(tree(iframe.contentDocument.body), `<b>`, "main document body");
assert_equals(w.other.childElementCount, 1,
"the second Document keeps only the <table> as its element child");
assert_false(w.fb.isConnected, "the moved node is dropped (detached)");
}, "Adoption agency foster target is a Document that already has an element child");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency: cycle after an inner-loop iteration</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// Isolated because it has crashed implementations.
//
// Like the cycle test, but with an extra formatting element (<i>) between the
// formatting element and the furthest block, so the inner loop runs an iteration
// (cloning <i> and appending the furthest block to it) before the cycle-inducing
// "Insert lastNode ... at the adjusted insertion location" step. The script
// detaches #fb then moves #ca inside it, so lastNode ends up containing the
// common ancestor.
promise_test(async t => {
const iframe = await parseInIframe(t, `<div id=ca><b><i><p id=fb><script>window.fb=document.getElementById('fb');window.ca=document.getElementById('ca');document.body.appendChild(window.fb);window.fb.appendChild(window.ca)<\/script></b></div>`);
const w = iframe.contentWindow;
assert_equals(tree(iframe.contentDocument.body), ``,
"main document body is empty (the cycle-inducing move was dropped)");
assert_false(w.fb.isConnected, "furthest block ends up detached");
assert_false(w.ca.isConnected, "common ancestor ends up detached");
}, "Adoption agency cycle reached after an inner-loop iteration");
</script>
26 changes: 26 additions & 0 deletions html/syntax/parsing/adoption-agency-reparenting-shadow-cycle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency: host-including (shadow-crossing) cycle</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// Isolated in case implementations crash.
//
// Like the cycle test, but the cycle crosses a shadow boundary: script puts the
// common ancestor (#ca) inside a shadow root whose host is a descendant of the
// furthest block (#fb). Inserting #fb into #ca is then a host-including
// inclusive-ancestor cycle, but *not* a same-tree containment (the target is in a
// shadow tree). A guard that only checks same-tree containment would miss it and
// build a cross-boundary cycle. The furthest block must be dropped.
promise_test(async t => {
const iframe = await parseInIframe(t, `<div id=ca><b><p id=fb><script>window.fb=document.getElementById('fb');window.ca=document.getElementById('ca');var host=document.createElement('span');var root=host.attachShadow({mode:'open'});document.body.appendChild(window.fb);window.fb.appendChild(host);root.appendChild(window.ca)<\/script></b></div>`);
const w = iframe.contentWindow;
assert_equals(tree(iframe.contentDocument.body), ``, "main document body is empty");
assert_false(w.ca.contains(w.fb),
"furthest block was not moved into the common ancestor");
assert_equals(w.fb.parentNode, null, "furthest block ends up parentless (dropped)");
}, "Adoption agency move that would create a host-including (shadow-crossing) cycle");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency: host-including cycle via a template's contents</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// Isolated in case implementations crash.
//
// Same host-including cycle as the shadow variant, but using a <template>'s
// contents instead of a shadow root. A template's content DocumentFragment has
// its host set to the template element, so a node in the content has the template
// (and thus the furthest block above it) as a host-including inclusive ancestor,
// even though Node.contains() (same-tree) does not cross into template contents.
// Unlike the shadow case this does not crash, because template contents are an
// inert separate document (never composed), so the cycle is silent: an engine
// that only checks same-tree containment moves #fb into #ca rather than dropping
// it. Per ensure pre-insert validity that move is invalid, so #fb must be dropped.
promise_test(async t => {
const iframe = await parseInIframe(t, `<div id=ca><b><p id=fb><script>window.fb=document.getElementById('fb');window.ca=document.getElementById('ca');var tmpl=document.createElement('template');document.body.appendChild(window.fb);window.fb.appendChild(tmpl);tmpl.content.appendChild(window.ca)<\/script></b></div>`);
const w = iframe.contentWindow;
assert_equals(tree(iframe.contentDocument.body), ``, "main document body is empty");
assert_false(w.ca.contains(w.fb),
"furthest block was not moved into the common ancestor");
assert_equals(w.fb.parentNode, null, "furthest block ends up parentless (dropped)");
}, "Adoption agency move that would create a host-including cycle via template contents");
</script>
70 changes: 70 additions & 0 deletions html/syntax/parsing/adoption-agency-reparenting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Adoption agency with nodes reparented by script during parsing</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/adoption-agency-reparenting.js"></script>
<body>
<script>
// The adoption agency algorithm moves *existing* nodes (the furthest block and
// its ancestors up to the common ancestor), assuming the DOM tree still matches
// the stack of open elements. An inline <script> that runs before the misnested
// </b> can reparent those nodes first. These are the cases where the tree stays
// well-formed (no cycle); the cycle-inducing variants live in separate files
// (adoption-agency-reparenting-*.html) since they have crashed implementations.
//
// Baseline (no script), from the spec's worked example for <b>1<p>2</b>3</p>:
// body > b["1"], p > (b["2"], "3")

promise_test(async t => {
const iframe = await parseInIframe(t, `<b>1<p>2</b>3</p>`);
assert_equals(tree(iframe.contentDocument.body), `<b>["1"]<p>[<b>["2"]"3"]`);
}, "Baseline: adoption agency for <b>1<p>2</b>3</p> (no script)");

// The furthest block (<p id=fb>) is moved into another Document before </b>.
// Expectation assumes the algorithm re-inserts it into the common ancestor (the
// original body), leaving the other Document's body empty.
promise_test(async t => {
const iframe = await parseInIframe(t, `<b>1<p id=fb>2<script>window.other=document.implementation.createHTMLDocument('');window.other.body.appendChild(document.getElementById('fb'))<\/script></b>3</p>`);
const other = iframe.contentWindow.other;
const fb = iframe.contentDocument.getElementById("fb") || other.getElementById("fb");
assert_equals(fb && fb.ownerDocument === iframe.contentDocument ? "main" : "other",
"main", "furthest block's document after the algorithm runs");
assert_equals(tree(iframe.contentDocument.body), `<b>["1"]<p#fb>[<b>["2"<script>]"3"]`,
"main document body");
assert_equals(tree(other.body), ``, "other document body");
}, "Furthest block adopted into another Document before </b>");

// The common ancestor (<div id=ca>) is moved into another Document before </b>,
// taking the whole subtree with it, so the algorithm runs entirely in the other
// Document and the main document body is left empty.
promise_test(async t => {
const iframe = await parseInIframe(t, `<div id=ca><b>1<p id=fb>2<script>window.other=document.implementation.createHTMLDocument('');window.other.body.appendChild(document.getElementById('ca'))<\/script></b>3</p></div>`);
const other = iframe.contentWindow.other;
assert_equals(tree(iframe.contentDocument.body), ``, "main document body");
assert_equals(tree(other.body), `<div#ca>[<b>["1"]<p#fb>[<b>["2"<script>]"3"]]`,
"other document body");
}, "Common ancestor adopted into another Document before </b>");

// The furthest block (<p id=fb>) is detached (removed) before </b>. Expectation
// assumes the algorithm re-inserts it into the common ancestor.
promise_test(async t => {
const iframe = await parseInIframe(t, `<b>1<p id=fb>2<script>document.getElementById('fb').remove()<\/script></b>3</p>`);
assert_equals(tree(iframe.contentDocument.body), `<b>["1"]<p#fb>[<b>["2"<script>]"3"]`,
"main document body");
}, "Furthest block detached before </b>");

// The common ancestor is a <tr>, a foster-parenting element, so the "Insert
// lastNode ... at the adjusted insertion location" step foster-parents. The <b>
// and <div id=fb> are themselves foster-parented during parsing (so the tree
// already does not match the stack); the script detaches the furthest block
// before </b>, and the <script>, being a child of the furthest block, is moved
// into the new inner <b> by the take-all-children step.
promise_test(async t => {
const iframe = await parseInIframe(t, `<table><tbody><tr><b><div id=fb><script>document.getElementById('fb').remove()<\/script></b></table>`);
assert_equals(tree(iframe.contentDocument.body),
`<b><div#fb>[<b>[<script>]]<table>[<tbody>[<tr>]]`,
"main document body");
}, "Adoption agency with a foster-parenting common ancestor (<tr>)");
</script>
32 changes: 32 additions & 0 deletions html/syntax/parsing/resources/adoption-agency-reparenting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use strict";

// Shared helpers for the adoption-agency-reparenting tests.

// Compact serializer: element -> <name>[#id][children]; text -> "data".
// <script> contents are elided to keep expectations stable.
function serialize(n) {
if (n.nodeType === Node.TEXT_NODE)
return `"${n.data}"`;
if (n.nodeType !== Node.ELEMENT_NODE)
return "";
const label = `<${n.localName}${n.id ? "#" + n.id : ""}>`;
if (n.localName === "script")
return label;
const kids = tree(n);
return kids ? `${label}[${kids}]` : label;
}

function tree(node) {
return [...node.childNodes].map(serialize).join("");
}

// Parse srcdoc in an iframe (so inline scripts run during parsing) and resolve
// with the iframe once loaded.
function parseInIframe(t, srcdoc) {
const iframe = document.createElement("iframe");
iframe.srcdoc = srcdoc;
return new Promise(resolve => {
iframe.onload = t.step_func(() => resolve(iframe));
document.body.appendChild(iframe);
});
}
Loading