mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-02 06:09:51 +00:00
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:
parent
a47c4dbc63
commit
31a3bc3681
Notes:
github-actions[bot]
2025-04-26 14:46:43 +00:00
Author: https://github.com/shannonbooth
Commit: 31a3bc3681
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3855
Reviewed-by: https://github.com/ADKaster
Reviewed-by: https://github.com/kalenikaliaksandr
39 changed files with 1383 additions and 12 deletions
|
@ -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 parent’s shadow-including root is not the same as node’s 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue