/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Tests that various mutations to the dom update the markup view correctly. const TEST_URL = URL_ROOT + "doc_markup_mutation.html"; // Mutation tests. Each entry in the array has the following properties: // - desc: for logging only // - numMutations: how many mutations are expected to come happen due to the // test case. Defaults to 1 if not set. // - test: a function supposed to mutate the DOM // - check: a function supposed to test that the mutation was handled const TEST_DATA = [ { desc: "Adding an attribute", async test() { await setContentPageElementAttribute("#node1", "newattr", "newattrval"); }, async check(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); ok( [...editor.attrList.querySelectorAll(".attreditor")].some(attr => { return ( attr.textContent.trim() === 'newattr="newattrval"' && attr.dataset.value === "newattrval" && attr.dataset.attr === "newattr" ); }), "newattr attribute found" ); }, }, { desc: "Removing an attribute", async test() { await removeContentPageElementAttribute("#node1", "newattr"); }, async check(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); ok( ![...editor.attrList.querySelectorAll(".attreditor")].some(attr => { return attr.textContent.trim() === 'newattr="newattrval"'; }), "newattr attribute removed" ); }, }, { desc: "Re-adding an attribute", async test() { await setContentPageElementAttribute("#node1", "newattr", "newattrval"); }, async check(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); ok( [...editor.attrList.querySelectorAll(".attreditor")].some(attr => { return ( attr.textContent.trim() === 'newattr="newattrval"' && attr.dataset.value === "newattrval" && attr.dataset.attr === "newattr" ); }), "newattr attribute found" ); }, }, { desc: "Changing an attribute", async test() { await setContentPageElementAttribute( "#node1", "newattr", "newattrchanged" ); }, async check(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); ok( [...editor.attrList.querySelectorAll(".attreditor")].some(attr => { return ( attr.textContent.trim() === 'newattr="newattrchanged"' && attr.dataset.value === "newattrchanged" && attr.dataset.attr === "newattr" ); }), "newattr attribute found" ); }, }, { desc: "Adding another attribute does not rerender unchanged attributes", async test(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); // This test checks the impact on the markup-view nodes after setting attributes on // content nodes. info("Expect attribute-container for 'new-attr' from the previous test"); const attributeContainer = editor.attrList.querySelector( "[data-attr=newattr]" ); ok(attributeContainer, "attribute-container for 'newattr' found"); info("Set a flag on the attribute-container to check after the mutation"); attributeContainer.beforeMutationFlag = true; info( "Add the attribute 'otherattr' on the content node to trigger the mutation" ); await setContentPageElementAttribute("#node1", "otherattr", "othervalue"); }, async check(inspector) { const { editor } = await getContainerForSelector("#node1", inspector); info( "Check the attribute-container for the new attribute mutation was created" ); const otherAttrContainer = editor.attrList.querySelector( "[data-attr=otherattr]" ); ok(otherAttrContainer, "attribute-container for 'otherattr' found"); info( "Check the attribute-container for 'new-attr' is the same node as earlier." ); const newAttrContainer = editor.attrList.querySelector( "[data-attr=newattr]" ); ok(newAttrContainer, "attribute-container for 'newattr' found"); ok( newAttrContainer.beforeMutationFlag, "attribute-container same as earlier" ); }, }, { desc: "Adding ::after element", numMutations: 2, async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node1 = content.document.querySelector("#node1"); node1.classList.add("pseudo"); }); }, async check(inspector) { const { children } = await getContainerForSelector("#node1", inspector); is( children.childNodes.length, 2, "Node1 now has 2 children (text child and ::after" ); }, }, { desc: "Removing ::after element", numMutations: 2, async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node1 = content.document.querySelector("#node1"); node1.classList.remove("pseudo"); }); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(container.inlineTextChild, "Has single text child."); }, }, { desc: "Updating the text-content", async test() { await setContentPageElementProperty("#node1", "textContent", "newtext"); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(container.inlineTextChild, "Has single text child."); ok(!container.canExpand, "Can't expand container with inlineTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); is( container.editor.elt.querySelector(".text").textContent.trim(), "newtext", "Single text child editor updated." ); }, }, { desc: "Adding a second text child", async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node1 = content.document.querySelector("#node1"); const newText = node1.ownerDocument.createTextNode("more"); node1.appendChild(newText); }); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(!container.inlineTextChild, "Does not have single text child."); ok(container.canExpand, "Can expand container with child nodes."); ok( container.editor.elt.querySelector(".text") == null, "Single text child editor removed." ); }, }, { desc: "Go from 2 to 1 text child", async test() { await setContentPageElementProperty("#node1", "textContent", "newtext"); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(container.inlineTextChild, "Has single text child."); ok(!container.canExpand, "Can't expand container with inlineTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); is( container.editor.elt.querySelector(".text").textContent.trim(), "newtext", "Single text child editor updated." ); }, }, { desc: "Removing an only text child", async test() { await setContentPageElementProperty("#node1", "innerHTML", ""); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(!container.inlineTextChild, "Does not have single text child."); ok(!container.canExpand, "Can't expand empty container."); ok( container.editor.elt.querySelector(".text") == null, "Single text child editor removed." ); }, }, { desc: "Go from 0 to 1 text child", async test() { await setContentPageElementProperty("#node1", "textContent", "newtext"); }, async check(inspector) { const container = await getContainerForSelector("#node1", inspector); ok(container.inlineTextChild, "Has single text child."); ok(!container.canExpand, "Can't expand container with inlineTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); is( container.editor.elt.querySelector(".text").textContent.trim(), "newtext", "Single text child editor updated." ); }, }, { desc: "Updating the innerHTML", async test() { await setContentPageElementProperty( "#node2", "innerHTML", "
foo
" ); }, async check(inspector) { const container = await getContainerForSelector("#node2", inspector); const openTags = container.children.querySelectorAll(".open .tag"); is(openTags.length, 2, "There are 2 tags in node2"); is(openTags[0].textContent.trim(), "div", "The first tag is a div"); is(openTags[1].textContent.trim(), "span", "The second tag is a span"); is( container.children.querySelector(".text").textContent.trim(), "foo", "The span's textcontent is correct" ); }, }, { desc: "Removing child nodes", async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node4 = content.document.querySelector("#node4"); while (node4.firstChild) { node4.firstChild.remove(); } }); }, async check(inspector) { const { children } = await getContainerForSelector("#node4", inspector); is(children.innerHTML, "", "Children have been removed"); }, }, { desc: "Appending a child to a different parent", async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node17 = content.document.querySelector("#node17"); const node2 = content.document.querySelector("#node2"); node2.appendChild(node17); }); }, async check(inspector) { const { children } = await getContainerForSelector("#node16", inspector); is( children.innerHTML, "", "Node17 has been removed from its node16 parent" ); const container = await getContainerForSelector("#node2", inspector); const openTags = container.children.querySelectorAll(".open .tag"); is(openTags.length, 3, "There are now 3 tags in node2"); is(openTags[2].textContent.trim(), "p", "The third tag is node17"); }, }, { desc: "Swapping a parent and child element, putting them in the same tree", // body // node1 // node18 // node19 // node20 // node21 // will become: // body // node1 // node20 // node21 // node18 // node19 async test() { await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const node18 = content.document.querySelector("#node18"); const node20 = content.document.querySelector("#node20"); const node1 = content.document.querySelector("#node1"); node1.appendChild(node20); node20.appendChild(node18); }); }, async check(inspector) { await inspector.markup.expandAll(); const { children } = await getContainerForSelector("#node1", inspector); is( children.childNodes.length, 2, "Node1 now has 2 children (textnode and node20)" ); const node20 = children.childNodes[1]; const node20Children = node20.container.children; is( node20Children.childNodes.length, 2, "Node20 has 2 children (21 and 18)" ); const node21 = node20Children.childNodes[0]; is( node21.container.editor.elt.querySelector(".text").textContent.trim(), "line21", "Node21 has a single text child" ); const node18 = node20Children.childNodes[1]; is( node18 .querySelector(".open .attreditor .attr-value") .textContent.trim(), "node18", "Node20's second child is indeed node18" ); }, }, ]; add_task(async function () { const { inspector } = await openInspectorForURL(TEST_URL); info("Expanding all markup-view nodes"); await inspector.markup.expandAll(); for (let { desc, test, check, numMutations } of TEST_DATA) { info("Starting test: " + desc); numMutations = numMutations || 1; info("Executing the test markup mutation"); // If a test expects more than one mutation it may come through in a single // event or possibly in multiples. let seenMutations = 0; const promise = new Promise(resolve => { inspector.on("markupmutation", function onmutation(mutations) { seenMutations += mutations.length; info( "Receieved " + seenMutations + " mutations, expecting at least " + numMutations ); if (seenMutations >= numMutations) { inspector.off("markupmutation", onmutation); resolve(); } }); }); await test(inspector); await promise; info("Expanding all markup-view nodes to make sure new nodes are imported"); await inspector.markup.expandAll(); info("Checking the markup-view content"); await check(inspector); } });