LibWeb: Implement 'State-preserving atomic move integration'

This was recently added to both the HTML and DOM specifications,
introducing the new moveBefore DOM API, as well as the new internal
'removing steps'.

See:

 * 432e8fb
 * eaf2ac7
This commit is contained in:
Shannon Booth 2025-03-08 12:45:26 +13:00 committed by Andrew Kaster
commit 31a3bc3681
Notes: github-actions[bot] 2025-04-26 14:46:43 +00:00
39 changed files with 1383 additions and 12 deletions

View file

@ -0,0 +1,349 @@
<!DOCTYPE html>
<title>Node.moveBefore</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<div id="log"></div>
<!-- First test shared pre-insertion checks that work similarly for replaceChild
and moveBefore -->
<script>
var insertFunc = Node.prototype.moveBefore;
</script>
<script src="../../pre-insertion-validation-hierarchy.js"></script>
<script>
preInsertionValidateHierarchy("moveBefore");
test(function() {
// WebIDL: first argument.
assert_throws_js(TypeError, function() { document.body.moveBefore(null, null) })
assert_throws_js(TypeError, function() { document.body.moveBefore(null, document.body.firstChild) })
assert_throws_js(TypeError, function() { document.body.moveBefore({'a':'b'}, document.body.firstChild) })
}, "Calling moveBefore with a non-Node first argument must throw TypeError.")
test(function() {
// WebIDL: second argument.
assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child")) })
assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child"), {'a':'b'}) })
}, "Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError.")
test(() => {
assert_false("moveBefore" in document.doctype, "moveBefore() not on DocumentType");
assert_false("moveBefore" in document.createTextNode("text"), "moveBefore() not on TextNode");
assert_false("moveBefore" in new Comment("comment"), "moveBefore() not on CommentNode");
assert_false("moveBefore" in document.createProcessingInstruction("foo", "bar"), "moveBefore() not on ProcessingInstruction");
}, "moveBefore() method does not exist on non-ParentNode Nodes");
// Pre-move validity, step 1:
// "If either parent or node are not connected, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
const connectedTarget = document.body.appendChild(document.createElement('div'));
const disconnectedDestination = document.createElement('div');
t.add_cleanup(() => connectedTarget.remove());
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
disconnectedDestination.moveBefore(connectedTarget, null);
});
}, "moveBefore() on disconnected parent throws a HierarchyRequestError");
test(t => {
const connectedDestination = document.body.appendChild(document.createElement('div'));
const disconnectedTarget = document.createElement('div');
t.add_cleanup(() => connectedDestination.remove());
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
connectedDestination.moveBefore(disconnectedTarget, null);
});
}, "moveBefore() with disconnected target node throws a HierarchyRequestError");
// Pre-move validity, step 2:
// "If parents shadow-including root is not the same as nodes shadow-including
// "root, then throw a "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
const iframe = document.createElement('iframe');
document.body.append(iframe);
const connectedCrossDocChild = iframe.contentDocument.createElement('div');
const connectedLocalParent = document.querySelector('div');
t.add_cleanup(() => iframe.remove());
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
connectedLocalParent.moveBefore(connectedCrossDocChild, null);
});
}, "moveBefore() on a cross-document target node throws a HierarchyRequestError");
// Pre-move validity, step 3:
// "If parent is not a Document, DocumentFragment, or Element node, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
const iframe = document.body.appendChild(document.createElement('iframe'));
const innerBody = iframe.contentDocument.querySelector('body');
assert_throws_dom("HIERARCHY_REQUEST_ERR", iframe.contentWindow.DOMException, () => {
// Moving the body into the same place that it already is, which is a valid
// action in the normal case, when moving an Element directly under the
// document. This is not `moveBefore()`-specific behavior; it is consistent
// with traditional Document insertion rules, just like `insertBefore()`.
iframe.contentDocument.moveBefore(innerBody, null);
});
}, "moveBefore() into a Document throws a HierarchyRequestError");
test(t => {
const iframe = document.body.appendChild(document.createElement('iframe'));
const comment = iframe.contentDocument.createComment("comment");
iframe.contentDocument.body.append(comment);
iframe.contentDocument.moveBefore(comment, null);
assert_equals(comment.parentNode, iframe.contentDocument);
}, "moveBefore() CharacterData into a Document");
// Pre-move validity, step 4:
// "If node is a host-including inclusive ancestor of parent, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
const parentDiv = document.body.appendChild(document.createElement('div'));
const childDiv = parentDiv.appendChild(document.createElement('div'));
t.add_cleanup(() => {
parentDiv.remove();
childDiv.remove();
});
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
parentDiv.moveBefore(parentDiv, null);
}, "parent moving itself");
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
childDiv.moveBefore(parentDiv, null);
}, "Moving parent into immediate child");
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
childDiv.moveBefore(document.body, null);
}, "Moving grandparent into grandchild");
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
document.body.moveBefore(document.documentElement, childDiv);
}, "Moving documentElement (<html>) into a deeper child");
}, "moveBefore() with node being an inclusive ancestor of parent throws a " +
"HierarchyRequestError");
// Pre-move validity, step 5:
// "If node is not an Element or a CharacterData node, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
assert_true(document.doctype.isConnected);
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
document.body.moveBefore(document.doctype, null);
}, "DocumentType throws");
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
document.body.moveBefore(new DocumentFragment(), null);
}, "DocumentFragment throws");
const doc = document.implementation.createHTMLDocument("title");
assert_true(doc.isConnected);
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
document.body.moveBefore(doc, null);
});
}, "moveBefore() with a non-{Element, CharacterData} throws a HierarchyRequestError");
promise_test(async t => {
const text = new Text("child text");
document.body.prepend(text);
const childElement = document.createElement('p');
document.body.prepend(childElement);
const comment = new Comment("comment");
document.body.prepend(comment);
t.add_cleanup(() => {
text.remove();
childElement.remove();
comment.remove();
});
// Wait until style is computed once, then continue after. This is necessary
// to reproduce a Chromium crash regression with moving Comment nodes in the
// DOM.
await new Promise(r => {
requestAnimationFrame(() => requestAnimationFrame(() => r()));
});
document.body.moveBefore(text, null);
assert_equals(document.body.lastChild, text);
document.body.moveBefore(childElement, null);
assert_equals(document.body.lastChild, childElement);
document.body.moveBefore(text, null);
assert_equals(document.body.lastChild, text);
document.body.moveBefore(comment, null);
assert_equals(document.body.lastChild, comment);
}, "moveBefore with an Element or CharacterData succeeds");
test(t => {
const p = document.createElement('p');
p.textContent = "Some content";
document.body.prepend(p);
const text_node = p.firstChild;
// The Text node is *inside* the paragraph.
assert_equals(text_node.textContent, "Some content");
assert_not_equals(document.body.lastChild, text_node);
t.add_cleanup(() => {
text_node.remove();
p.remove();
});
document.body.moveBefore(p.firstChild, null);
assert_equals(document.body.lastChild, text_node);
}, "moveBefore on a paragraph's Text node child");
// Pre-move validity, step 6:
// "If child is non-null and its parent is not parent, then throw a
// "NotFoundError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
const a = document.body.appendChild(document.createElement("div"));
const b = document.body.appendChild(document.createElement("div"));
const c = document.body.appendChild(document.createElement("div"));
t.add_cleanup(() => {
a.remove();
b.remove();
c.remove();
});
assert_throws_dom("NotFoundError", () => {
a.moveBefore(b, c);
});
}, "moveBefore with reference child whose parent is NOT the destination " +
"parent (context node) throws a NotFoundError.")
test(() => {
const a = document.body.appendChild(document.createElement("div"));
const b = document.createElement("div");
const c = document.createElement("div");
a.append(b);
a.append(c);
assert_array_equals(a.childNodes, [b, c]);
assert_equals(a.moveBefore(c, b), undefined, "moveBefore() returns undefined");
assert_array_equals(a.childNodes, [c, b]);
}, "moveBefore() returns undefined");
test(() => {
const a = document.body.appendChild(document.createElement("div"));
const b = document.createElement("div");
const c = document.createElement("div");
a.append(b);
a.append(c);
assert_array_equals(a.childNodes, [b, c]);
a.moveBefore(b, b);
assert_array_equals(a.childNodes, [b, c]);
a.moveBefore(c, c);
assert_array_equals(a.childNodes, [b, c]);
}, "Moving a node before itself should not move the node");
test(() => {
const disconnectedOrigin = document.createElement('div');
const disconnectedDestination = document.createElement('div');
const p = disconnectedOrigin.appendChild(document.createElement('p'));
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
disconnectedDestination.moveBefore(p, null);
});
}, "Moving a node from a disconnected container to a disconnected new parent " +
"without a shared ancestor throws a HIERARCHY_REQUEST_ERR");
test(() => {
const disconnectedOrigin = document.createElement('div');
const disconnectedDestination = disconnectedOrigin.appendChild(document.createElement('div'));
const p = disconnectedOrigin.appendChild(document.createElement('p'));
disconnectedDestination.moveBefore(p, null);
assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds");
test(() => {
const disconnectedOrigin = document.createElement('div');
const disconnectedHost = disconnectedOrigin.appendChild(document.createElement('div'));
const p = disconnectedOrigin.appendChild(document.createElement('p'));
const shadow = disconnectedHost.attachShadow({mode: "closed"});
const disconnectedDestination = shadow.appendChild(document.createElement('div'));
disconnectedDestination.moveBefore(p, null);
assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds," +
"also across shadow-roots");
test(() => {
const disconnectedOrigin = document.createElement('div');
const connectedDestination = document.body.appendChild(document.createElement('div'));
const p = disconnectedOrigin.appendChild(document.createElement('p'));
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => connectedDestination.moveBefore(p, null));
}, "Moving a node from disconnected->connected throws a HIERARCHY_REQUEST_ERR");
test(() => {
const connectedOrigin = document.body.appendChild(document.createElement('div'));
const disconnectedDestination = document.createElement('div');
const p = connectedOrigin.appendChild(document.createElement('p'));
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => disconnectedDestination.moveBefore(p, null));
}, "Moving a node from connected->disconnected throws a HIERARCHY_REQUEST_ERR");
promise_test(async t => {
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
connectedMoveCallback() { reactions.push("connectedMove"); }
connectedCallback() { reactions.push("connected"); }
disconnectedCallback() { reactions.push("disconnected"); }
});
const oldParent = document.createElement('div');
const newParent = oldParent.appendChild(document.createElement('div'));
const element = oldParent.appendChild(document.createElement(element_name));
t.add_cleanup(() => {
element.remove();
newParent.remove();
oldParent.remove();
});
// Wait a microtask to let any custom element reactions run (should be none,
// since the initial parent is disconnected).
await Promise.resolve();
newParent.moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, []);
}, "No custom element callbacks are run during disconnected moveBefore()");
// This is a regression test for a Chromium crash: https://crbug.com/388934346.
test(t => {
// This test caused a crash in Chromium because after the detection of invalid
// /node hierarchy, and throwing the JS error, we did not return from native
// code, and continued to operate on the node tree on bad assumptions.
const outer = document.createElement('div');
const div = outer.appendChild(document.createElement('div'));
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => div.moveBefore(outer, null));
}, "Invalid node hierarchy with null old parent does not crash");
test(t => {
const outerDiv = document.createElement('div');
const innerDiv = outerDiv.appendChild(document.createElement('div'));
const iframe = innerDiv.appendChild(document.createElement('iframe'));
outerDiv.moveBefore(iframe, null);
}, "Move disconnected iframe does not crash");
</script>

