// META: title=Foster parenting: script moves the table between two insertions for one token // Spec: https://html.spec.whatwg.org/#appropriate-place-for-inserting-a-node // https://html.spec.whatwg.org/#reconstruct-the-active-formatting-elements // https://html.spec.whatwg.org/#adoption-agency-algorithm // // A single "nobr" start tag foster parents twice: once when reconstructing the active // formatting elements, and again after the adoption agency algorithm finds no furthest // block and pops the stack of open elements back to the . The `; function parse(markup) { const iframe = document.createElement("iframe"); iframe.srcdoc = markup; return new Promise(resolve => { iframe.onload = () => resolve(iframe); document.body.append(iframe); }); } // The document tree plus any template contents, which are a separate tree. function roots(doc) { return [doc, ...[...doc.querySelectorAll("template")].map(template => template.content)]; } // Every parent/child/sibling link that disagrees. Inserting before a reference child // that has moved away shows up here as a node listed among a parent's children whose // parentNode is something else. function brokenLinks(doc) { const name = node => `<${node.nodeName.toLowerCase()}${node.id ? "#" + node.id : ""}>`; const problems = []; const seen = new Set(); const walk = node => { if (seen.has(node)) { problems.push(`${name(node)} has two parents`); return; } seen.add(node); const children = []; for (let child = node.firstChild; child && children.length < 100; child = child.nextSibling) children.push(child); children.forEach((child, i) => { if (child.parentNode !== node) problems.push(`${name(node)} lists ${name(child)}, whose parentNode is elsewhere`); if (child.previousSibling !== (children[i - 1] ?? null)) problems.push(`${name(node)} has a broken previousSibling at ${name(child)}`); walk(child); }); if (node.lastChild !== (children.at(-1) ?? null)) problems.push(`${name(node)}'s lastChild is not the end of its child list`); }; roots(doc).forEach(walk); return problems; } // The second foster-parented element is the only with a child, and it is // reachable only if it was inserted at all. function secondInsertion(doc) { for (const root of roots(doc)) { const b = [...root.querySelectorAll("b")].find(b => b.firstElementChild?.localName === "nobr"); if (b) return b; } return null; } function placement(node) { if (!node) return "not inserted"; const parent = node.parentNode; if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) return "template contents"; return parent.id || parent.localName; } for (const variant of variants) { const parsed = parse(markupFor(variant)); promise_test(async () => { const doc = (await parsed).contentDocument; assert_equals(brokenLinks(doc).join("; "), "", "the tree is well-formed"); }, `Table ${variant.name}: the tree stays well-formed`); promise_test(async () => { const iframe = await parsed; const doc = iframe.contentDocument; assert_equals(iframe.contentWindow.tableParentWhenRun, "foster-parent", "the script ran while the parser was foster parenting into it"); const inserted = secondInsertion(doc); assert_equals(placement(inserted), variant.parent, "the second foster-parented element follows the table"); if (variant.beforeTable) assert_equals(inserted.nextSibling, doc.querySelector("table"), "immediately before the table"); }, `Table ${variant.name}: the second foster-parented element follows it`); }