/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js", this ); function loadSelector(walker, selector) { return walker.querySelectorAll(walker.rootNode, selector).then(nodeList => { return nodeList.items(); }); } function loadSelectors(walker, selectors) { return Promise.all(Array.from(selectors, sel => loadSelector(walker, sel))); } function doMoves(movesArg) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [movesArg], function (moves) { function setParent(nodeSelector, newParentSelector) { const node = content.document.querySelector(nodeSelector); if (newParentSelector) { const newParent = content.document.querySelector(newParentSelector); newParent.appendChild(node); } else { node.remove(); } } for (const move of moves) { setParent(move[0], move[1]); } } ); } /** * Test a set of tree rearrangements and make sure they cause the expected changes. */ var gDummySerial = 0; function mutationTest(testSpec) { return async function () { const { walker } = await initInspectorFront( MAIN_DOMAIN + "inspector-traversal-data.html" ); await loadSelectors(walker, testSpec.load || ["html"]); walker.autoCleanup = !!testSpec.autoCleanup; if (testSpec.preCheck) { testSpec.preCheck(); } const onMutations = walker.once("mutations"); await doMoves(testSpec.moves || []); // Some of these moves will trigger no mutation events, // so do a dummy change to the root node to trigger // a mutation event anyway. await SpecialPowers.spawn( gBrowser.selectedBrowser, [[gDummySerial++]], function (serial) { content.document.documentElement.setAttribute("data-dummy", serial); } ); let mutations = await onMutations; // Filter out our dummy mutation. mutations = mutations.filter(change => { if (change.type == "attributes" && change.attributeName == "data-dummy") { return false; } return true; }); await assertOwnershipTrees(walker); if (testSpec.postCheck) { testSpec.postCheck(walker, mutations); } }; } // Verify that our dummy mutation works. add_task( mutationTest({ autoCleanup: false, postCheck(walker, mutations) { is(mutations.length, 0, "Dummy mutation is filtered out."); }, }) ); // Test a simple move to a different location in the sibling list for the same // parent. add_task( mutationTest({ autoCleanup: false, load: ["#longlist div"], moves: [["#a", "#longlist"]], postCheck(walker, mutations) { const remove = mutations[0]; is(remove.type, "childList", "First mutation should be a childList."); ok(!!remove.removed.length, "First mutation should be a removal."); const add = mutations[1]; is( add.type, "childList", "Second mutation should be a childList removal." ); ok(!!add.added.length, "Second mutation should be an addition."); const a = add.added[0]; is(a.id, "a", "Added node should be #a"); is(a.parentNode(), remove.target, "Should still be a child of longlist."); is( remove.target, add.target, "First and second mutations should be against the same node." ); }, }) ); // Test a move to another location that is within our ownership tree. add_task( mutationTest({ autoCleanup: false, load: ["#longlist div", "#longlist-sibling"], moves: [["#a", "#longlist-sibling"]], postCheck(walker, mutations) { const remove = mutations[0]; is(remove.type, "childList", "First mutation should be a childList."); ok(!!remove.removed.length, "First mutation should be a removal."); const add = mutations[1]; is( add.type, "childList", "Second mutation should be a childList removal." ); ok(!!add.added.length, "Second mutation should be an addition."); const a = add.added[0]; is(a.id, "a", "Added node should be #a"); is(a.parentNode(), add.target, "Should still be a child of longlist."); is( add.target.id, "longlist-sibling", "long-sibling should be the target." ); }, }) ); // Move an unseen node with a seen parent into our ownership tree - should generate a // childList pair with no adds or removes. add_task( mutationTest({ autoCleanup: false, load: ["#longlist"], moves: [["#longlist-sibling", "#longlist"]], postCheck(walker, mutations) { is(mutations.length, 2, "Should generate two mutations"); is(mutations[0].type, "childList", "Should be childList mutations."); is(mutations[0].added.length, 0, "Should have no adds."); is(mutations[0].removed.length, 0, "Should have no removes."); is(mutations[1].type, "childList", "Should be childList mutations."); is(mutations[1].added.length, 0, "Should have no adds."); is(mutations[1].removed.length, 0, "Should have no removes."); }, }) ); // Move an unseen node with an unseen parent into our ownership tree. Should only // generate one childList mutation with no adds or removes. add_task( mutationTest({ autoCleanup: false, load: ["#longlist div"], moves: [["#longlist-sibling-firstchild", "#longlist"]], postCheck(walker, mutations) { is(mutations.length, 1, "Should generate two mutations"); is(mutations[0].type, "childList", "Should be childList mutations."); is(mutations[0].added.length, 0, "Should have no adds."); is(mutations[0].removed.length, 0, "Should have no removes."); }, }) ); // Move a node between unseen nodes, should generate no mutations. add_task( mutationTest({ autoCleanup: false, load: ["html"], moves: [["#longlist-sibling", "#longlist"]], postCheck(walker, mutations) { is(mutations.length, 0, "Should generate no mutations."); }, }) ); // Orphan a node and don't clean it up add_task( mutationTest({ autoCleanup: false, load: ["#longlist div"], moves: [["#longlist", null]], postCheck(walker, mutations) { is(mutations.length, 1, "Should generate one mutation."); const change = mutations[0]; is(change.type, "childList", "Should be a childList."); is(change.removed.length, 1, "Should have removed a child."); const ownership = clientOwnershipTree(walker); is(ownership.orphaned.length, 1, "Should have one orphaned subtree."); is( ownershipTreeSize(ownership.orphaned[0]), 1 + 26 + 26, "Should have orphaned longlist, and 26 children, and 26 singleTextChilds" ); }, }) ); // Orphan a node, and do clean it up. add_task( mutationTest({ autoCleanup: true, load: ["#longlist div"], moves: [["#longlist", null]], postCheck(walker, mutations) { is(mutations.length, 1, "Should generate one mutation."); const change = mutations[0]; is(change.type, "childList", "Should be a childList."); is(change.removed.length, 1, "Should have removed a child."); const ownership = clientOwnershipTree(walker); is(ownership.orphaned.length, 0, "Should have no orphaned subtrees."); }, }) ); // Orphan a node by moving it into the tree but out of our visible subtree. add_task( mutationTest({ autoCleanup: false, load: ["#longlist div"], moves: [["#longlist", "#longlist-sibling"]], postCheck(walker, mutations) { is(mutations.length, 1, "Should generate one mutation."); const change = mutations[0]; is(change.type, "childList", "Should be a childList."); is(change.removed.length, 1, "Should have removed a child."); const ownership = clientOwnershipTree(walker); is(ownership.orphaned.length, 1, "Should have one orphaned subtree."); is( ownershipTreeSize(ownership.orphaned[0]), 1 + 26 + 26, "Should have orphaned longlist, 26 children, and 26 singleTextChilds." ); }, }) ); // Orphan a node by moving it into the tree but out of our visible subtree, // and clean it up. add_task( mutationTest({ autoCleanup: true, load: ["#longlist div"], moves: [["#longlist", "#longlist-sibling"]], postCheck(walker, mutations) { is(mutations.length, 1, "Should generate one mutation."); const change = mutations[0]; is(change.type, "childList", "Should be a childList."); is(change.removed.length, 1, "Should have removed a child."); const ownership = clientOwnershipTree(walker); is(ownership.orphaned.length, 0, "Should have no orphaned subtrees."); }, }) );