View file

@ -0,0 +1,133 @@
<!DOCTYPE html>
<title>Node.moveBefore custom element reactions</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script>
;
</script>
<body>
<section id="section"></section>
<script>
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
connectedCallback() { reactions.push("connected"); }
disconnectedCallback() { reactions.push("disconnected"); }
});
const element = document.createElement(element_name);
t.add_cleanup(() => element.remove());
document.body.append(element);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, ["disconnected", "connected"]);
}, "the disconnected/connected callbacks should be called when no other callback is defined");
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
connectedCallback() { reactions.push(this.isConnected); }
disconnectedCallback() { reactions.push(this.isConnected); }
});
const element = document.createElement(element_name);
t.add_cleanup(() => element.remove());
document.body.append(element);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, [true, true]);
}, "the element should stay connected during the callbacks");
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
connectedMoveCallback() { reactions.push("connectedMove"); }
connectedCallback() { reactions.push("connected"); }
disconnectedCallback() { reactions.push("disconnected"); }
});
const element = document.createElement(element_name);
t.add_cleanup(() => element.remove());
document.body.append(element);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, ["connectedMove"]);
}, "When connectedMoveCallback is defined, it is called instead of disconnectedCallback/connectedCallback");
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const outer_element_name = `ce-${performance.now()}-outer`;
const inner_element_name = `ce-${performance.now()}-inner`;
customElements.define(outer_element_name,
class MockCustomElement extends HTMLElement {
connectedCallback() { reactions.push("outer connected"); }
disconnectedCallback() { reactions.push("outer disconnected"); }
});
customElements.define(inner_element_name,
class MockCustomElement extends HTMLElement {
connectedCallback() { reactions.push("inner connected"); }
disconnectedCallback() { reactions.push("inner disconnected"); }
});
const outer = document.createElement(outer_element_name);
const inner = document.createElement(inner_element_name);
t.add_cleanup(() => outer.remove());
outer.append(inner);
document.body.append(outer);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(outer, null);
await Promise.resolve();
assert_array_equals(reactions, ["outer disconnected", "outer connected", "inner disconnected", "inner connected"]);
}, "Reactions to atomic move are called in order of element, not in order of operation");
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
disconnectedCallback() { reactions.push("disconnected"); }
});
const element = document.createElement(element_name);
t.add_cleanup(() => element.remove());
document.body.append(element);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, ["disconnected"]);
}, "When connectedCallback is not defined, no crash");
promise_test(async t => {
const ce = document.getElementById("ce");
let reactions = [];
const element_name = `ce-${performance.now()}`;
customElements.define(element_name,
class MockCustomElement extends HTMLElement {
connectedCallback() { reactions.push("connected"); }
});
const element = document.createElement(element_name);
t.add_cleanup(() => element.remove());
document.body.append(element);
await Promise.resolve();
reactions = [];
document.getElementById("section").moveBefore(element, null);
await Promise.resolve();
assert_array_equals(reactions, ["connected"]);
}, "When disconnectedCallback is not defined, no crash");
</script>
</body>

View file

@ -0,0 +1,81 @@
<!DOCTYPE html>
<title>moveBefore should handle focus bubbling correctly</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<section id="old_parent">
<button id="button" tabindex="1">Button</button>
</section>
<section id="new_parent">
</section>
<section id="inert_parent" inert>
</section>
<section id="inert_when_not_empty_parent">
</section>
<style>
#inert_when_not_empty_parent:has(button) {
display: none;
}
</style>
<script>
function assert_focus_within(expected) {
const element_to_string = e => e.id || e.nodeName;
assert_array_equals(
Array.from(document.querySelectorAll(":focus-within"), element_to_string),
expected.map(element_to_string));
}
test(t => {
const old_parent = document.querySelector("#old_parent");
const button = document.querySelector("#button");
t.add_cleanup(() => old_parent.append(button));
button.focus();
assert_focus_within([document.documentElement, document.body, old_parent, button]);
new_parent.moveBefore(button, null);
assert_focus_within([document.documentElement, document.body, new_parent, button]);
}, "focus-within should be updated when reparenting focused element directly");
test(t => {
const old_parent = document.querySelector("#old_parent");
const button = document.querySelector("#button");
t.add_cleanup(() => document.body.append(old_parent));
button.focus();
new_parent.moveBefore(old_parent, null);
assert_focus_within([document.documentElement, document.body, new_parent, old_parent, button]);
}, "focus-within should be updated when reparenting an element that has focus within");
test(t => {
const old_parent = document.querySelector("#old_parent");
const button = document.querySelector("#button");
t.add_cleanup(() => old_parent.append(button));
button.focus();
old_parent.moveBefore(button, null);
assert_focus_within([document.documentElement, document.body, old_parent, button]);
}, "focus-within should remain the same when moving to the same parent");
promise_test(async t => {
const old_parent = document.querySelector("#old_parent");
const inert_parent= document.querySelector("#inert_parent");
const button = document.querySelector("#button");
t.add_cleanup(() => old_parent.append(button));
button.focus();
inert_parent.moveBefore(button, null);
assert_focus_within([document.documentElement, document.body, inert_parent, button]);
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
assert_focus_within([]);
}, ":focus-within should be eventually up to date when moving to an inert subtree");
promise_test(async t => {
const old_parent = document.querySelector("#old_parent");
const inert_when_not_empty_parent = document.querySelector("#inert_when_not_empty_parent");
const button = document.querySelector("#button");
t.add_cleanup(() => old_parent.append(button));
button.focus();
inert_when_not_empty_parent.moveBefore(button, null);
assert_focus_within([document.documentElement, document.body, inert_when_not_empty_parent, button]);
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
assert_focus_within([]);
}, ":focus-within should be eventually up to date when moving to a subtree that would become inert via style");
</script>

