Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
- /dom/nodes/MutationObserver-transient-registered-observers.window.html - WPT Dashboard Interop Dashboard
// META: title=MutationObserver: transient registered observers
"use strict";
// When a node is removed from a subtree that is observed with subtree set to true, the node and its
// descendants remain observed until mutation observers are notified.
function createTree() {
const parent = document.createElement("div");
parent.id = "parent";
const child = parent.appendChild(document.createElement("div"));
child.id = "child";
const grandchild = child.appendChild(document.createElement("div"));
grandchild.id = "grandchild";
return { parent, child, grandchild };
}
function createElement(id) {
const element = document.createElement("span");
element.id = id;
return element;
}
function summarize(records) {
return records.map(({ type, target, addedNodes, removedNodes, attributeName }) => {
let summary = `${type} ${target.id}`;
for (const node of addedNodes) {
summary += ` +${node.id}`;
}
for (const node of removedNodes) {
summary += ` -${node.id}`;
}
if (attributeName !== null) {
summary += ` ${attributeName}`;
}
return summary;
});
}
// Mutation observers are notified in a microtask, so they have been notified once a new task runs.
function nextTask(t) {
return new Promise(resolve => t.step_timeout(resolve, 0));
}
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), [
"childList parent -child",
"childList child +added"
]);
}, "Mutations in a removed subtree are observed until mutation observers are notified");
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { attributes: true, subtree: true });
child.remove();
child.setAttribute("data-test", "");
assert_array_equals(summarize(observer.takeRecords()), ["attributes child data-test"]);
}, "Attribute mutations in a removed subtree are observed until mutation observers are notified");
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
parent.replaceChildren();
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), [
"childList parent -child",
"childList child +added"
]);
}, "Mutations in a subtree removed by replaceChildren() are observed until mutation observers are notified");
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
assert_array_equals(summarize(observer.takeRecords()), ["childList parent -child"]);
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), ["childList child +added"]);
}, "takeRecords() does not stop observing a removed subtree");
promise_test(async t => {
const { parent, child } = createTree();
const delivered = [];
const observer = new MutationObserver(records => delivered.push(...summarize(records)));
observer.observe(parent, { childList: true, subtree: true });
// This microtask is queued before the mutation observer microtask and therefore runs first.
Promise.resolve().then(() => child.append(createElement("before")));
child.remove();
await nextTask(t);
assert_array_equals(delivered, ["childList parent -child", "childList child +before"]);
child.append(createElement("after"));
assert_array_equals(summarize(observer.takeRecords()), []);
}, "A removed subtree is no longer observed after mutation observers are notified");
promise_test(async t => {
const { parent, child } = createTree();
const observer = new MutationObserver(t.unreached_func("callback invoked"));
observer.observe(parent, { attributes: true, subtree: true });
// This does not queue a record as observer does not observe childList mutations.
child.remove();
await nextTask(t);
child.setAttribute("data-test", "");
assert_array_equals(summarize(observer.takeRecords()), []);
}, "A removed subtree is no longer observed after mutation observers are notified, even if no record was queued");
promise_test(async t => {
const { parent, child } = createTree();
const observer = new MutationObserver(t.unreached_func("callback invoked"));
observer.observe(parent, { childList: true, subtree: true });
child.remove();
assert_array_equals(summarize(observer.takeRecords()), ["childList parent -child"]);
await nextTask(t);
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), []);
}, "A removed subtree is no longer observed after mutation observers are notified, even if the callback was not invoked");
promise_test(async t => {
const { parent, child, grandchild } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
// grandchild is removed from a subtree that is itself only observed because it was removed.
grandchild.remove();
grandchild.append(createElement("before"));
assert_array_equals(summarize(observer.takeRecords()), [
"childList parent -child",
"childList child -grandchild",
"childList grandchild +before"
]);
await nextTask(t);
grandchild.append(createElement("after"));
assert_array_equals(summarize(observer.takeRecords()), []);
}, "A subtree removed from a removed subtree is no longer observed after mutation observers are notified");
promise_test(async t => {
const { parent, child } = createTree();
const order = [];
const observer = new MutationObserver(() => order.push("observer"));
observer.observe(parent, { attributes: true, subtree: true });
// This queues no record, as observer does not observe childList mutations.
child.remove();
Promise.resolve().then(() => order.push("promise"));
child.setAttribute("data-test", "");
await nextTask(t);
assert_array_equals(order, ["observer", "promise"]);
}, "Removing a node from an observed subtree queues the mutation observer microtask");
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
observer.disconnect();
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), []);
}, "disconnect() stops observing a removed subtree");
test(() => {
const { parent, child, grandchild } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
grandchild.remove();
observer.disconnect();
child.append(createElement("added-to-child"));
grandchild.append(createElement("added-to-grandchild"));
assert_array_equals(summarize(observer.takeRecords()), []);
}, "disconnect() stops observing a subtree removed from a removed subtree");
test(() => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
const otherObserver = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
otherObserver.observe(parent, { childList: true, subtree: true });
child.remove();
observer.disconnect();
child.append(createElement("added"));
assert_array_equals(summarize(observer.takeRecords()), []);
assert_array_equals(summarize(otherObserver.takeRecords()), [
"childList parent -child",
"childList child +added"
]);
}, "disconnect() does not affect another observer observing a removed subtree");
test(() => {
const { parent, child, grandchild } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
observer.observe(child, { attributes: true, subtree: true });
// grandchild remains observed through both parent and child.
grandchild.remove();
// This stops observing grandchild through child, but not through parent.
observer.observe(child, { attributes: true, subtree: true });
grandchild.append(createElement("added"));
grandchild.setAttribute("data-test", "");
assert_array_equals(summarize(observer.takeRecords()), [
"childList child -grandchild",
"childList grandchild +added"
]);
}, "observe() stops observing a removed subtree through the target, but not through other targets");
test(() => {
const { parent, child, grandchild } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
// grandchild is removed from a subtree that is itself only observed because it was removed.
grandchild.remove();
observer.observe(parent, { childList: true, subtree: true });
child.append(createElement("added-to-child"));
grandchild.append(createElement("added-to-grandchild"));
assert_array_equals(summarize(observer.takeRecords()), [
"childList parent -child",
"childList child -grandchild"
]);
}, "observe() stops observing subtrees removed from a removed subtree");
promise_test(async t => {
const { parent, child } = createTree();
const observer = new MutationObserver(() => {});
observer.observe(parent, { childList: true, subtree: true });
child.remove();
observer.observe(child, { attributes: true });
await nextTask(t);
child.append(createElement("added"));
child.setAttribute("data-test", "");
assert_array_equals(summarize(observer.takeRecords()), ["attributes child data-test"]);
}, "observe() on a node in a removed subtree keeps observing it after mutation observers are notified");