<!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>