View file

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<script src='../../../../resources/testharness.js'></script>
<script src='../../../../resources/testharnessreport.js'></script>
</head>
<body>
</body>
<script>
'use strict';
test(t => {
document.body.innerHTML = `
<div id=old_parent>
<span id=start>RangeStartTarget</span>
<span id=middle>Middle</span>
<span id=end>RangeEndTarget</span>
</div>`;
const range = new Range();
range.setStart(start, 0);
range.setEnd(end, 0);
assert_true(range.intersectsNode(middle), "Intersection before move");
// Moves `start` span to the very bottom of the container.
old_parent.moveBefore(start, null);
// In an ordinary removal, when a node whose descendant is the start (or end)
// of a live range is removed, the range's start is set to the removed node's
// parent. For now, the same thing happens during `moveBefore()`.
assert_equals(range.startContainer, old_parent, "startContainer updates during move");
assert_equals(range.endContainer, end, "endContainer does not update after move");
assert_true(range.intersectsNode(middle), "adjusted range still intersects " +
"middle node after move");
}, "moveBefore still results in range startContainer snapping up to parent " +
"when startContainer is moved");
test(t => {
document.body.innerHTML = `
<div id=old_parent>
<div id=movable_div>
<span id=start>RangeStartTarget</span>
<span id=middle>Middle</span>
</div>
<span id=end>RangeEndTarget</span>
</div>
<div id=new_parent></div>`;
const range = new Range();
range.setStart(start, 0);
range.setEnd(end, 0);
assert_true(range.intersectsNode(middle), "Intersection before move");
new_parent.moveBefore(movable_div, null);
assert_equals(range.startContainer, old_parent, "startContainer still " +
"updates during move, to snap to parent");
assert_equals(range.endContainer, end, "endContainer does not update after move");
assert_false(range.intersectsNode(middle), "range no longer intersects " +
"middle node after move, since middle node was moved outside of the range");
}, "moveBefore still causes range startContainer to snap up to parent, when " +
"startContainer ancestor is moved");
test(t => {
document.body.innerHTML = `
<div id=old_parent>
<span id=start>RangeStartTarget</span>
<div id=movable_div>
<span id=end>RangeEndTarget</span>
</div>
</div>
<div id=new_parent>
<span id=middle>Middle</span>
</div>`;
const range = new Range();
range.setStart(start, 0);
range.setEnd(end, 0);
assert_false(range.intersectsNode(middle), "No intersection before move");
new_parent.moveBefore(movable_div, null);
assert_equals(range.startContainer, start, "startContainer does not update " +
"after move");
assert_equals(range.endContainer, old_parent, "endContainer still snaps up " +
"to parent after move");
assert_false(range.intersectsNode(middle), "adjusted range still does not " +
"intersect middle node after move");
}, "moveBefore still causes range endContainer to snap up to parent, when " +
"endContainer ancestor is moved");
</script>
</html>

