<!DOCTYPE HTML> <html> <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=641821 --> <head> <meta charset="utf-8"> <title>Test for Bug 641821</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> </head> <body onload="runTest()"> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641821">Mozilla Bug 641821</a> <p id="display"></p> <div id="content" style="display: none"> </div> <pre id="test"> <script type="application/javascript"> /** Test for Bug 641821 **/ SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly. (But make sure marquee has time to initialize itself.)"); var div = document.createElement("div"); var M; if ("MozMutationObserver" in window) { M = window.MozMutationObserver; } else if ("WebKitMutationObserver" in window) { M = window.WebKitMutationObserver; } else { M = window.MutationObserver; } function log(str) { var d = document.createElement("div"); d.textContent = str; if (str.indexOf("PASSED") >= 0) { d.setAttribute("style", "color: green;"); } else { d.setAttribute("style", "color: red;"); } document.getElementById("log").appendChild(d); } // Some helper functions so that this test runs also outside mochitest. if (!("ok" in window)) { window.ok = function(val, str) { log(str + (val ? " PASSED\n" : " FAILED\n")); } } if (!("is" in window)) { window.is = function(val, refVal, str) { log(str + (val == refVal? " PASSED " : " FAILED ") + (val != refVal ? "expected " + refVal + " got " + val + "\n" : "\n")); } } if (!("isnot" in window)) { window.isnot = function(val, refVal, str) { log(str + (val != refVal? " PASSED " : " FAILED ") + (val == refVal ? "Didn't expect " + refVal + "\n" : "\n")); } } if (!("SimpleTest" in window)) { window.SimpleTest = { finish: function() { document.getElementById("log").appendChild(document.createTextNode("DONE")); }, waitForExplicitFinish: function() {} } } function then(thenFn) { setTimeout(function() { if (thenFn) { setTimeout(thenFn, 0); } else { SimpleTest.finish(); } }, 0); } var m; var m2; var m3; var m4; // Checks basic parameter validation and normal 'this' handling. // Tests also basic attribute handling. function runTest() { m = new M(function(){}); ok(m, "MutationObserver supported"); var e = null; try { m.observe(document, {}); } catch (ex) { e = ex; } ok(e, "Should have thrown an exception"); is(e.name, "TypeError", "Should have thrown TypeError"); e = null; try { m.observe(document, { childList: true, attributeOldValue: true }); } catch (ex) { e = ex; } ok(!e, "Shouldn't have thrown an exception"); e = null; try { m.observe(document, { childList: true, attributeFilter: ["foo"] }); } catch (ex) { e = ex; } ok(!e, "Shouldn't have thrown an exception"); e = null; try { m.observe(document, { childList: true, characterDataOldValue: true }); } catch (ex) { e = ex; } ok(!e, "Shouldn't have thrown an exception"); e = null; try { m.observe(document); } catch (ex) { e = ex; } ok(e, "Should have thrown an exception"); m = new M(function(records, observer) { is(observer, m, "2nd parameter should be the mutation observer"); is(observer, this, "2nd parameter should be 'this'"); is(records.length, 1, "Should have one record."); is(records[0].type, "attributes", "Should have got attributes record"); is(records[0].target, div, "Should have got div as target"); is(records[0].attributeName, "foo", "Should have got record about foo attribute"); observer.disconnect(); then(testThisBind); m = null; }); m.observe(div, { attributes: true, attributeFilter: ["foo"] }); is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].attributes, true); is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].attributeFilter.length, 1) is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].attributeFilter[0], "foo") div.setAttribute("foo", "bar"); } // 'this' handling when fn.bind() is used. function testThisBind() { var child = div.appendChild(document.createElement("div")); var gchild = child.appendChild(document.createElement("div")); m = new M((function(records, observer) { is(observer, m, "2nd parameter should be the mutation observer"); isnot(observer, this, "2nd parameter should be 'this'"); is(records.length, 3, "Should have one record."); is(records[0].type, "attributes", "Should have got attributes record"); is(records[0].target, div, "Should have got div as target"); is(records[0].attributeName, "foo", "Should have got record about foo attribute"); is(records[0].oldValue, "bar", "oldValue should be bar"); is(records[1].type, "attributes", "Should have got attributes record"); is(records[1].target, div, "Should have got div as target"); is(records[1].attributeName, "foo", "Should have got record about foo attribute"); is(records[1].oldValue, "bar2", "oldValue should be bar2"); is(records[2].type, "attributes", "Should have got attributes record"); is(records[2].target, gchild, "Should have got div as target"); is(records[2].attributeName, "foo", "Should have got record about foo attribute"); is(records[2].oldValue, null, "oldValue should be bar2"); observer.disconnect(); then(testCharacterData); m = null; }).bind(window)); m.observe(div, { attributes: true, attributeOldValue: true, subtree: true }); is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].attributes, true) is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].attributeOldValue, true) is(SpecialPowers.wrap(div).getBoundMutationObservers()[0].getObservingInfo()[0].subtree, true) div.setAttribute("foo", "bar2"); div.removeAttribute("foo"); div.removeChild(child); child.removeChild(gchild); div.appendChild(gchild); div.removeChild(gchild); gchild.setAttribute("foo", "bar"); } function testCharacterData() { m = new M(function(records, observer) { is(records[0].type, "characterData", "Should have got characterData"); is(records[0].oldValue, null, "Shouldn't have got oldData"); observer.disconnect(); m = null; }); m2 = new M(function(records, observer) { is(records[0].type, "characterData", "Should have got characterData"); is(records[0].oldValue, "foo", "Should have got oldData"); observer.disconnect(); m2 = null; }); m3 = new M(function(records, observer) { ok(false, "This should not be called!"); observer.disconnect(); m3 = null; }); m4 = new M(function(records, observer) { is(records[0].oldValue, null, "Shouldn't have got oldData"); observer.disconnect(); m3.disconnect(); m3 = null; then(testChildList); m4 = null; }); div.appendChild(document.createTextNode("foo")); m.observe(div, { characterData: true, subtree: true }); m2.observe(div, { characterData: true, characterDataOldValue: true, subtree: true}); // If observing the same node twice, only the latter option should apply. m3.observe(div, { characterData: true, subtree: true }); m3.observe(div, { characterData: true, subtree: false }); m4.observe(div.firstChild, { characterData: true, subtree: false }); is(SpecialPowers.wrap(div).getBoundMutationObservers().length, 3) is(SpecialPowers.wrap(div).getBoundMutationObservers()[2].getObservingInfo()[0].characterData, true) is(SpecialPowers.wrap(div).getBoundMutationObservers()[2].getObservingInfo()[0].subtree, false) div.firstChild.data = "bar"; } function testChildList() { var fc = div.firstChild; m = new M(function(records, observer) { is(records[0].type, "childList", "Should have got childList"); is(records[0].addedNodes.length, 0, "Shouldn't have got addedNodes"); is(records[0].removedNodes.length, 1, "Should have got removedNodes"); is(records[0].removedNodes[0], fc, "Should have removed a text node"); observer.disconnect(); then(testChildList2); m = null; }); m.observe(div, { childList: true}); div.removeChild(div.firstChild); } function testChildList2() { div.innerHTML = "<span>1</span><span>2</span>"; m = new M(function(records, observer) { is(records[0].type, "childList", "Should have got childList"); is(records[0].removedNodes.length, 2, "Should have got removedNodes"); is(records[0].addedNodes.length, 1, "Should have got addedNodes"); observer.disconnect(); then(testChildList3); m = null; }); m.observe(div, { childList: true }); div.innerHTML = "<span><span>foo</span></span>"; } function testChildList3() { m = new M(function(records, observer) { is(records[0].type, "childList", "Should have got childList"); is(records[0].removedNodes.length, 1, "Should have got removedNodes"); is(records[0].addedNodes.length, 1, "Should have got addedNodes"); observer.disconnect(); then(testChildList4); m = null; }); m.observe(div, { childList: true }); div.textContent = "hello"; } function testChildList4() { div.textContent = null; var df = document.createDocumentFragment(); var t1 = df.appendChild(document.createTextNode("Hello ")); var t2 = df.appendChild(document.createTextNode("world!")); var s1 = div.appendChild(document.createElement("span")); s1.textContent = "foo"; var s2 = div.appendChild(document.createElement("span")); function callback(records, observer) { is(records.length, 3, "Should have got one record for removing nodes from document fragment and one record for adding them to div"); is(records[0].removedNodes.length, 2, "Should have got removedNodes"); is(records[0].removedNodes[0], t1, "Should be the 1st textnode"); is(records[0].removedNodes[1], t2, "Should be the 2nd textnode"); is(records[1].addedNodes.length, 2, "Should have got addedNodes"); is(records[1].addedNodes[0], t1, "Should be the 1st textnode"); is(records[1].addedNodes[1], t2, "Should be the 2nd textnode"); is(records[1].previousSibling, s1, "Should have previousSibling"); is(records[1].nextSibling, s2, "Should have nextSibling"); is(records[2].type, "characterData", "3rd record should be characterData"); is(records[2].target, t1, "target should be the textnode"); is(records[2].oldValue, "Hello ", "oldValue was 'Hello '"); observer.disconnect(); then(testChildList5); m = null; }; m = new M(callback); m.observe(df, { childList: true, characterData: true, characterDataOldValue: true, subtree: true }); is(SpecialPowers.wrap(df).getBoundMutationObservers()[0].getObservingInfo()[0].childList, true) is(SpecialPowers.wrap(df).getBoundMutationObservers()[0].getObservingInfo()[0].characterData, true) is(SpecialPowers.wrap(df).getBoundMutationObservers()[0].getObservingInfo()[0].characterDataOldValue, true) is(SpecialPowers.wrap(df).getBoundMutationObservers()[0].getObservingInfo()[0].subtree, true) ok(SpecialPowers.compare(SpecialPowers.wrap(df).getBoundMutationObservers()[0].mutationCallback, callback)) m.observe(div, { childList: true }); is(SpecialPowers.wrap(df).getBoundMutationObservers()[0].getObservingInfo().length, 2) // Make sure transient observers aren't leaked. var leakTest = new M(function(){}); leakTest.observe(div, { characterData: true, subtree: true }); div.insertBefore(df, s2); s1.firstChild.data = "bar"; // This should *not* create a record. t1.data = "Hello the whole "; // This should create a record. } function testChildList5() { div.textContent = null; var c1 = div.appendChild(document.createElement("div")); var c2 = document.createElement("div"); var div2 = document.createElement("div"); var c3 = div2.appendChild(document.createElement("div")); var c4 = document.createElement("div"); var c5 = document.createElement("div"); var df = document.createDocumentFragment(); var emptyDF = document.createDocumentFragment(); var dfc1 = df.appendChild(document.createElement("div")); var dfc2 = df.appendChild(document.createElement("div")); var dfc3 = df.appendChild(document.createElement("div")); m = new M(function(records, observer) { is(records.length, 6 , ""); is(records[0].removedNodes.length, 1, "Should have got removedNodes"); is(records[0].removedNodes[0], c1, ""); is(records[0].addedNodes.length, 1, "Should have got addedNodes"); is(records[0].addedNodes[0], c2, ""); is(records[0].previousSibling, null, ""); is(records[0].nextSibling, null, ""); is(records[1].removedNodes.length, 1, "Should have got removedNodes"); is(records[1].removedNodes[0], c3, ""); is(records[1].addedNodes.length, 0, "Shouldn't have got addedNodes"); is(records[1].previousSibling, null, ""); is(records[1].nextSibling, null, ""); is(records[2].removedNodes.length, 1, "Should have got removedNodes"); is(records[2].removedNodes[0], c2, ""); is(records[2].addedNodes.length, 1, "Should have got addedNodes"); is(records[2].addedNodes[0], c3, ""); is(records[2].previousSibling, null, ""); is(records[2].nextSibling, null, ""); // Check document fragment handling is(records[5].removedNodes.length, 1, ""); is(records[5].removedNodes[0], c4, ""); is(records[5].addedNodes.length, 3, ""); is(records[5].addedNodes[0], dfc1, ""); is(records[5].addedNodes[1], dfc2, ""); is(records[5].addedNodes[2], dfc3, ""); is(records[5].previousSibling, c3, ""); is(records[5].nextSibling, c5, ""); observer.disconnect(); then(testAdoptNode); m = null; }); m.observe(div, { childList: true, subtree: true }); m.observe(div2, { childList: true, subtree: true }); div.replaceChild(c2, c1); div.replaceChild(c3, c2); div.appendChild(c4); div.appendChild(c5); div.replaceChild(df, c4); div.appendChild(emptyDF); // empty document shouldn't cause mutation records } function testAdoptNode() { var d1 = document.implementation.createHTMLDocument(null); var d2 = document.implementation.createHTMLDocument(null); var addedNode; m = new M(function(records, observer) { is(records.length, 3, "Should have 2 records"); is(records[0].target.ownerDocument, d1, "ownerDocument should be the initial document") is(records[1].target.ownerDocument, d2, "ownerDocument should be the new document"); is(records[2].type, "attributes", "Should have got attribute mutation") is(records[2].attributeName, "foo", "Should have got foo attribute mutation") observer.disconnect(); then(testOuterHTML); m = null; }); m.observe(d1, { childList: true, subtree: true, attributes: true }); d2.body.appendChild(d1.body); addedNode = d2.body.lastChild.appendChild(d2.createElement("div")); addedNode.setAttribute("foo", "bar"); } function testOuterHTML() { var doc = document.implementation.createHTMLDocument(null); var d1 = doc.body.appendChild(document.createElement("div")); var d2 = doc.body.appendChild(document.createElement("div")); var d3 = doc.body.appendChild(document.createElement("div")); var d4 = doc.body.appendChild(document.createElement("div")); m = new M(function(records, observer) { is(records.length, 4, "Should have 1 record"); is(records[0].removedNodes.length, 1, "Should have 1 removed nodes"); is(records[0].addedNodes.length, 2, "Should have 2 added nodes"); is(records[0].previousSibling, null, ""); is(records[0].nextSibling, d2, ""); is(records[1].removedNodes.length, 1, "Should have 1 removed nodes"); is(records[1].addedNodes.length, 2, "Should have 2 added nodes"); is(records[1].previousSibling, records[0].addedNodes[1], ""); is(records[1].nextSibling, d3, ""); is(records[2].removedNodes.length, 1, "Should have 1 removed nodes"); is(records[2].addedNodes.length, 2, "Should have 2 added nodes"); is(records[2].previousSibling, records[1].addedNodes[1], ""); is(records[2].nextSibling, d4, ""); is(records[3].removedNodes.length, 1, "Should have 1 removed nodes"); is(records[3].addedNodes.length, 0); is(records[3].previousSibling, records[2].addedNodes[1], ""); is(records[3].nextSibling, null, ""); observer.disconnect(); then(testInsertAdjacentHTML); m = null; }); m.observe(doc, { childList: true, subtree: true }); d1.outerHTML = "<div>1</div><div>1</div>"; d2.outerHTML = "<div>2</div><div>2</div>"; d3.outerHTML = "<div>3</div><div>3</div>"; d4.outerHTML = ""; } function testInsertAdjacentHTML() { var doc = document.implementation.createHTMLDocument(null); var d1 = doc.body.appendChild(document.createElement("div")); var d2 = doc.body.appendChild(document.createElement("div")); var d3 = doc.body.appendChild(document.createElement("div")); var d4 = doc.body.appendChild(document.createElement("div")); m = new M(function(records, observer) { is(records.length, 4, ""); is(records[0].target, doc.body, ""); is(records[0].previousSibling, null, ""); is(records[0].nextSibling, d1, ""); is(records[1].target, d2, ""); is(records[1].previousSibling, null, ""); is(records[1].nextSibling, null, ""); is(records[2].target, d3, ""); is(records[2].previousSibling, null, ""); is(records[2].nextSibling, null, ""); is(records[3].target, doc.body, ""); is(records[3].previousSibling, d4, ""); is(records[3].nextSibling, null, ""); observer.disconnect(); then(testSyncXHR); m = null; }); m.observe(doc, { childList: true, subtree: true }); d1.insertAdjacentHTML("beforebegin", "<div></div><div></div>"); d2.insertAdjacentHTML("afterbegin", "<div></div><div></div>"); d3.insertAdjacentHTML("beforeend", "<div></div><div></div>"); d4.insertAdjacentHTML("afterend", "<div></div><div></div>"); } var callbackHandled = false; function testSyncXHR() { div.textContent = null; m = new M(function(records, observer) { is(records.length, 1, ""); is(records[0].addedNodes.length, 1, ""); callbackHandled = true; observer.disconnect(); m = null; }); m.observe(div, { childList: true, subtree: true }); div.innerHTML = "<div>hello</div>"; var x = new XMLHttpRequest(); x.open("GET", window.location, false); x.send(); ok(!callbackHandled, "Shouldn't have called the mutation callback!"); setTimeout(testSyncXHR2, 0); } function testSyncXHR2() { ok(callbackHandled, "Should have called the mutation callback!"); then(testModalDialog); } function testModalDialog() { var didHandleCallback = false; div.innerHTML = "<span>1</span><span>2</span>"; m = new M(function(records, observer) { is(records[0].type, "childList", "Should have got childList"); is(records[0].removedNodes.length, 2, "Should have got removedNodes"); is(records[0].addedNodes.length, 1, "Should have got addedNodes"); observer.disconnect(); m = null; didHandleCallback = true; }); m.observe(div, { childList: true }); div.innerHTML = "<span><span>foo</span></span>"; try { window.showModalDialog("mutationobserver_dialog.html"); ok(didHandleCallback, "Should have called the callback while showing modal dialog!"); } catch(e) { todo(false, "showModalDialog not implemented on this platform"); } then(testTakeRecords); } function testTakeRecords() { var s = "<span>1</span><span>2</span>"; div.innerHTML = s; var takenRecords; m = new M(function(records, observer) { is(records.length, 3, "Should have got 3 records"); is(records[0].type, "attributes", "Should have got attributes"); is(records[0].attributeName, "foo", ""); is(records[0].attributeNamespace, null, ""); is(records[0].prevValue, null, ""); is(records[1].type, "childList", "Should have got childList"); is(records[1].removedNodes.length, 2, "Should have got removedNodes"); is(records[1].addedNodes.length, 2, "Should have got addedNodes"); is(records[2].type, "attributes", "Should have got attributes"); is(records[2].attributeName, "foo", ""); is(records.length, takenRecords.length, "Should have had similar mutations"); is(records[0].type, takenRecords[0].type, "Should have had similar mutations"); is(records[1].type, takenRecords[1].type, "Should have had similar mutations"); is(records[2].type, takenRecords[2].type, "Should have had similar mutations"); is(records[1].removedNodes.length, takenRecords[1].removedNodes.length, "Should have had similar mutations"); is(records[1].addedNodes.length, takenRecords[1].addedNodes.length, "Should have had similar mutations"); is(m.takeRecords().length, 0, "Shouldn't have any records"); observer.disconnect(); then(testMutationObserverAndEvents); m = null; }); m.observe(div, { childList: true, attributes: true }); div.setAttribute("foo", "bar"); div.innerHTML = s; div.removeAttribute("foo"); takenRecords = m.takeRecords(); div.setAttribute("foo", "bar"); div.innerHTML = s; div.removeAttribute("foo"); } function testTakeRecords() { function mutationListener(e) { ++mutationEventCount; is(e.attrChange, MutationEvent.ADDITION, "unexpected change"); } m = new M(function(records, observer) { is(records.length, 2, "Should have got 2 records"); is(records[0].type, "attributes", "Should have got attributes"); is(records[0].attributeName, "foo", ""); is(records[0].oldValue, null, ""); is(records[1].type, "attributes", "Should have got attributes"); is(records[1].attributeName, "foo", ""); is(records[1].oldValue, "bar", ""); observer.disconnect(); div.removeEventListener("DOMAttrModified", mutationListener); then(testExpandos); m = null; }); m.observe(div, { attributes: true, attributeOldValue: true }); // Note, [0] points to a mutation observer which is there for a leak test! ok(SpecialPowers.compare(SpecialPowers.wrap(div).getBoundMutationObservers()[1], m)); var mutationEventCount = 0; div.addEventListener("DOMAttrModified", mutationListener); div.setAttribute("foo", "bar"); div.setAttribute("foo", "bar"); is(mutationEventCount, 1, "Should have got only one mutation event!"); } function testExpandos() { var m2 = new M(function(records, observer) { is(observer.expandoProperty, true); observer.disconnect(); then(testOutsideShadowDOM); }); m2.expandoProperty = true; m2.observe(div, { attributes: true }); m2 = null; if (SpecialPowers) { // Run GC several times to see if the expando property disappears. SpecialPowers.gc(); SpecialPowers.gc(); SpecialPowers.gc(); SpecialPowers.gc(); } div.setAttribute("foo", "bar2"); } function testOutsideShadowDOM() { var m = new M(function(records, observer) { is(records.length, 1); is(records[0].type, "attributes", "Should have got attributes"); observer.disconnect(); then(testInsideShadowDOM); }); m.observe(div, { attributes: true, childList: true, characterData: true, subtree: true }) var sr = div.createShadowRoot(); sr.innerHTML = "<div" + ">text</" + "div>"; sr.firstChild.setAttribute("foo", "bar"); sr.firstChild.firstChild.data = "text2"; sr.firstChild.appendChild(document.createElement("div")); div.setAttribute("foo", "bar"); } function testInsideShadowDOM() { var m = new M(function(records, observer) { is(records.length, 4); is(records[0].type, "childList"); is(records[1].type, "attributes"); is(records[2].type, "characterData"); is(records[3].type, "childList"); observer.disconnect(); then(testMarquee); }); var sr = div.createShadowRoot(); m.observe(sr, { attributes: true, childList: true, characterData: true, subtree: true }); sr.innerHTML = "<div" + ">text</" + "div>"; sr.firstChild.setAttribute("foo", "bar"); sr.firstChild.firstChild.data = "text2"; sr.firstChild.appendChild(document.createElement("div")); div.setAttribute("foo", "bar2"); } function testMarquee() { var m = new M(function(records, observer) { is(records.length, 1); is(records[0].type, "attributes"); is(records[0].attributeName, "ok"); is(records[0].oldValue, null); observer.disconnect(); then(testStyleCreate); }); var marquee = document.createElement("marquee"); m.observe(marquee, { attributes: true, attributeOldValue: true, childList: true, characterData: true, subtree: true }); document.body.appendChild(marquee); setTimeout(function() {marquee.setAttribute("ok", "ok")}, 500); } function testStyleCreate() { m = new M(function(records, observer) { is(records.length, 1, "number of records"); is(records[0].type, "attributes", "record.type"); is(records[0].attributeName, "style", "record.attributeName"); is(records[0].oldValue, null, "record.oldValue"); isnot(div.getAttribute("style"), null, "style attribute after creation"); observer.disconnect(); m = null; div.removeAttribute("style"); then(testStyleModify); }); m.observe(div, { attributes: true, attributeOldValue: true }); is(div.getAttribute("style"), null, "style attribute before creation"); div.style.color = "blue"; } function testStyleModify() { div.style.color = "yellow"; m = new M(function(records, observer) { is(records.length, 1, "number of records"); is(records[0].type, "attributes", "record.type"); is(records[0].attributeName, "style", "record.attributeName"); isnot(div.getAttribute("style"), null, "style attribute after modification"); observer.disconnect(); m = null; div.removeAttribute("style"); then(testStyleRead); }); m.observe(div, { attributes: true }); isnot(div.getAttribute("style"), null, "style attribute before modification"); div.style.color = "blue"; } function testStyleRead() { m = new M(function(records, observer) { is(records.length, 1, "number of records"); is(records[0].type, "attributes", "record.type"); is(records[0].attributeName, "data-test", "record.attributeName"); is(div.getAttribute("style"), null, "style attribute after read"); observer.disconnect(); div.removeAttribute("data-test"); m = null; then(testStyleRemoveProperty); }); m.observe(div, { attributes: true }); is(div.getAttribute("style"), null, "style attribute before read"); var value = div.style.color; // shouldn't generate any mutation records div.setAttribute("data-test", "a"); } function testStyleRemoveProperty() { div.style.color = "blue"; m = new M(function(records, observer) { is(records.length, 1, "number of records"); is(records[0].type, "attributes", "record.type"); is(records[0].attributeName, "style", "record.attributeName"); isnot(div.getAttribute("style"), null, "style attribute after successful removeProperty"); observer.disconnect(); m = null; div.removeAttribute("style"); then(testStyleRemoveProperty2); }); m.observe(div, { attributes: true }); isnot(div.getAttribute("style"), null, "style attribute before successful removeProperty"); div.style.removeProperty("color"); } function testStyleRemoveProperty2() { m = new M(function(records, observer) { is(records.length, 1, "number of records"); is(records[0].type, "attributes", "record.type"); is(records[0].attributeName, "data-test", "record.attributeName"); is(div.getAttribute("style"), null, "style attribute after unsuccessful removeProperty"); observer.disconnect(); m = null; div.removeAttribute("data-test"); then(testAttributeRecordMerging1); }); m.observe(div, { attributes: true }); is(div.getAttribute("style"), null, "style attribute before unsuccessful removeProperty"); div.style.removeProperty("color"); // shouldn't generate any mutation records div.setAttribute("data-test", "a"); } function testAttributeRecordMerging1() { ok(true, "testAttributeRecordMerging1"); var m = new M(function(records, observer) { is(records.length, 2); is(records[0].type, "attributes"); is(records[0].target, div); is(records[0].attributeName, "foo"); is(records[0].attributeNamespace, null); is(records[0].oldValue, null); is(records[1].type, "attributes"); is(records[1].target, div.firstChild); is(records[1].attributeName, "foo"); is(records[1].attributeNamespace, null); is(records[1].oldValue, null); observer.disconnect(); div.innerHTML = ""; div.removeAttribute("foo"); then(testAttributeRecordMerging2); }); m.observe(div, { attributes: true, subtree: true }); SpecialPowers.wrap(m).mergeAttributeRecords = true; div.setAttribute("foo", "bar_1"); div.setAttribute("foo", "bar_2"); div.innerHTML = "<div></div>"; div.firstChild.setAttribute("foo", "bar_1"); div.firstChild.setAttribute("foo", "bar_2"); } function testAttributeRecordMerging2() { ok(true, "testAttributeRecordMerging2"); var m = new M(function(records, observer) { is(records.length, 2); is(records[0].type, "attributes"); is(records[0].target, div); is(records[0].attributeName, "foo"); is(records[0].attributeNamespace, null); is(records[0].oldValue, "initial"); is(records[1].type, "attributes"); is(records[1].target, div.firstChild); is(records[1].attributeName, "foo"); is(records[1].attributeNamespace, null); is(records[1].oldValue, "initial"); observer.disconnect(); div.innerHTML = ""; div.removeAttribute("foo"); then(testAttributeRecordMerging3); }); div.setAttribute("foo", "initial"); div.innerHTML = "<div></div>"; div.firstChild.setAttribute("foo", "initial"); m.observe(div, { attributes: true, subtree: true, attributeOldValue: true }); SpecialPowers.wrap(m).mergeAttributeRecords = true; div.setAttribute("foo", "bar_1"); div.setAttribute("foo", "bar_2"); div.firstChild.setAttribute("foo", "bar_1"); div.firstChild.setAttribute("foo", "bar_2"); } function testAttributeRecordMerging3() { ok(true, "testAttributeRecordMerging3"); var m = new M(function(records, observer) { is(records.length, 4); is(records[0].type, "attributes"); is(records[0].target, div); is(records[0].attributeName, "foo"); is(records[0].attributeNamespace, null); is(records[0].oldValue, "initial"); is(records[1].type, "attributes"); is(records[1].target, div.firstChild); is(records[1].attributeName, "foo"); is(records[1].attributeNamespace, null); is(records[1].oldValue, "initial"); is(records[2].type, "attributes"); is(records[2].target, div); is(records[2].attributeName, "foo"); is(records[2].attributeNamespace, null); is(records[2].oldValue, "bar_1"); is(records[3].type, "attributes"); is(records[3].target, div.firstChild); is(records[3].attributeName, "foo"); is(records[3].attributeNamespace, null); is(records[3].oldValue, "bar_1"); observer.disconnect(); div.innerHTML = ""; div.removeAttribute("foo"); then(testAttributeRecordMerging4); }); div.setAttribute("foo", "initial"); div.innerHTML = "<div></div>"; div.firstChild.setAttribute("foo", "initial"); m.observe(div, { attributes: true, subtree: true, attributeOldValue: true }); SpecialPowers.wrap(m).mergeAttributeRecords = true; // No merging should happen. div.setAttribute("foo", "bar_1"); div.firstChild.setAttribute("foo", "bar_1"); div.setAttribute("foo", "bar_2"); div.firstChild.setAttribute("foo", "bar_2"); } function testAttributeRecordMerging4() { ok(true, "testAttributeRecordMerging4"); var m = new M(function(records, observer) { }); div.setAttribute("foo", "initial"); div.innerHTML = "<div></div>"; div.firstChild.setAttribute("foo", "initial"); m.observe(div, { attributes: true, subtree: true, attributeOldValue: true }); SpecialPowers.wrap(m).mergeAttributeRecords = true; div.setAttribute("foo", "bar_1"); div.setAttribute("foo", "bar_2"); div.firstChild.setAttribute("foo", "bar_1"); div.firstChild.setAttribute("foo", "bar_2"); var records = m.takeRecords(); is(records.length, 2); is(records[0].type, "attributes"); is(records[0].target, div); is(records[0].attributeName, "foo"); is(records[0].attributeNamespace, null); is(records[0].oldValue, "initial"); is(records[1].type, "attributes"); is(records[1].target, div.firstChild); is(records[1].attributeName, "foo"); is(records[1].attributeNamespace, null); is(records[1].oldValue, "initial"); m.disconnect(); div.innerHTML = ""; div.removeAttribute("foo"); then(testChromeOnly); } function testChromeOnly() { // Content can't access nativeAnonymousChildList try { var mo = new M(function(records, observer) { }); mo.observe(div, { nativeAnonymousChildList: true }); ok(false, "Should have thrown when trying to observe with chrome-only init"); } catch (e) { ok(true, "Throws when trying to observe with chrome-only init"); } then(); } SimpleTest.waitForExplicitFinish(); </script> </pre> <div id="log"> </div> </body> </html>