View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<style>
section {
position: relative;
}
#item {
position: absolute;
inset: 0;
}
#section1 {
width: 200px;
height: 200px;
}
#section2 {
width: 300px;
height: 300px;
}
</style>
<section id="section1">
<div id="item">
<template shadowRootMode="open">
<style>
div {
position: absolute;
inset: 0;
background-color: var(--color, red);
}
</style>
<div></div>
</template>
</div>
</section>
<section id="section2">
</section>
<script>
promise_test(async () => {
const item = document.querySelector("#item");
document.querySelector("#section2").moveBefore(item, null);
await new Promise(resolve => requestAnimationFrame(() => resolve()));
assert_equals(item.shadowRoot.querySelector("div").getBoundingClientRect().width, 300);
});
</script>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<div id=shadowTarget></div>
</body>
<script>
test(() => {
shadowTarget.attachShadow({mode: 'open'});
const child1 = document.createElement('p');
child1.textContent = 'Child1';
const child2 = document.createElement('p');
child2.textContent = 'Child2';
shadowTarget.shadowRoot.append(child1, child2);
shadowTarget.shadowRoot.moveBefore(child2, child1);
assert_equals(shadowTarget.shadowRoot.firstChild, child2, "Original lastChild is now firstChild");
assert_equals(shadowTarget.shadowRoot.lastChild, child1, "Original firstChild is now lastChild");
}, "moveBefore() is allowed in ShadowRoots (i.e., connected DocumentFragments)");
</script>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<title>Mutation events are suppressed during moveBefore()</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<p id=reference>reference</p>
<p id=target>target</p>
<script>
const reference = document.querySelector('#reference');
const target = document.querySelector('#target');
test(() => {
target.addEventListener('DOMNodeInserted', () => assert_unreached('DOMNodeInserted not called'));
target.addEventListener('DOMNodeRemoved', () => assert_unreached('DOMNodeRemoved not called'));
document.body.moveBefore(target, reference);
}, "MutationEvents (if supported by the UA) are suppressed during `moveBefore()`");
</script>
</body>

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<title>slotchanged event</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<div id=oldParent>
<p id=target></p>
</div>
<div id=newParent></div>
<script>
async function runTest(oldParent, target, newParent) {
const observations = [];
const observer = new MutationObserver(mutationList => observations.push(mutationList));
observer.observe(oldParent, {childList: true});
observer.observe(target, {childList: true});
observer.observe(newParent, {childList: true});
newParent.moveBefore(target, null);
// Wait for microtasks to settle.
await new Promise(resolve => queueMicrotask(resolve));
assert_equals(observations.length, 1, "MutationObserver has emitted a single mutation list");
assert_equals(observations[0].length, 2, "Mutation list has two MutationRecords");
const removalRecord = observations[0][0];
const insertionRecord = observations[0][1];
assert_equals(removalRecord.target, oldParent, "removalRecord target is correct");
assert_equals(removalRecord.removedNodes[0], target, "removedNodes contains the moved node");
assert_equals(insertionRecord.target, newParent, "insertionRecord target is correct");
assert_equals(insertionRecord.addedNodes[0], target, "addedNodes contains the moved node");
observer.disconnect();
}
promise_test(async t => {
await runTest(oldParent, target, newParent);
}, "[Connected move] MutationObserver removal + insertion is tracked by moveBefore()");
promise_test(async t => {
const oldParent = document.createElement('div');
const target = document.createElement('p');
const newParent = document.createElement('div');
// We must append `newParent` as well, since the origin and destination nodes
// must share the same shadow-including root.
oldParent.append(target, newParent);
await runTest(oldParent, target, newParent);
}, "[Disconnected move] MutationObserver removal + insertion is tracked by moveBefore()");
</script>

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<title>Nonce attribute is not cleared</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<section id="new_parent"></section>
<script>
test(t => {
const div = document.createElement('div');
document.body.append(div);
const kNonce = '8IBTHwOdqNKAWeKl7plt8g==';
div.setAttribute('nonce', kNonce);
assert_equals(div.getAttribute('nonce'), kNonce);
new_parent.moveBefore(div, null);
assert_equals(div.getAttribute('nonce'), kNonce);
new_parent.insertBefore(div, null);
assert_equals(div.getAttribute('nonce'), "");
}, "Element nonce content attribute is not cleared after move");
</script>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<title>Object element moveBefore() regression test</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<b id="p"><object>
<script>
test(t => {
// Per https://crbug.com/373924127, simply moving an object element would
// crash, due to an internal subframe count mechanism getting out of sync.
p.moveBefore(p.lastChild, p.firstChild);
}, "Moving an object element does not crash");
</script>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<title>moveBefore should not close a popover</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<body>
<section id="old_parent">
<div popover>
Popover
</div>
</section>
<section id="new_parent">
</section>
<script>
promise_test(async t => {
const popover = document.querySelector("div[popover]");
popover.showPopover();
await new Promise(resolve => requestAnimationFrame(() => resolve()));
assert_equals(document.querySelector(":popover-open"), popover);
document.querySelector("#new_parent").moveBefore(popover, null);
assert_equals(document.querySelector(":popover-open"), popover);
}, "when reparenting an open popover, it shouldn't be closed automatically");
</script>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<script src='../../../../resources/testharness.js'></script>
<script src='../../../../resources/testharnessreport.js'></script>
<body>
</body>
<script>
'use strict';
promise_test(async t => {
const link = document.createElement("link");
link.href = "data:text/css,body{background: green}";
link.rel = "stylesheet";
t.add_cleanup(() => link.remove());
document.body.append(link);
const backgroundColorBefore = getComputedStyle(document.body).backgroundColor;
document.body.moveBefore(link, null);
assert_equals(getComputedStyle(document.body).backgroundColor, backgroundColorBefore);
}, "Moving a style inside the document should not affect whether it's applied");
</script>
</html>

View file

@ -0,0 +1,86 @@
/**
* Validations where `child` argument is irrelevant.
* @param {Function} methodName
*/
function preInsertionValidateHierarchy(methodName) {
function insert(parent, node) {
if (parent[methodName].length > 1) {
// This is for insertBefore(). We can't blindly pass `null` for all methods
// as doing so will move nodes before validation.
parent[methodName](node, null);
} else {
parent[methodName](node);
}
}
// Step 2
test(() => {
const doc = document.implementation.createHTMLDocument("title");
assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.body));
assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.documentElement));
}, "If node is a host-including inclusive ancestor of parent, then throw a HierarchyRequestError DOMException.");
// Step 4
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const doc2 = document.implementation.createHTMLDocument("title2");
assert_throws_dom("HierarchyRequestError", () => insert(doc, doc2));
}, "If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, then throw a HierarchyRequestError DOMException.");
// Step 5, in case of inserting a text node into a document
test(() => {
const doc = document.implementation.createHTMLDocument("title");
assert_throws_dom("HierarchyRequestError", () => insert(doc, doc.createTextNode("text")));
}, "If node is a Text node and parent is a document, then throw a HierarchyRequestError DOMException.");
// Step 5, in case of inserting a doctype into a non-document
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const doctype = doc.childNodes[0];
assert_throws_dom("HierarchyRequestError", () => insert(doc.createElement("a"), doctype));
}, "If node is a doctype and parent is not a document, then throw a HierarchyRequestError DOMException.")
// Step 6, in case of DocumentFragment including multiple elements
test(() => {
const doc = document.implementation.createHTMLDocument("title");
doc.documentElement.remove();
const df = doc.createDocumentFragment();
df.appendChild(doc.createElement("a"));
df.appendChild(doc.createElement("b"));
assert_throws_dom("HierarchyRequestError", () => insert(doc, df));
}, "If node is a DocumentFragment with multiple elements and parent is a document, then throw a HierarchyRequestError DOMException.");
// Step 6, in case of DocumentFragment has multiple elements when document already has an element
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const df = doc.createDocumentFragment();
df.appendChild(doc.createElement("a"));
assert_throws_dom("HierarchyRequestError", () => insert(doc, df));
}, "If node is a DocumentFragment with an element and parent is a document with another element, then throw a HierarchyRequestError DOMException.");
// Step 6, in case of an element
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const el = doc.createElement("a");
assert_throws_dom("HierarchyRequestError", () => insert(doc, el));
}, "If node is an Element and parent is a document with another element, then throw a HierarchyRequestError DOMException.");
// Step 6, in case of a doctype when document already has another doctype
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const doctype = doc.childNodes[0].cloneNode();
doc.documentElement.remove();
assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype));
}, "If node is a doctype and parent is a document with another doctype, then throw a HierarchyRequestError DOMException.");
// Step 6, in case of a doctype when document has an element
if (methodName !== "prepend") {
// Skip `.prepend` as this doesn't throw if `child` is an element
test(() => {
const doc = document.implementation.createHTMLDocument("title");
const doctype = doc.childNodes[0].cloneNode();
doc.childNodes[0].remove();
assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype));
}, "If node is a doctype and parent is a document with an element, then throw a HierarchyRequestError DOMException.");
}
}