text: // XXX make sure that when tests use id="..." to trigger patching, "preserve" happens
  // XXX test that events inside constant regions still work after patching
  // XXX test arguments to landmark rendered callback
  // XXX test variable wrapping (eg TR vs THEAD) inside each branch of Spark.list?
  
  
  Spark._checkIECompliance = true;
  
  (function () {
  
  // Tests can use {preserve: idNameLabels} or renderWithPreservation
  // to cause any element with an id or name to be preserved.  This effect
  // is similar to what the preserve-inputs package does, though it applies
  // to all elements, not just inputs.
  
  var idNameLabels = {
    '*[id], *[name]': Spark._labelFromIdOrName
  };
  
  var renderWithPreservation = function (htmlFunc) {
    return Meteor.render(function () {
      return Spark.createLandmark({ preserve: idNameLabels}, htmlFunc);
    });
  };
  
  var eventmap = function (/*args*/) {
    // support event_buf as final argument
    var event_buf = null;
    if (arguments.length && _.isArray(arguments[arguments.length-1])) {
      event_buf = arguments[arguments.length-1];
      arguments.length--;
    }
    var events = {};
    _.each(arguments, function (esel) {
      var etyp = esel.split(' ')[0];
      events[esel] = function (evt) {
        if (evt.type !== etyp)
          throw new Error(etyp+" event arrived as "+evt.type);
        (event_buf || this).push(esel);
      };
    });
    return events;
  };
  
  var nodesToArray = function (array) {
    // Starting in underscore 1.4, _.toArray does not work right on a node
    // list in IE8. This is a workaround to support IE8.
    return _.map(array, _.identity);
  };
  
  Tinytest.add("spark - assembly", function (test) {
  
    var furtherCanon = function(str) {
      // further canonicalize innerHTML in IE by adding close
      // li tags to "<ul><li>one<li>two<li>three</li></ul>"
      return str.replace(/<li>(\w*)(?=<li>)/g, function(s) {
        return s+"</li>";
      });
    };
  
    var doTest = function (calc) {
      var frag = Spark.render(function () {
        return calc(function (str, expected) {
          return Spark.setDataContext(null, str);
        });
      });
      var groups = [];
      var html = calc(function (str, expected, noRange) {
        if (arguments.length > 1)
          str = expected;
        if (! noRange)
          groups.push(str);
        return str;
      });
      var f = WrappedFrag(frag);
      test.equal(furtherCanon(f.html()), html);
  
      var actualGroups = [];
      var tempRange = new LiveRange(Spark._TAG, frag);
      tempRange.visit(function (isStart, rng) {
        if (! isStart && rng.type === Spark._ANNOTATION_DATA)
          actualGroups.push(furtherCanon(canonicalizeHtml(
            DomUtils.rangeToHtml(rng.firstNode(), rng.lastNode()))));
      });
      test.equal(actualGroups.join(','), groups.join(','));
    };
  
    doTest(function (A) { return "<p>Hello</p>"; });
    doTest(function (A) { return "<td>Hello</td><td>World</td>"; });
    doTest(function (A) { return "<td>"+A("Hello")+"</td>"; });
    doTest(function (A) { return A("<td>"+A("Hello")+"</td>"); });
    doTest(function (A) { return A(A(A(A(A(A("foo")))))); });
    doTest(
      function (A) { return "<div>Yo"+A("<p>Hello "+A(A("World")),"<p>Hello World</p>")+
                    "</div>"; });
    doTest(function (A) {
      return A("<ul>"+A("<li>one","<li>one</li>")+
               A("<li>two","<li>two</li>")+
               A("<li>three","<li>three</li>"),
               "<ul><li>one</li><li>two</li><li>three</li></ul>"); });
  
    doTest(function (A) {
      return A("<table>"+A("<tr>"+A("<td>"+A("Hi")+"</td>")+"</tr>")+"</table>",
               "<table><tbody><tr><td>Hi</td></tr></tbody></table>");
    });
  
    test.throws(function () {
      doTest(function (A) {
        var z = A("Hello");
        return z+z;
      });
    });
  
    var frag = Spark.render(function () {
      return '<div foo="abc' +
        Spark.setDataContext(null, "bar") +
        'xyz">Hello</div>';
    });
    var div = frag.firstChild;
    test.equal(div.nodeName, "DIV");
    var attrValue = div.getAttribute('foo');
    test.isTrue(attrValue.indexOf('abc<!--') === 0, attrValue);
    test.isTrue(attrValue.indexOf('-->xyz') >= 0, attrValue);
  });
  
  
  Tinytest.add("spark - repeat inclusion", function(test) {
    test.throws(function() {
      var frag = Spark.render(function() {
        var x = Spark.setDataContext({}, "abc");
        return x + x;
      });
    });
  });
  
  
  Tinytest.add("spark - replace tag contents", function (test) {
  
    // adapted from nateps / metamorph
  
    var do_onscreen = function (f) {
      var div = OnscreenDiv();
      var stuff = {
        div: div,
        node: _.bind(div.node, div),
        render: function (rfunc) {
          div.node().appendChild(Meteor.render(rfunc));
        }
      };
  
      f.call(stuff);
  
      div.kill();
    };
  
    var R, div;
  
    // basic text replace
  
    do_onscreen(function () {
      R = ReactiveVar("one two three");
      this.render(function () {
        return R.get();
      });
      R.set("three four five six");
      Deps.flush();
      test.equal(this.div.html(), "three four five six");
    });
  
    // work inside a table
  
    do_onscreen(function () {
      R = ReactiveVar("<tr><td>HI!</td></tr>");
      this.render(function () {
        return "<table id='morphing'>" + R.get() + "</table>";
      });
  
      test.equal($(this.node()).find("#morphing td").text(), "HI!");
      R.set("<tr><td>BUH BYE!</td></tr>");
      Deps.flush();
      test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
    });
  
    // work inside a tbody
  
    do_onscreen(function () {
      R = ReactiveVar("<tr><td>HI!</td></tr>");
      this.render(function () {
        return "<table id='morphing'><tbody>" + R.get() + "</tbody></table>";
      });
  
      test.equal($(this.node()).find("#morphing td").text(), "HI!");
      R.set("<tr><td>BUH BYE!</td></tr>");
      Deps.flush();
      test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
    });
  
    // work inside a tr
  
    do_onscreen(function () {
      R = ReactiveVar("<td>HI!</td>");
      this.render(function () {
        return "<table id='morphing'><tr>" + R.get() + "</tr></table>";
      });
  
      test.equal($(this.node()).find("#morphing td").text(), "HI!");
      R.set("<td>BUH BYE!</td>");
      Deps.flush();
      test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
    });
  
    // work inside a ul
  
    do_onscreen(function () {
      R = ReactiveVar("<li>HI!</li>");
      this.render(function () {
        return "<ul id='morphing'>" + R.get() + "</ul>";
      });
  
      test.equal($(this.node()).find("#morphing li").text(), "HI!");
      R.set("<li>BUH BYE!</li>");
      Deps.flush();
      test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!");
    });
  
    // work inside a select
  
    do_onscreen(function () {
      R = ReactiveVar("<option>HI!</option>");
      this.render(function () {
        return "<select id='morphing'>" + R.get() + "</select>";
      });
  
      test.equal($(this.node()).find("#morphing option").text(), "HI!");
      R.set("<option>BUH BYE!</option>");
      Deps.flush();
      test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!");
    });
  
    // list of select options
  
    do_onscreen(function () {
      var c = new LocalCollection();
      c.insert({name: 'Hamburger', value: 1});
      c.insert({name: 'Cheeseburger', value: 2});
      this.render(function () {
        return "<select id='morphing' name='fred'>" +
          Spark.list(c.find({}, {sort: ['value']}), function (doc) {
            return '<option value="' + doc.value + '">' + doc.name + '</option>';
          }) +
          "</select>";
      });
  
      var furtherCanon = function (html) {
        return html.replace(/\s*selected="selected"/g, '');
      };
  
      test.equal(furtherCanon(this.div.html()),
                 '<select id="morphing" name="fred">' +
                 '<option value="1">Hamburger</option>' +
                 '<option value="2">Cheeseburger</option>' +
                 '</select>');
      c.insert({name: 'Chicken Snickers', value: 8});
      Deps.flush();
      test.equal(furtherCanon(this.div.html()),
                 '<select id="morphing" name="fred">' +
                 '<option value="1">Hamburger</option>' +
                 '<option value="2">Cheeseburger</option>' +
                 '<option value="8">Chicken Snickers</option>' +
                 '</select>');
      c.remove({value: 1});
      c.remove({value: 2});
      Deps.flush();
      test.equal(furtherCanon(this.div.html()),
                 '<select id="morphing" name="fred">' +
                 '<option value="8">Chicken Snickers</option>' +
                 '</select>');
      c.remove({});
      Deps.flush();
      test.equal(furtherCanon(this.div.html()),
                 '<select id="morphing" name="fred">' +
                 '<!---->' +
                 '</select>');
      c.insert({name: 'Hamburger', value: 1});
      c.insert({name: 'Cheeseburger', value: 2});
      Deps.flush();
      test.equal(furtherCanon(this.div.html()),
                 '<select id="morphing" name="fred">' +
                 '<option value="1">Hamburger</option>' +
                 '<option value="2">Cheeseburger</option>' +
                 '</select>');
    });
  
  });
  
  
  Tinytest.add("spark - basic isolate", function (test) {
  
    var R = ReactiveVar('foo');
  
    var div = OnscreenDiv(Spark.render(function () {
      return '<div>' + Spark.isolate(function () {
        return '<span>' + R.get() + '</span>';
      }) + '</div>';
    }));
  
    test.equal(div.html(), '<div><span>foo</span></div>');
    R.set('bar');
    test.equal(div.html(), '<div><span>foo</span></div>');
    Deps.flush();
    test.equal(div.html(), '<div><span>bar</span></div>');
    R.set('baz');
    Deps.flush();
    test.equal(div.html(), '<div><span>baz</span></div>');
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - one render", function (test) {
  
    var R = ReactiveVar("foo");
  
    var frag = WrappedFrag(Meteor.render(function () {
      return R.get();
    })).hold();
  
    test.equal(R.numListeners(), 1);
  
    // frag should be "foo" initially
    test.equal(frag.html(), "foo");
    R.set("bar");
    // haven't flushed yet, so update won't have happened
    test.equal(frag.html(), "foo");
    Deps.flush();
    // flushed now, frag should say "bar"
    test.equal(frag.html(), "bar");
    frag.release(); // frag is now considered offscreen
    Deps.flush();
    R.set("baz");
    Deps.flush();
    // no update should have happened, offscreen range dep killed
    test.equal(frag.html(), "bar");
  
    // should be back to no listeners
    test.equal(R.numListeners(), 0);
  
    // empty return value should work, and show up as a comment
    frag = WrappedFrag(Meteor.render(function () {
      return "";
    }));
    test.equal(frag.html(), "<!---->");
  
    // nodes coming and going at top level of fragment
    R.set(true);
    frag = WrappedFrag(Meteor.render(function () {
      return R.get() ? "<div>hello</div><div>world</div>" : "";
    })).hold();
    test.equal(frag.html(), "<div>hello</div><div>world</div>");
    R.set(false);
    Deps.flush();
    test.equal(frag.html(), "<!---->");
    R.set(true);
    Deps.flush();
    test.equal(frag.html(), "<div>hello</div><div>world</div>");
    test.equal(R.numListeners(), 1);
    frag.release();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    // more complicated changes
    R.set(1);
    frag = WrappedFrag(Meteor.render(function () {
      var result = [];
      for(var i=0; i<R.get(); i++) {
        result.push('<div id="x'+i+'" class="foo" name="bar"><p><b>'+
                    R.get()+'</b></p></div>');
      }
      return result.join('');
    })).hold();
    test.equal(frag.html(),
                 '<div class="foo" id="x0" name="bar"><p><b>1</b></p></div>');
    R.set(3);
    Deps.flush();
    test.equal(frag.html(),
                 '<div class="foo" id="x0" name="bar"><p><b>3</b></p></div>'+
                 '<div class="foo" id="x1" name="bar"><p><b>3</b></p></div>'+
                 '<div class="foo" id="x2" name="bar"><p><b>3</b></p></div>');
    R.set(2);
    Deps.flush();
    test.equal(frag.html(),
                 '<div class="foo" id="x0" name="bar"><p><b>2</b></p></div>'+
                 '<div class="foo" id="x1" name="bar"><p><b>2</b></p></div>');
    frag.release();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    // caller violating preconditions
    test.equal(WrappedFrag(Meteor.render("foo")).html(), "foo");
  });
  
  Tinytest.add("spark - heuristic finalize", function (test) {
  
    var R = ReactiveVar(123);
  
    var div = OnscreenDiv(Meteor.render(function () {
      return "<p>The number is "+R.get()+".</p><hr><br><br><u>underlined</u>";
    }));
  
    test.equal(div.html(), "<p>The number is 123.</p><hr><br><br><u>underlined</u>");
    test.equal(R.numListeners(), 1);
    Deps.flush();
    R.set(456); // won't take effect until flush()
    test.equal(div.html(), "<p>The number is 123.</p><hr><br><br><u>underlined</u>");
    test.equal(R.numListeners(), 0); // listener already gone
    Deps.flush();
    test.equal(div.html(), "<p>The number is 456.</p><hr><br><br><u>underlined</u>");
    test.equal(R.numListeners(), 1);
  
    div.remove();
    R.set(789); // update should force div dependency to be GCed when div is updated
    Deps.flush();
    test.equal(R.numListeners(), 0);
  });
  
  Tinytest.add("spark - isolate", function (test) {
  
    var inc = function (v) {
      v.set(v.get() + 1); };
  
    var R1 = ReactiveVar(0);
    var R2 = ReactiveVar(0);
    var R3 = ReactiveVar(0);
    var count1 = 0, count2 = 0, count3 = 0;
  
    var frag = WrappedFrag(Meteor.render(function () {
      return R1.get() + "," + (count1++) + " " +
        Spark.isolate(function () {
          return R2.get() + "," + (count2++) + " " +
            Spark.isolate(function () {
              return R3.get() + "," + (count3++);
            });
        });
    })).hold();
  
    test.equal(frag.html(), "0,0 0,0 0,0");
  
    inc(R1); Deps.flush();
    test.equal(frag.html(), "1,1 0,1 0,1");
  
    inc(R2); Deps.flush();
    test.equal(frag.html(), "1,1 1,2 0,2");
  
    inc(R3); Deps.flush();
    test.equal(frag.html(), "1,1 1,2 1,3");
  
    inc(R2); Deps.flush();
    test.equal(frag.html(), "1,1 2,3 1,4");
  
    inc(R1); Deps.flush();
    test.equal(frag.html(), "2,2 2,4 1,5");
  
    frag.release();
    Deps.flush();
    test.equal(R1.numListeners(), 0);
    test.equal(R2.numListeners(), 0);
    test.equal(R3.numListeners(), 0);
  
    R1.set(0);
    R2.set(0);
    R3.set(0);
  
    frag = WrappedFrag(Meteor.render(function () {
      var buf = [];
      buf.push('<div class="foo', R1.get(), '">');
      buf.push(Spark.isolate(function () {
        var buf = [];
        for(var i=0; i<R2.get(); i++) {
          buf.push(Spark.isolate(function () {
            return '<div>'+R3.get()+'</div>';
          }));
        }
        return buf.join('');
      }));
      buf.push('</div>');
      return buf.join('');
    })).hold();
  
    test.equal(frag.html(), '<div class="foo0"><!----></div>');
    R2.set(3); Deps.flush();
    test.equal(frag.html(), '<div class="foo0">'+
                 '<div>0</div><div>0</div><div>0</div>'+
                 '</div>');
  
    R3.set(5); Deps.flush();
    test.equal(frag.html(), '<div class="foo0">'+
                 '<div>5</div><div>5</div><div>5</div>'+
                 '</div>');
  
    R1.set(7); Deps.flush();
    test.equal(frag.html(), '<div class="foo7">'+
                 '<div>5</div><div>5</div><div>5</div>'+
                 '</div>');
  
    R2.set(1); Deps.flush();
    test.equal(frag.html(), '<div class="foo7">'+
                 '<div>5</div>'+
                 '</div>');
  
    R1.set(11); Deps.flush();
    test.equal(frag.html(), '<div class="foo11">'+
                 '<div>5</div>'+
                 '</div>');
  
    R2.set(2); Deps.flush();
    test.equal(frag.html(), '<div class="foo11">'+
                 '<div>5</div><div>5</div>'+
                 '</div>');
  
    R3.set(4); Deps.flush();
    test.equal(frag.html(), '<div class="foo11">'+
                 '<div>4</div><div>4</div>'+
                 '</div>');
  
    frag.release();
  
    // calling isolate() outside of render mode
    test.equal(Spark.isolate(function () { return "foo"; }), "foo");
  
    // caller violating preconditions
  
    test.throws(function () {
      Meteor.render(function () {
        return Spark.isolate("foo");
      });
    });
  
  
    // unused isolate
  
    var Q = ReactiveVar("foo");
    Meteor.render(function () {
      // create an isolate, in render mode,
      // but don't use it.
      Spark.isolate(function () {
        return Q.get();
      });
      return "";
    });
    Q.set("bar");
    // might get an error on flush() if implementation
    // deals poorly with unused isolates, or a listener
    // still existing after flush.
    Deps.flush();
    test.equal(Q.numListeners(), 0);
  
    // nesting
  
    var stuff = ReactiveVar(true);
    var div = OnscreenDiv(Meteor.render(function () {
      return Spark.isolate(function () {
        return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function () {
          return "hi";
        });
      });
    }));
    test.equal(div.html(), "xyhi");
    stuff.set(false);
    Deps.flush();
    test.equal(div.html(), "xhi");
    div.kill();
    Deps.flush();
  
    // more nesting
  
    var num1 = ReactiveVar(false);
    var num2 = ReactiveVar(false);
    var num3 = ReactiveVar(false);
    var numset = function (n) {
      _.each([num1, num2, num3], function (v, i) {
        v.set((i+1) === n);
      });
    };
    numset(1);
  
    var div = OnscreenDiv(Meteor.render(function () {
      return Spark.isolate(function () {
        return (num1.get() ? '1' : '')+
          Spark.isolate(function () {
            return (num2.get() ? '2' : '')+
              Spark.isolate(function () {
                return (num3.get() ? '3' : '')+'x';
              });
          });
      });
    }));
    test.equal(div.html(), "1x");
    numset(2);
    Deps.flush();
    test.equal(div.html(), "2x");
    numset(3);
    Deps.flush();
    test.equal(div.html(), "3x");
    numset(1);
    Deps.flush();
    test.equal(div.html(), "1x");
    numset(3);
    Deps.flush();
    test.equal(div.html(), "3x");
    numset(2);
    Deps.flush();
    test.equal(div.html(), "2x");
    div.remove();
    Deps.flush();
  
    // the real test for slow-path GC finalization:
    num2.set(! num2.get());
    Deps.flush();
    test.equal(num1.numListeners(), 0);
    test.equal(num2.numListeners(), 0);
    test.equal(num3.numListeners(), 0);
  });
  
  Tinytest.add("spark - data context", function (test) {
    var d1 = {x: 1};
    var d2 = {x: 2};
    var d3 = {x: 3};
    var d4 = {x: 4};
    var d5 = {x: 5};
  
    var traverse = function (frag) {
      var out = '';
      var walkChildren = function (parent) {
        for (var node = parent.firstChild; node; node = node.nextSibling) {
          if (node.nodeType !== 8 /* COMMENT */)  {
            var data = Spark.getDataContext(node);
            out += (data === null) ? "_" : data.x;
          }
          if (node.nodeType === 1 /* ELEMENT */)
            walkChildren(node);
        }
      };
      walkChildren(frag);
      return out;
    };
  
    var testData = function (serialized, htmlFunc) {
      test.equal(traverse(Spark.render(htmlFunc)), serialized);
    };
  
    testData("_", function () {
      return "hi";
    });
  
    testData("__", function () {
      return "<div>hi</div>";
    });
  
    testData("_1", function () {
      return "<div>" + Spark.setDataContext(d1, "hi") + "</div>";
    });
  
    testData("21", function () {
      return Spark.setDataContext(
        d2, "<div>" + Spark.setDataContext(d1, "hi") + "</div>");
    });
  
    testData("21", function () {
      return Spark.setDataContext(
        d2, "<div>" +
          Spark.setDataContext(d3,
                               Spark.setDataContext(d1, "hi")) +
          "</div>");
    });
  
    testData("23", function () {
      return Spark.setDataContext(
        d2, "<div>" +
          Spark.setDataContext(d1,
                               Spark.setDataContext(d3, "hi")) +
          "</div>");
    });
  
    testData("23", function () {
      var html = Spark.setDataContext(
        d2, "<div>" +
          Spark.setDataContext(d1,
                               Spark.setDataContext(d3, "hi")) +
          "</div>");
      return Spark.setDataContext(d4, html);
    });
  
    testData("1_2", function () {
      return Spark.setDataContext(d1, "hi") + "-" +
        Spark.setDataContext(d2, "there");
    });
  
    testData("_122_3__45", function () {
      return "<div>" +
        Spark.setDataContext(d1, "<div></div>") +
        Spark.setDataContext(d2, "<div><div></div></div>") +
        "<div></div>" +
        Spark.setDataContext(d3, "<div></div>") +
        "<div><div></div></div>" +
        Spark.setDataContext(d4, "<div>" +
                             Spark.setDataContext(d5, "<div></div>") +
                             "</div>");
    });
  });
  
  Tinytest.add("spark - tables", function (test) {
    var R = ReactiveVar(0);
  
    var table = OnscreenDiv(Meteor.render(function () {
      var buf = [];
      buf.push("<table>");
      for(var i=0; i<R.get(); i++)
        buf.push("<tr><td>"+(i+1)+"</td></tr>");
      buf.push("</table>");
      return buf.join('');
    }));
  
    R.set(1);
    Deps.flush();
    test.equal(table.html(), "<table><tbody><tr><td>1</td></tr></tbody></table>");
  
    R.set(10);
    test.equal(table.html(), "<table><tbody><tr><td>1</td></tr></tbody></table>");
    Deps.flush();
    test.equal(table.html(), "<table><tbody>"+
                 "<tr><td>1</td></tr>"+
                 "<tr><td>2</td></tr>"+
                 "<tr><td>3</td></tr>"+
                 "<tr><td>4</td></tr>"+
                 "<tr><td>5</td></tr>"+
                 "<tr><td>6</td></tr>"+
                 "<tr><td>7</td></tr>"+
                 "<tr><td>8</td></tr>"+
                 "<tr><td>9</td></tr>"+
                 "<tr><td>10</td></tr>"+
                 "</tbody></table>");
  
    R.set(0);
    Deps.flush();
    test.equal(table.html(), "<table></table>");
    table.kill();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    var div = OnscreenDiv();
    div.node().appendChild(document.createElement("TABLE"));
    div.node().firstChild.appendChild(Meteor.render(function () {
      var buf = [];
      for(var i=0; i<R.get(); i++)
        buf.push("<tr><td>"+(i+1)+"</td></tr>");
      return buf.join('');
    }));
    test.equal(div.html(), "<table><!----></table>");
    R.set(3);
    Deps.flush();
    test.equal(div.html(), "<table><tbody>"+
                 "<tr><td>1</td></tr>"+
                 "<tr><td>2</td></tr>"+
                 "<tr><td>3</td></tr>"+
                 "</tbody></table>");
    test.equal(div.node().firstChild.rows.length, 3);
    R.set(0);
    Deps.flush();
    test.equal(div.html(), "<table><!----></table>");
    div.kill();
    Deps.flush();
  
    test.equal(R.numListeners(), 0);
  
    div = OnscreenDiv();
    div.node().appendChild(DomUtils.htmlToFragment("<table><tr></tr></table>"));
    R.set(3);
    div.node().getElementsByTagName("tr")[0].appendChild(Meteor.render(
      function () {
        var buf = [];
        for(var i=0; i<R.get(); i++)
          buf.push("<td>"+(i+1)+"</td>");
        return buf.join('');
      }));
    test.equal(div.html(),
                 "<table><tbody><tr><td>1</td><td>2</td><td>3</td>"+
                 "</tr></tbody></table>");
    R.set(1);
    Deps.flush();
    test.equal(div.html(),
                 "<table><tbody><tr><td>1</td></tr></tbody></table>");
    div.kill();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    div = OnscreenDiv(renderWithPreservation(function() {
      return '<table id="my-awesome-table">'+R.get()+'</table>';
    }));
    Deps.flush();
    R.set("<tr><td>Hello</td></tr>");
    Deps.flush();
    test.equal(
      div.html(),
      '<table id="my-awesome-table"><tbody><tr><td>Hello</td></tr></tbody></table>');
    div.kill();
    Deps.flush();
  
    test.equal(R.numListeners(), 0);
  });
  
  Tinytest.add("spark - event handling", function (test) {
    var event_buf = [];
    var getid = function (id) {
      return document.getElementById(id);
    };
  
    var div;
  
    var chunk = function (htmlFunc, options) {
      var html = Spark.isolate(htmlFunc);
      options = options || {};
      if (options.events)
        html = Spark.attachEvents(options.events, html);
      if (options.event_data)
        html = Spark.setDataContext(options.event_data, html);
      return html;
    };
  
    var render = function (htmlFunc, options) {
      return Spark.render(function () {
        return chunk(htmlFunc, options);
      });
    };
  
  
    // clicking on a div at top level
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return '<div id="foozy">Foo</div>';
    }, {events: eventmap("click"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click']);
    div.kill();
    Deps.flush();
  
    // selector that specifies a top-level div
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return '<div id="foozy">Foo</div>';
    }, {events: eventmap("click div"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click div']);
    div.kill();
    Deps.flush();
  
    // selector that specifies a second-level span
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return '<div id="foozy"><span>Foo</span></div>';
    }, {events: eventmap("click span"), event_data:event_buf}));
    clickElement(getid("foozy").firstChild);
    test.equal(event_buf, ['click span']);
    div.kill();
    Deps.flush();
  
    // replaced top-level elements still have event handlers
    // even if replaced by an isolate above the handlers in the DOM
    var R = ReactiveVar("p");
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return chunk(function () {
        return '<'+R.get()+' id="foozy">Hello</'+R.get()+'>';
      });
    }, {events: eventmap("click"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click']);
    event_buf.length = 0;
    R.set("div"); // change tag, which is sure to replace element
    Deps.flush();
    clickElement(getid("foozy")); // still clickable?
    test.equal(event_buf, ['click']);
    event_buf.length = 0;
    R.set("p");
    Deps.flush();
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // bubbling from event on descendent of element matched
    // by selector
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
        '<span>Bar</span></div>';
    }, {events: eventmap("click span"), event_data:event_buf}));
    clickElement(
      getid("foozy").firstChild.firstChild.firstChild);
    test.equal(event_buf, ['click span']);
    div.kill();
    Deps.flush();
  
    // bubbling order (for same event, same render node, different selector nodes)
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
        '<span>Bar</span></div>';
    }, {events: eventmap("click span", "click b"), event_data:event_buf}));
    clickElement(
      getid("foozy").firstChild.firstChild.firstChild);
    test.equal(event_buf, ['click b', 'click span']);
    div.kill();
    Deps.flush();
  
    // "bubbling" order for handlers at same level
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return chunk(function () {
        return chunk(function () {
          return '<span id="foozy" class="a b c">Hello</span>';
        }, {events: eventmap("click .c"), event_data:event_buf});
      }, {events: eventmap("click .b"), event_data:event_buf});
    }, {events: eventmap("click .a"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // stopPropagation doesn't prevent other event maps from
    // handling same node
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return chunk(function () {
        return chunk(function () {
          return '<span id="foozy" class="a b c">Hello</span>';
        }, {events: eventmap("click .c"), event_data:event_buf});
      }, {events: {"click .b": function (evt) {
        event_buf.push("click .b"); evt.stopPropagation();}}});
    }, {events: eventmap("click .a"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // stopImmediatePropagation DOES
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return chunk(function () {
        return chunk(function () {
          return '<span id="foozy" class="a b c">Hello</span>';
        }, {events: eventmap("click .c"), event_data:event_buf});
      }, {events: {"click .b": function (evt) {
        event_buf.push("click .b");
        evt.stopImmediatePropagation();}}});
    }, {events: eventmap("click .a"), event_data:event_buf}));
    clickElement(getid("foozy"));
    test.equal(event_buf, ['click .c', 'click .b']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // bubbling continues even with DOM change
    event_buf.length = 0;
    R = ReactiveVar(true);
    div = OnscreenDiv(render(function () {
      return chunk(function () {
        return '<div id="blarn">'+(R.get()?'<span id="foozy">abcd</span>':'')+'</div>';
      }, {events: { 'click span': function () {
        event_buf.push('click span');
        R.set(false);
        Deps.flush(); // kill the span
      }, 'click div': function (evt) {
        event_buf.push('click div');
      }}});
    }));
    // click on span
    clickElement(getid("foozy"));
    test.expect_fail(); // doesn't seem to work in old IE
    test.equal(event_buf, ['click span', 'click div']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // "deep reach" from high node down to replaced low node.
    // Tests that events are registered correctly to work in
    // old IE.  Also tests change event bubbling
    // and proper interpretation of event maps.
    event_buf.length = 0;
    R = ReactiveVar('foo');
    div = OnscreenDiv(render(function () {
      return '<div><p><span><b>'+
        chunk(function () {
          return '<input type="checkbox">'+R.get();
        }, {events: eventmap('click input'), event_data:event_buf}) +
        '</b></span></p></div>';
    }, { events: eventmap('change b', 'change input'), event_data:event_buf }));
    R.set('bar');
    Deps.flush();
    // click on input
    clickElement(div.node().getElementsByTagName('input')[0]);
    event_buf.sort(); // don't care about order
    test.equal(event_buf, ['change b', 'change input', 'click input']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // test that 'click *' fires on bubble
    event_buf.length = 0;
    R = ReactiveVar('foo');
    div = OnscreenDiv(render(function () {
      return '<div><p><span><b>'+
        chunk(function () {
          return '<input type="checkbox">'+R.get();
        }, {events: eventmap('click input'), event_data:event_buf}) +
        '</b></span></p></div>';
    }, { events: eventmap('click *'), event_data:event_buf }));
    R.set('bar');
    Deps.flush();
    // click on input
    clickElement(div.node().getElementsByTagName('input')[0]);
    test.equal(
      event_buf,
      ['click input', 'click *', 'click *', 'click *', 'click *', 'click *']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // clicking on a div in a nested chunk (without patching)
    event_buf.length = 0;
    R = ReactiveVar('foo');
    div = OnscreenDiv(render(function () {
      return R.get() + chunk(function () {
        return '<span>ism</span>';
      }, {events: eventmap("click"), event_data:event_buf});
    }));
    test.equal(div.text(), 'fooism');
    clickElement(div.node().getElementsByTagName('SPAN')[0]);
    test.equal(event_buf, ['click']);
    event_buf.length = 0;
    R.set('bar');
    Deps.flush();
    test.equal(div.text(), 'barism');
    clickElement(div.node().getElementsByTagName('SPAN')[0]);
    test.equal(event_buf, ['click']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // Test that reactive fragments manually inserted inside
    // a reactive fragment eventually get wired.
    event_buf.length = 0;
    div = OnscreenDiv(render(function () {
      return "<div></div>";
    }, { events: eventmap("click span", event_buf) }));
    Deps.flush();
    div.node().firstChild.appendChild(render(function () {
      return '<span id="foozy">hello</span>';
    }));
    clickElement(getid("foozy"));
    // implementation has no way to know we've inserted the fragment
    test.equal(event_buf, []);
    event_buf.length = 0;
    Deps.flush();
    clickElement(getid("foozy"));
    // now should be wired up
    test.equal(event_buf, ['click span']);
    event_buf.length = 0;
    div.kill();
    Deps.flush();
  
    // Event data comes from event.currentTarget, not event.target
    var data_buf = [];
    div = OnscreenDiv(render(function () {
      return "<ul>"+chunk(function () {
        return '<li id="funyard">Hello</li>';
      }, { event_data: {x:'listuff'} })+"</ul>";
    }, { event_data: {x:'ulstuff'},
         events: { 'click ul': function () { data_buf.push(this); }}}));
    clickElement(getid("funyard"));
    test.equal(data_buf, [{x:'ulstuff'}]);
    div.kill();
    Deps.flush();
  });
  
  
  Tinytest.add("spark - list event handling", function(test) {
    var event_buf = [];
    var div;
  
    // same thing, but with events wired by listChunk "added" and "removed"
    event_buf.length = 0;
    var lst = [];
    lst.observeChanges = function(callbacks) {
      lst.callbacks = callbacks;
      return {
        stop: function() {
          lst.callbacks = null;
        }
      };
    };
    div = OnscreenDiv(Meteor.render(function() {
      var chkbx = function(doc) {
        return '<input type="checkbox">'+(doc ? doc._id : 'else');
      };
      var html = '<div><p><span><b>' +
        Spark.setDataContext(
          event_buf, Spark.attachEvents(
            eventmap('click input', event_buf), Spark.list(lst, chkbx, chkbx))) +
        '</b></span></p></div>';
      html = Spark.setDataContext(event_buf, html);
      html = Spark.attachEvents(eventmap('change b', 'change input', event_buf),
                                html);
      return html;
    }));
    Deps.flush();
    test.equal(div.text().match(/\S+/)[0], 'else');
    // click on input
    var doClick = function() {
      clickElement(div.node().getElementsByTagName('input')[0]);
      event_buf.sort(); // don't care about order
      test.equal(event_buf, ['change b', 'change input', 'click input']);
      event_buf.length = 0;
    };
    doClick();
    // add item
    lst.push({_id:'foo'});
    lst.callbacks.addedBefore(lst[0]._id, lst[0], null);
    Deps.flush();
    test.equal(div.text().match(/\S+/)[0], 'foo');
    doClick();
    // remove item, back to "else" case
    lst.callbacks.removed(lst[0]._id);
    lst.pop();
    Deps.flush();
    test.equal(div.text().match(/\S+/)[0], 'else');
    doClick();
    // cleanup
    div.kill();
    Deps.flush();
  
  });
  
  
  Tinytest.add("spark - basic landmarks", function (test) {
    var R = ReactiveVar("111");
    var x = [];
    var expect = function (what) {
      test.equal(x, what);
      x = [];
    };
  
    var X = {};
  
    var div = OnscreenDiv(Spark.render(function () {
      return Spark.isolate(function () {
        return R.get() +
          Spark.createLandmark({
            created: function () {
              x.push("c");
              this.a = X;
            },
            rendered: function () {
              x.push("r", this.a);
            },
            destroyed: function () {
              x.push("d", this.a);
            }
          }, function() { return "hi"; });
      });
    }));
  
    expect(["c"]);
    Deps.flush();
    expect(["r", X]);
    Deps.flush();
    expect([]);
    R.set("222");
    expect([]);
    Deps.flush();
    expect(["r", X]);
    Deps.flush();
    expect([]);
    div.remove();
    expect([]);
    Deps.flush();
    expect([]);
    div.kill();
    Deps.flush();
    expect(["d", X]);
  });
  
  Tinytest.add("spark - labeled landmarks", function (test) {
    var R = [];
    for (var i = 0; i < 10; i++)
      R.push(ReactiveVar(""));
  
    var x = [];
    var s = [];
    var expect = function (what_x, what_s) {
      test.equal(x, what_x);
      test.equal(s, what_s);
      x = [];
      s = [];
    };
  
    var excludeLandmarks = [];
    for (var i = 0; i < 6; i++)
      excludeLandmarks.push(ReactiveVar(false));
  
    var isolateLandmarks = ReactiveVar(false);
    var serial = 1;
    var testLandmark = function (id, htmlFunc) {
      if (excludeLandmarks[id].get())
        return "";
  
      var f = function () {
        var thisSerial = serial++;
  
        return Spark.createLandmark({
          created: function () {
            x.push("c", id);
            s.push(thisSerial);
            this.id = id;
          },
          rendered: function () {
            x.push("r", id);
            s.push(thisSerial);
            test.equal(this.id, id);
          },
          destroyed: function () {
            x.push("d", id);
            s.push(thisSerial);
            test.equal(this.id, id);
          }
        }, htmlFunc);
      };
  
      if (isolateLandmarks.get())
        return Spark.isolate(function () { return f(); });
      else
        return f();
    };
  
    var label = Spark.labelBranch;
  
    var dep = function (i) {
      return R[i].get();
    };
  
    // this frog is pretty well boiled
    var div = OnscreenDiv(Spark.render(function () {
      var html = Spark.isolate(function () {
        return (
          dep(0) +
            testLandmark(1, function () {return "hi" + dep(1); }) +
            label("a", function () {
              return dep(2) +
                testLandmark(2, function () { return "hi" + dep(3);});}) +
            label("b", function () {
              return dep(4) +
                testLandmark(3, function () {
                  return "hi" + dep(5) +                                                                     label("c", function () {
                    return dep(6) +
                      testLandmark(4, function () {
                        return "hi" + dep(7) +
                          label("d", function () {
                            return label("e", function () {
                              return dep(8) +
                                label("f", function () {
                                  return testLandmark(
                                    5, function () { return "hi" + dep(9);}
                                  );});});});});});});}));
      });
      return html;
    }));
  
    // callback order is not specced
    expect(["c", 1, "c", 2, "c", 3, "c", 4, "c", 5], [1, 2, 3, 4, 5]);
    Deps.flush();
    expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [1, 2, 5, 4, 3]);
    for (var i = 0; i < 10; i++) {
      R[i].set(1);
      expect([], []);
      Deps.flush();
      expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3],
             [i*5 + 6, i*5 + 7, i*5 + 10, i*5 + 9, i*5 + 8]);
    };
  
    excludeLandmarks[2].set(true);
    expect([], []);
    Deps.flush();
    expect(["d", 2, "r", 1, "r", 5, "r", 4, "r", 3],
           [52, 56, 59, 58, 57]);
  
    excludeLandmarks[2].set(false);
    excludeLandmarks[3].set(true);
    expect([], []);
    Deps.flush();
    expect(["c", 2, "d", 3, "d", 4, "d", 5, "r", 1, "r", 2],
           [61, 57, 58, 59, 60, 61]);
  
    excludeLandmarks[2].set(true);
    excludeLandmarks[3].set(false);
    expect([], []);
    Deps.flush();
    expect(["c", 3, "c", 4, "c", 5, "d", 2, "r", 1, "r", 5, "r", 4, "r", 3],
           [63, 64, 65, 61, 62, 65, 64, 63]);
  
    excludeLandmarks[2].set(false);
    expect([], []);
    Deps.flush();
    expect(["c", 2, "r", 1, "r", 2, "r", 5, "r", 4, "r", 3],
           [67, 66, 67, 70, 69, 68]);
  
    isolateLandmarks.set(true);
    expect([], []);
    Deps.flush();
    expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3],
           [71, 72, 75, 74, 73]);
  
    for (var i = 0; i < 10; i++) {
      var expected = [
        [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [76, 77, 80, 79, 78]],
        [["r", 1], [81]],
        [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [82, 83, 86, 85, 84]],
        [["r", 2], [87]],
        [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [88, 89, 92, 91, 90]],
        [["r", 5, "r", 4, "r", 3], [95, 94, 93]],
        [["r", 5, "r", 4, "r", 3], [98, 97, 96]],
        [["r", 5, "r", 4, "r", 3], [100, 99, 96]],
        [["r", 5, "r", 4, "r", 3], [102, 101, 96]],
        [["r", 5, "r", 4, "r", 3], [103, 101, 96]]
      ][i];
      R[i].set(2);
      expect([], []);
      Deps.flush();
      expect.apply(null, expected);
    };
  
    excludeLandmarks[4].set(true);
    Deps.flush();
    expect(["d", 4, "d", 5, "r", 3], [101, 103, 104]);
  
    excludeLandmarks[4].set(false);
    excludeLandmarks[5].set(true);
    Deps.flush();
    expect(["c", 4, "r", 4, "r", 3], [106, 106, 105]);
  
    excludeLandmarks[5].set(false);
    Deps.flush();
    expect(["c", 5, "r", 5, "r", 4, "r", 3], [108, 108, 107, 105]);
  
    div.kill();
    Deps.flush();
  });
  
  
  Tinytest.add("spark - preserve copies attributes", function(test) {
    // make sure attributes are correctly changed (i.e. copied)
    // when preserving old nodes, either because they are labeled
    // or because they are a parent of a labeled node.
  
    var R1 = ReactiveVar("foo");
    var R2 = ReactiveVar("abcd");
  
    var frag = WrappedFrag(renderWithPreservation(function() {
      return '<div puppy="'+R1.get()+'"><div><div><div><input name="blah" kittycat="'+
        R2.get()+'"></div></div></div></div>';
    })).hold();
    var node1 = frag.node().firstChild;
    var node2 = frag.node().firstChild.getElementsByTagName("input")[0];
    test.equal(node1.nodeName, "DIV");
    test.equal(node2.nodeName, "INPUT");
    test.equal(node1.getAttribute("puppy"), "foo");
    test.equal(node2.getAttribute("kittycat"), "abcd");
  
    R1.set("bar");
    R2.set("efgh");
    Deps.flush();
    test.equal(node1.getAttribute("puppy"), "bar");
    test.equal(node2.getAttribute("kittycat"), "efgh");
  
    frag.release();
    Deps.flush();
    test.equal(R1.numListeners(), 0);
    test.equal(R2.numListeners(), 0);
  
    var R;
    R = ReactiveVar(false);
    frag = WrappedFrag(renderWithPreservation(function() {
      return '<input id="foo" type="checkbox"' + (R.get() ? ' checked="checked"' : '') + '>';
    })).hold();
    var get_checked = function() { return !! frag.node().firstChild.checked; };
    test.equal(get_checked(), false);
    Deps.flush();
    test.equal(get_checked(), false);
    R.set(true);
    test.equal(get_checked(), false);
    Deps.flush();
    test.equal(get_checked(), true);
    R.set(false);
    test.equal(get_checked(), true);
    Deps.flush();
    test.equal(get_checked(), false);
    R.set(true);
    Deps.flush();
    test.equal(get_checked(), true);
    frag.release();
    R = ReactiveVar(true);
    frag = WrappedFrag(renderWithPreservation(function() {
      return '<input type="checkbox"' + (R.get() ? ' checked="checked"' : '') + '>';
    })).hold();
    test.equal(get_checked(), true);
    Deps.flush();
    test.equal(get_checked(), true);
    R.set(false);
    test.equal(get_checked(), true);
    Deps.flush();
    test.equal(get_checked(), false);
    frag.release();
  
  
    _.each([false, true], function(with_focus) {
      R = ReactiveVar("apple");
      var div = OnscreenDiv(renderWithPreservation(function() {
        return '<input id="foo" type="text" value="' + R.get() + '">';
      }));
      var maybe_focus = function(div) {
        if (with_focus) {
          div.show();
          focusElement(div.node().firstChild);
        }
      };
      maybe_focus(div);
      var get_value = function() { return div.node().firstChild.value; };
      var set_value = function(v) { div.node().firstChild.value = v; };
      var if_blurred = function(v, v2) {
        return with_focus ? v2 : v; };
      test.equal(get_value(), "apple");
      Deps.flush();
      test.equal(get_value(), "apple");
      R.set("");
      test.equal(get_value(), "apple");
      Deps.flush();
      test.equal(get_value(), if_blurred("", "apple"));
      R.set("pear");
      test.equal(get_value(), if_blurred("", "apple"));
      Deps.flush();
      test.equal(get_value(), if_blurred("pear", "apple"));
      set_value("jerry"); // like user typing
      R.set("steve");
      Deps.flush();
      // should overwrite user typing if blurred
      test.equal(get_value(), if_blurred("steve", "jerry"));
      div.kill();
      R = ReactiveVar("");
      div = OnscreenDiv(renderWithPreservation(function() {
        return '<input id="foo" type="text" value="' + R.get() + '">';
      }));
      maybe_focus(div);
      test.equal(get_value(), "");
      Deps.flush();
      test.equal(get_value(), "");
      R.set("tom");
      test.equal(get_value(), "");
      Deps.flush();
      test.equal(get_value(), if_blurred("tom", ""));
      div.kill();
      Deps.flush();
    });
  });
  
  Tinytest.add("spark - bad labels", function(test) {
    // make sure patching behaves gracefully even when labels violate
    // the rules that would allow preservation of nodes identity.
  
    var go = function(html1, html2) {
      var R = ReactiveVar(true);
      var frag = WrappedFrag(renderWithPreservation(function() {
        return R.get() ? html1 : html2;
      })).hold();
  
      R.set(false);
      Deps.flush();
      test.equal(frag.html(), html2);
      frag.release();
    };
  
    go('hello', 'world');
  
    // duplicate IDs (bad developer; but should patch correctly)
    go('<div id="foo">hello</div><b id="foo">world</b>',
       '<div id="foo">hi</div><b id="foo">there</b>');
    go('<div id="foo"><b id="foo">hello</b></div>',
       '<div id="foo"><b id="foo">hi</b></div>');
    go('<div id="foo">hello</div><b id="foo">world</b>',
       '<div id="foo"><b id="foo">hi</b></div>');
  
    // tag name changes
    go('<div id="foo">abcd</div>',
       '<p id="foo">efgh</p>');
  
    // parent chain changes at all
    go('<div><div><div><p id="foo">test123</p></div></div></div>',
       '<div><div><p id="foo">test123</p></div></div>');
    go('<div><div><div><p id="foo">test123</p></div></div></div>',
       '<div><ins><div><p id="foo">test123</p></div></ins></div>');
  
    // ambiguous names
    go('<ul><li name="me">1</li><li name="me">3</li><li name="me">3</li></ul>',
       '<ul><li name="me">4</li><li name="me">5</li></ul>');
  });
  
  
  Tinytest.add("spark - landmark patching", function(test) {
  
    var rand;
  
    var randomNodeList = function(optParentTag, depth) {
      var atTopLevel = ! optParentTag;
      var len = rand.nextIntBetween(atTopLevel ? 1 : 0, 6);
      var buf = [];
      for(var i=0; i<len; i++)
        buf.push(randomNode(optParentTag, depth));
      return buf;
    };
  
    var randomNode = function(optParentTag, depth) {
      var n = {};
  
      if (rand.nextBoolean()) {
        // text node
        n.text = rand.nextIdentifier(2);
      } else {
  
        n.tagName = rand.nextChoice((function() {
          switch (optParentTag) {
          case "p": return ['b', 'i', 'u'];
          case "b": return ['i', 'u'];
          case "i": return ['u'];
          case "u": case "span": return ['span'];
          default: return ['div', 'ins', 'center', 'p'];
          }
        })());
  
        if (rand.nextBoolean())
          n.id = rand.nextIdentifier();
        if (rand.nextBoolean())
          n.name = rand.nextIdentifier();
  
        if (depth === 0) {
          n.children = [];
        } else {
          n.children = randomNodeList(n.tagName, depth-1);
        }
      }
  
      var existence = rand.nextChoice([[true, true], [false, true], [true, false]]);
      n.existsBefore = existence[0];
      n.existsAfter = existence[1];
  
      return n;
    };
  
    var nodeListToHtml = function(list, is_after, optBuf) {
      var buf = (optBuf || []);
      _.each(list, function(n) {
        if (is_after ? n.existsAfter : n.existsBefore) {
          if (n.text) {
            buf.push(n.text);
          } else {
            buf.push('<', n.tagName);
            if (n.id)
              buf.push(' id="', n.id, '"');
            if (n.name)
              buf.push(' name="', n.name, '"');
            buf.push('>');
            nodeListToHtml(n.children, is_after, buf);
            buf.push('</', n.tagName, '>');
          }
        }
      });
      return optBuf ? null : buf.join('');
    };
  
    var fillInElementIdentities = function(list, parent, is_after) {
      var elementsInList = _.filter(
        list,
        function(x) {
          return (is_after ? x.existsAfter : x.existsBefore) && x.tagName;
        });
      var elementsInDom = _.filter(parent.childNodes,
                                   function(x) { return x.nodeType === 1; });
      test.equal(elementsInList.length, elementsInDom.length);
      for(var i=0; i<elementsInList.length; i++) {
        elementsInList[i].node = elementsInDom[i];
        fillInElementIdentities(elementsInList[i].children,
                                elementsInDom[i]);
      }
    };
  
    var getParentChain = function(node) {
      var buf = [];
      while (node) {
        buf.push(node);
        node = node.parentNode;
      }
      return buf;
    };
  
    var isSameElements = function(a, b) {
      if (a.length !== b.length)
        return false;
      for(var i=0; i<a.length; i++) {
        if (a[i] !== b[i])
          return false;
      }
      return true;
    };
  
    var collectLabeledNodeData = function(list, optArray) {
      var buf = optArray || [];
  
      _.each(list, function(x) {
        if (x.tagName && x.existsBefore && x.existsAfter) {
          if (x.name || x.id) {
            buf.push({ node: x.node, parents: getParentChain(x.node) });
          }
          collectLabeledNodeData(x.children, buf);
        }
      });
  
      return buf;
    };
  
    for(var i=0; i<5; i++) {
      // Use non-deterministic randomness so we can have a shorter fuzz
      // test (fewer iterations).  For deterministic (fully seeded)
      // randomness, remove the call to Random.fraction().
      rand = new SeededRandom("preserved nodes "+i+" "+Random.fraction());
  
      var R = ReactiveVar(false);
      var structure = randomNodeList(null, 6);
      var frag = WrappedFrag(Meteor.render(function () {
        return Spark.createLandmark({ preserve: idNameLabels }, function () {
          return nodeListToHtml(structure, R.get());
        });
      })).hold();
      test.equal(frag.html(), nodeListToHtml(structure, false) || "<!---->");
      fillInElementIdentities(structure, frag.node());
      var labeledNodes = collectLabeledNodeData(structure);
      R.set(true);
      Deps.flush();
      test.equal(frag.html(), nodeListToHtml(structure, true) || "<!---->");
      _.each(labeledNodes, function(x) {
        test.isTrue(isSameElements(x.parents, getParentChain(x.node)));
      });
  
      frag.release();
      Deps.flush();
      test.equal(R.numListeners(), 0);
    }
  
  });
  
  Tinytest.add("spark - landmark constant", function(test) {
  
    var R, div;
  
    // top-level { constant: true }
  
    R = ReactiveVar(0);
    var states = [];
    div = OnscreenDiv(Meteor.render(function() {
      R.get(); // create dependency
      return Spark.createLandmark({
        constant: true,
        rendered: function() {
          states.push(this);
        }
      }, function() { return '<b/><i/><u/>'; });
    }));
  
    var nodes = nodesToArray(div.node().childNodes);
    test.equal(nodes.length, 3);
    Deps.flush();
    test.equal(states.length, 1);
    R.set(1);
    Deps.flush();
    test.equal(states.length, 1); // no render callback on constant
    var nodes2 = nodesToArray(div.node().childNodes);
    test.equal(nodes2.length, 3);
    test.isTrue(nodes[0] === nodes2[0]);
    test.isTrue(nodes[1] === nodes2[1]);
    test.isTrue(nodes[2] === nodes2[2]);
    div.kill();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    // non-top-level
  
    var i = 1;
    // run test with and without matching branch label
    _.each([false, true], function(matchLandmark) {
      // run test with node before or after, or neither or both
      _.each([false, true], function(nodeBefore) {
        _.each([false, true], function(nodeAfter) {
          var hasSpan = true;
          var isConstant = true;
  
          var crd = null; // [createCount, renderCount, destroyCount]
  
          R = ReactiveVar('foo');
          div = OnscreenDiv(Meteor.render(function() {
            R.get(); // create unconditional dependency
            var brnch = matchLandmark ? 'myBranch' : ('branch'+(++i));
            return (nodeBefore ? R.get() : '') +
              Spark.labelBranch(
                brnch, function () {
                  return Spark.createLandmark(
                    {
                      constant: isConstant,
                      created: function () {
                        this.crd = [0,0,0];
                        if (! crd)
                          crd = this.crd; // capture first landmark's crd
                        this.crd[0]++;
                      },
                      rendered: function () { this.crd[1]++; },
                      destroyed: function () { this.crd[2]++; }
                    },
                    function() { return hasSpan ?
                                 '<span>stuff</span>' : 'blah'; });}) +
              (nodeAfter ? R.get() : '');
          }));
  
          var span = div.node().getElementsByTagName('span')[0];
          hasSpan = false;
  
          test.equal(div.text(),
                     (nodeBefore ? 'foo' : '')+
                     'stuff'+
                     (nodeAfter ? 'foo' : ''));
  
          R.set('bar');
          Deps.flush();
  
          // only non-matching landmark should cause the constant
          // chunk to be re-rendered
          test.equal(div.text(),
                     (nodeBefore ? 'bar' : '')+
                     (matchLandmark ? 'stuff' : 'blah')+
                     (nodeAfter ? 'bar' : ''));
          // in non-matching case, first landmark is destroyed.
          // otherwise, it is kept (and not re-rendered because
          // it is constant)
          test.equal(crd, matchLandmark ? [1,1,0] : [1,1,1]);
  
          R.set('baz');
          Deps.flush();
  
          // should be repeatable (liveranges not damaged)
          test.equal(div.text(),
                     (nodeBefore ? 'baz' : '')+
                     (matchLandmark ? 'stuff' : 'blah')+
                     (nodeAfter ? 'baz' : ''));
  
          isConstant = false; // no longer constant:true!
          R.set('qux');
          Deps.flush();
          test.equal(div.text(),
                     (nodeBefore ? 'qux' : '')+
                     'blah'+
                     (nodeAfter ? 'qux' : ''));
  
          // turn constant back on
          isConstant = true;
          hasSpan = true;
          R.set('popsicle');
          Deps.flush();
          // we don't get the span, instead old "blah" is preserved
          test.equal(div.text(),
                     (nodeBefore ? 'popsicle' : '')+
                     (matchLandmark ? 'blah' : 'stuff')+
                     (nodeAfter ? 'popsicle' : ''));
  
          isConstant = false;
          R.set('hi');
          Deps.flush();
          // now we get the span!
          test.equal(div.text(),
                     (nodeBefore ? 'hi' : '')+
                     'stuff'+
                     (nodeAfter ? 'hi' : ''));
  
          div.kill();
          Deps.flush();
        });
      });
    });
  
    // test that constant landmark gets rendered callback if it
    // wasn't preserved.
  
    var renderCount;
  
    renderCount = 0;
    R = ReactiveVar('div');
    div = OnscreenDiv(Meteor.render(function () {
      return '<' + R.get() + '>' + Spark.createLandmark(
        {constant: true, rendered: function () { renderCount++; }},
        function () {
          return "hi";
        }) +
        '</' + R.get().split(' ')[0] + '>';
    }));
    Deps.flush();
    test.equal(renderCount, 1);
  
    R.set('div class="hamburger"');
    Deps.flush();
    // constant patched around, not re-rendered!
    test.equal(renderCount, 1);
  
    R.set('span class="hamburger"');
    Deps.flush();
    // can't patch parent to a different tag
    test.equal(renderCount, 2);
  
    R.set('span');
    Deps.flush();
    // can patch here, renderCount stays the same
    test.equal(renderCount, 2);
  
    div.kill();
    Deps.flush();
  });
  
  _.each(['STRING', 'MONGO'], function (idGeneration) {
  Tinytest.add("spark - leaderboard, " + idGeneration, function(test) {
    // use a simplified, local leaderboard to test some stuff
  
    var players = new LocalCollection();
    var selected_player = ReactiveVar();
  
    var scores = OnscreenDiv(renderWithPreservation(function() {
      var html = Spark.list(
        players.find({}, {sort: {score: -1}}),
        function(player) {
          return Spark.labelBranch(player._id.valueOf(), function () {
            return Spark.isolate(function () {
              var style;
              if (_.isEqual(selected_player.get(), player._id))
                style = "player selected";
              else
                style = "player";
  
              var html = '<div class="' + style + '">' +
                '<div class="name">' + player.name + '</div>' +
                '<div name="score">' + player.score + '</div></div>';
              html = Spark.setDataContext(player, html);
              html = Spark.createLandmark(
                {preserve: idNameLabels},
                function() { return html; });
              return html;
            });
          });
        });
      html = Spark.attachEvents({
        "click": function () {
          selected_player.set(this._id);
        }
      }, html);
      return html;
    }));
    var idGen;
    if (idGeneration === 'STRING')
      idGen = Random.id;
    else
      idGen = function () { return new LocalCollection._ObjectID(); };
  
    // back before we had scientists we had Vancian hussade players
    var names = ["Glinnes Hulden", "Shira Hulden", "Denzel Warhound",
                 "Lute Casagave", "Akadie", "Thammas, Lord Gensifer",
                 "Ervil Savat", "Duissane Trevanyi", "Sagmondo Bandolio",
                 "Rhyl Shermatz", "Yalden Wirp", "Tyran Lucho",
                 "Bump Candolf", "Wilmer Guff", "Carbo Gilweg"];
    for (var i = 0; i < names.length; i++)
      players.insert({_id: idGen(), name: names[i], score: i*5});
  
    var bump = function() {
      players.update(selected_player.get(), {$inc: {score: 5}});
    };
  
    var findPlayerNameDiv = function(name) {
      var divs = scores.node().getElementsByTagName('DIV');
      return _.find(divs, function(div) {
        return div.innerHTML === name;
      });
    };
  
    Deps.flush();
    var glinnesNameNode = findPlayerNameDiv(names[0]);
    test.isTrue(!! glinnesNameNode);
    var glinnesScoreNode = glinnesNameNode.nextSibling;
    test.equal(glinnesScoreNode.getAttribute("name"), "score");
    clickElement(glinnesNameNode);
    Deps.flush();
    glinnesNameNode = findPlayerNameDiv(names[0]);
    test.isTrue(!! glinnesNameNode);
    test.equal(glinnesNameNode.parentNode.className, 'player selected');
    var glinnesId = players.findOne({name: names[0]})._id;
    test.isTrue(!! glinnesId);
    test.equal(selected_player.get(), glinnesId);
    test.equal(
      canonicalizeHtml(glinnesNameNode.parentNode.innerHTML),
      '<div class="name">Glinnes Hulden</div><div name="score">0</div>');
  
    bump();
    Deps.flush();
  
    glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode);
    var glinnesScoreNode2 = glinnesNameNode.nextSibling;
    test.equal(glinnesScoreNode2.getAttribute("name"), "score");
    // move and patch should leave score node the same, because it
    // has a name attribute!
    test.equal(glinnesScoreNode, glinnesScoreNode2);
    test.equal(glinnesNameNode.parentNode.className, 'player selected');
    test.equal(
      canonicalizeHtml(glinnesNameNode.parentNode.innerHTML),
      '<div class="name">Glinnes Hulden</div><div name="score">5</div>');
  
    bump();
    Deps.flush();
  
    glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode);
    test.equal(
      canonicalizeHtml(glinnesNameNode.parentNode.innerHTML),
      '<div class="name">Glinnes Hulden</div><div name="score">10</div>');
  
    scores.kill();
    Deps.flush();
    test.equal(selected_player.numListeners(), 0);
  });
  });
  
  Tinytest.add("spark - list cursor stop", function(test) {
    // test Spark.list outside of render mode, on custom observable
  
    var numHandles = 0;
    var observable = {
      observeChanges: function(x) {
        x.addedBefore("123", {}, null);
        x.addedBefore("456", {}, null);
        var handle;
        numHandles++;
        return handle = {
          stop: function() {
            numHandles--;
          }
        };
      }
    };
  
    test.equal(numHandles, 0);
    var result = Spark.list(observable, function(doc) {
      return "#"+doc._id;
    });
    test.equal(result, "#123#456");
    Deps.flush();
    // chunk killed because not created inside Spark.render
    test.equal(numHandles, 0);
  
  
    var R = ReactiveVar(1);
    var frag = WrappedFrag(Meteor.render(function() {
      if (R.get() > 0)
        return Spark.list(observable, function() { return "*"; });
      return "";
    })).hold();
    test.equal(numHandles, 1);
    Deps.flush();
    test.equal(numHandles, 1);
    R.set(2);
    Deps.flush();
    test.equal(numHandles, 1);
    R.set(-1);
    Deps.flush();
    test.equal(numHandles, 0);
  
    frag.release();
    Deps.flush();
  });
  
  Tinytest.add("spark - list table", function(test) {
    var c = new LocalCollection();
  
    c.insert({value: "fudge", order: "A"});
    c.insert({value: "sundae", order: "B"});
  
    var R = ReactiveVar();
  
    var table = WrappedFrag(Meteor.render(function() {
      var buf = [];
      buf.push('<table>');
      buf.push(Spark.list(
        c.find({}, {sort: ['order']}),
        function(doc) {
          return Spark.labelBranch(doc._id, function () {
            return Spark.isolate(function () {
              var html = "<tr><td>"+doc.value + (doc.reactive ? R.get() : '')+
                "</td></tr>";
              html = Spark.createLandmark(
                {preserve: idNameLabels},
                function() { return html; });
              return html;
            });
          });
        },
        function() {
          return "<tr><td>(nothing)</td></tr>";
        }));
      buf.push('</table>');
      return buf.join('');
    })).hold();
  
    var lastHtml;
  
    var shouldFlushTo = function(html) {
      // same before flush
      test.equal(table.html(), lastHtml);
      Deps.flush();
      test.equal(table.html(), html);
      lastHtml = html;
    };
    var tableOf = function(/*htmls*/) {
      if (arguments.length === 0) {
        return '<table></table>';
      } else {
        return '<table><tbody><tr><td>' +
          _.toArray(arguments).join('</td></tr><tr><td>') +
          '</td></tr></tbody></table>';
      }
    };
  
    test.equal(table.html(), lastHtml = tableOf('fudge', 'sundae'));
  
    // switch order
    c.update({value: "fudge"}, {$set: {order: "BA"}});
    shouldFlushTo(tableOf('sundae', 'fudge'));
  
    // change text
    c.update({value: "fudge"}, {$set: {value: "hello"}});
    c.update({value: "sundae"}, {$set: {value: "world"}});
    shouldFlushTo(tableOf('world', 'hello'));
  
    // remove all
    c.remove({});
    shouldFlushTo(tableOf('(nothing)'));
  
    c.insert({value: "1", order: "A"});
    c.insert({value: "5", order: "B"});
    c.insert({value: "3", order: "AB"});
    c.insert({value: "7", order: "BB"});
    c.insert({value: "2", order: "AA"});
    c.insert({value: "4", order: "ABA"});
    c.insert({value: "6", order: "BA"});
    c.insert({value: "8", order: "BBA"});
    shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7', '8'));
  
    // make one item newly reactive
    R.set('*');
    c.update({value: "7"}, {$set: {reactive: true}});
    shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7*', '8'));
  
    R.set('!');
    shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7!', '8'));
  
    // move it
    c.update({value: "7"}, {$set: {order: "A0"}});
    shouldFlushTo(tableOf('1', '7!', '2', '3', '4', '5', '6', '8'));
  
    // still reactive?
    R.set('?');
    shouldFlushTo(tableOf('1', '7?', '2', '3', '4', '5', '6', '8'));
  
    // go nuts
    c.update({value: '1'}, {$set: {reactive: true}});
    c.update({value: '1'}, {$set: {reactive: false}});
    c.update({value: '2'}, {$set: {reactive: true}});
    c.update({value: '2'}, {$set: {order: "BBB"}});
    R.set(';');
    R.set('.');
    shouldFlushTo(tableOf('1', '7.', '3', '4', '5', '6', '8', '2.'));
  
    for(var i=1; i<=8; i++)
      c.update({value: String(i)},
               {$set: {reactive: true, value: '='+String(i)}});
    R.set('!');
    shouldFlushTo(tableOf('=1!', '=7!', '=3!', '=4!', '=5!', '=6!', '=8!', '=2!'));
  
    for(var i=1; i<=8; i++)
      c.update({value: '='+String(i)},
               {$set: {order: "A"+i}});
    shouldFlushTo(tableOf('=1!', '=2!', '=3!', '=4!', '=5!', '=6!', '=7!', '=8!'));
  
    var valueFunc = function(n) { return '<b name="bold">'+n+'</b>'; };
    for(var i=1; i<=8; i++)
      c.update({value: '='+String(i)},
               {$set: {value: valueFunc(i)}});
    shouldFlushTo(tableOf.apply(
      null,
      _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); })));
  
    test.equal(table.node().firstChild.rows.length, 8);
  
    var bolds = table.node().firstChild.getElementsByTagName('B');
    test.equal(bolds.length, 8);
    _.each(bolds, function(b) {
      b.nifty = {}; // mark the nodes; non-primitive value won't appear in IE HTML
    });
  
    R.set('...');
    shouldFlushTo(tableOf.apply(
      null,
      _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); })));
    var bolds2 = table.node().firstChild.getElementsByTagName('B');
    test.equal(bolds2.length, 8);
    // make sure patching is actually happening
    _.each(bolds2, function(b) {
      test.equal(!! b.nifty, true);
    });
  
    // change value func, and still we should be patching
    var valueFunc2 = function(n) { return '<b name="bold">'+n+'</b><i>yeah</i>'; };
    for(var i=1; i<=8; i++)
      c.update({value: valueFunc(i)},
               {$set: {value: valueFunc2(i)}});
    shouldFlushTo(tableOf.apply(
      null,
      _.map(_.range(1,9), function(n) { return valueFunc2(n)+R.get(); })));
    var bolds3 = table.node().firstChild.getElementsByTagName('B');
    test.equal(bolds3.length, 8);
    _.each(bolds3, function(b) {
      test.equal(!! b.nifty, true);
    });
  
    table.release();
  
  });
  
  
  Tinytest.add("spark - list event data", function(test) {
    // this is based on a bug
  
    var lastClicked = null;
    var R = ReactiveVar(0);
    var later;
    var div = OnscreenDiv(Meteor.render(function() {
      var html = Spark.list(
        {
          observeChanges: function(observer) {
            observer.addedBefore("1", {name: 'Foo'}, null);
            observer.addedBefore("2", {name: 'Bar'}, null);
            // exercise callback path
            later = function() {
              observer.addedBefore("3", {name: 'Baz'}, null);
              observer.addedBefore("4", {name: 'Qux'}, null);
            };
            return { stop: function() {} };
          }
        },
        function(doc) {
          var html = Spark.isolate(function () {
            R.get(); // depend on R
            return '<div>' + doc.name + '</div>';
          });
          html = Spark.setDataContext(doc, html);
          return html;
        }
      );
      html = Spark.attachEvents({
        'click': function (event) {
          lastClicked = this.name;
          R.set(R.get() + 1); // signal all dependers on R
        }
      }, html);
      return html;
    }));
  
    var item = function(name) {
      return _.find(div.node().getElementsByTagName('div'), function(d) {
        return d.innerHTML === name; });
    };
  
    later();
    Deps.flush();
    test.equal(item("Foo").innerHTML, "Foo");
    test.equal(item("Bar").innerHTML, "Bar");
    test.equal(item("Baz").innerHTML, "Baz");
    test.equal(item("Qux").innerHTML, "Qux");
  
    var doClick = function(name) {
      clickElement(item(name));
      test.equal(lastClicked, name);
      Deps.flush();
    };
  
    doClick("Foo");
    doClick("Bar");
    doClick("Baz");
    doClick("Qux");
    doClick("Bar");
    doClick("Foo");
    doClick("Foo");
    doClick("Foo");
    doClick("Qux");
    doClick("Baz");
    doClick("Baz");
    doClick("Baz");
    doClick("Bar");
    doClick("Baz");
    doClick("Foo");
    doClick("Qux");
    doClick("Foo");
  
    div.kill();
    Deps.flush();
  
  });
  
  
  Tinytest.add("spark - events on preserved nodes", function(test) {
    var count = ReactiveVar(0);
    var demo = OnscreenDiv(renderWithPreservation(function() {
      var html = Spark.isolate(function () {
        return '<div class="button_demo">'+
          '<input type="button" name="press" value="Press this button">'+
          '<div>The button has been pressed '+count.get()+' times.</div>'+
          '</div>';
      });
      html = Spark.attachEvents({
        'click input': function() {
          count.set(count.get() + 1);
        }
      }, html);
      return html;
    }));
  
    var click = function() {
      clickElement(demo.node().getElementsByTagName('input')[0]);
    };
  
    test.equal(count.get(), 0);
    for(var i=0; i<10; i++) {
      click();
      Deps.flush();
      test.equal(count.get(), i+1);
    }
  
    demo.kill();
    Deps.flush();
  });
  
  
  Tinytest.add("spark - cleanup", function(test) {
  
    // more exhaustive clean-up testing
    var stuff = new LocalCollection();
  
    var add_doc = function() {
      stuff.insert({foo:'bar'}); };
    var clear_docs = function() {
      stuff.remove({}); };
    var remove_one = function() {
      stuff.remove(stuff.findOne()._id); };
  
    add_doc(); // start the collection with a doc
  
    var R = ReactiveVar("x");
  
    var div = OnscreenDiv(Spark.render(function() {
      return Spark.list(
        stuff.find(),
        function() {
          return Spark.isolate(function () { return R.get()+"1"; });
        },
        function() {
          return Spark.isolate(function () { return R.get()+"0"; });
        });
    }));
  
    test.equal(div.text(), "x1");
    Deps.flush();
    test.equal(div.text(), "x1");
    test.equal(R.numListeners(), 1);
  
    clear_docs();
    Deps.flush();
    test.equal(div.text(), "x0");
    test.equal(R.numListeners(), 1); // test clean-up of doc on remove
  
    add_doc();
    Deps.flush();
    test.equal(div.text(), "x1");
    test.equal(R.numListeners(), 1); // test clean-up of "else" listeners
  
    add_doc();
    Deps.flush();
    test.equal(div.text(), "x1x1");
    test.equal(R.numListeners(), 2);
  
    remove_one();
    Deps.flush();
    test.equal(div.text(), "x1");
    test.equal(R.numListeners(), 1); // test clean-up of doc with other docs
  
    div.kill();
    Deps.flush();
    test.equal(R.numListeners(), 0);
  
    //// list stopped if not materialized
  
    var observeCount = 0;
    var stopCount = 0;
    var cursor = {
      observeChanges: function (callbacks) {
        observeCount++;
        return {
          stop: function () {
            stopCount++;
          }
        };
      }
    };
  
    div = OnscreenDiv(Spark.render(function () {
      var html = Spark.list(cursor,
                            function () { return ''; });
      // don't return html
      return 'hi';
    }));
    // we expect that the implementation of Spark.list observed the
    // cursor in order to generate HTML, and then stopped it when
    // it saw that the annotation wasn't materialized.  Other acceptable
    // implementations of Spark.list might avoid observing the cursor
    // altogether, resulting in [0, 0], or might defer the stopping to
    // flush time.
    test.equal([observeCount, stopCount], [1, 1]);
  
    div.kill();
    Deps.flush();
  });
  
  
  var make_input_tester = function(render_func, events) {
    var buf = [];
  
    if (typeof render_func === "string") {
      var render_str = render_func;
      render_func = function() { return render_str; };
    }
    if (typeof events === "string") {
      events = eventmap.apply(null, _.toArray(arguments).slice(1));
    }
  
    var R = ReactiveVar(0);
    var div = OnscreenDiv(
      renderWithPreservation(function() {
        R.get(); // create dependency
        var html = render_func();
        html = Spark.attachEvents(events, html);
        html = Spark.setDataContext(buf, html);
        return html;
      }));
    div.show(true);
  
    var getbuf = function() {
      var ret = buf.slice();
      buf.length = 0;
      return ret;
    };
  
    var self;
    return self = {
      focus: function(optCallback) {
        focusElement(self.inputNode());
  
        if (optCallback)
          Meteor.defer(function() { optCallback(getbuf()); });
        else
          return getbuf();
      },
      blur: function(optCallback) {
        blurElement(self.inputNode());
  
        if (optCallback)
          Meteor.defer(function() { optCallback(getbuf()); });
        else
          return getbuf();
      },
      click: function() {
        clickElement(self.inputNode());
        return getbuf();
      },
      kill: function() {
        // clean up
        div.kill();
        Deps.flush();
      },
      inputNode: function() {
        return div.node().getElementsByTagName("input")[0];
      },
      redraw: function() {
        R.set(R.get() + 1);
        Deps.flush();
      }
    };
  };
  
  // Note:  These tests MAY FAIL if the browser window doesn't have focus
  // (isn't frontmost) in some browsers, particularly Firefox.
  testAsyncMulti("spark - focus/blur events",
    (function() {
  
      var textLevel1 = '<input type="text" />';
      var textLevel2 = '<span id="spanOfMurder"><input type="text" /></span>';
  
      var focus_test = function(render_func, events, expected_results) {
        return function(test, expect) {
          var tester = make_input_tester(render_func, events);
          var callback = expect(expected_results);
          tester.focus(function(buf) {
            tester.kill();
            callback(buf);
          });
        };
      };
  
      var blur_test = function(render_func, events, expected_results) {
        return function(test, expect) {
          var tester = make_input_tester(render_func, events);
          var callback = expect(expected_results);
          tester.focus();
          tester.blur(function(buf) {
            tester.kill();
            callback(buf);
          });
        };
      };
  
      return [
  
        // focus on top-level input
        focus_test(textLevel1, 'focus input', ['focus input']),
  
        // focus on second-level input
        // issue #108
        focus_test(textLevel2, 'focus input', ['focus input']),
  
        // focusin
        focus_test(textLevel1, 'focusin input', ['focusin input']),
        focus_test(textLevel2, 'focusin input', ['focusin input']),
  
        // focusin bubbles
        focus_test(textLevel2, 'focusin span', ['focusin span']),
  
        // focus doesn't bubble
        focus_test(textLevel2, 'focus span', []),
  
        // blur works, doesn't bubble
        blur_test(textLevel1, 'blur input', ['blur input']),
        blur_test(textLevel2, 'blur input', ['blur input']),
        blur_test(textLevel2, 'blur span', []),
  
        // focusout works, bubbles
        blur_test(textLevel1, 'focusout input', ['focusout input']),
        blur_test(textLevel2, 'focusout input', ['focusout input']),
        blur_test(textLevel2, 'focusout span', ['focusout span'])
      ];
    })());
  
  
  Tinytest.add("spark - change events", function(test) {
  
    var checkboxLevel1 = '<input type="checkbox" />';
    var checkboxLevel2 = '<span id="spanOfMurder">'+
      '<input type="checkbox" id="checkboxy" /></span>';
  
  
    // on top-level
    var checkbox1 = make_input_tester(checkboxLevel1, 'change input');
    test.equal(checkbox1.click(), ['change input']);
    checkbox1.kill();
  
    // on second-level (should bubble)
    var checkbox2 = make_input_tester(checkboxLevel2,
                                      'change input', 'change span');
    test.equal(checkbox2.click(), ['change input', 'change span']);
    test.equal(checkbox2.click(), ['change input', 'change span']);
    checkbox2.redraw();
    test.equal(checkbox2.click(), ['change input', 'change span']);
    checkbox2.kill();
  
    checkbox2 = make_input_tester(checkboxLevel2, 'change input');
    test.equal(checkbox2.focus(), []);
    test.equal(checkbox2.click(), ['change input']);
    test.equal(checkbox2.blur(), []);
    test.equal(checkbox2.click(), ['change input']);
    checkbox2.kill();
  
    var checkbox2 = make_input_tester(
      checkboxLevel2,
      'change input', 'change span', 'change div');
    test.equal(checkbox2.click(), ['change input', 'change span']);
    checkbox2.kill();
  
  });
  
  
  testAsyncMulti(
    "spark - submit events",
    (function() {
      var hitlist = [];
      var killLater = function(thing) {
        hitlist.push(thing);
      };
  
      var LIVEUI_TEST_RESPONDER = "/spark_test_responder";
      var IFRAME_URL_1 = LIVEUI_TEST_RESPONDER + "/";
      var IFRAME_URL_2 = "about:blank"; // most cross-browser-compatible
      if (window.opera) // opera doesn't like 'about:blank' form target
        IFRAME_URL_2 = LIVEUI_TEST_RESPONDER+"/blank";
  
      return [
        function(test, expect) {
  
          // Submit events can be canceled with preventDefault, which prevents the
          // browser's native form submission behavior.  This behavior takes some
          // work to ensure cross-browser, so we want to test it.  To detect
          // a form submission, we target the form at an iframe.  Iframe security
          // makes this tricky.  What we do is load a page from the server that
          // calls us back on 'load' and 'unload'.  We wait for 'load', set up the
          // test, and then see if we get an 'unload' (due to the form submission
          // going through) or not.
          //
          // This is quite a tricky implementation.
  
          var withIframe = function(onReady1, onReady2) {
            var frameName = "submitframe"+String(Random.fraction()).slice(2);
            var iframeDiv = OnscreenDiv(
              Meteor.render(function() {
                return '<iframe name="'+frameName+'" '+
                  'src="'+IFRAME_URL_1+'"></iframe>';
              }));
            var iframe = iframeDiv.node().firstChild;
  
            iframe.loadFunc = function() {
              onReady1(frameName, iframe, iframeDiv);
              onReady2(frameName, iframe, iframeDiv);
            };
            iframe.unloadFunc = function() {
              iframe.DID_CHANGE_PAGE = true;
            };
          };
          var expectCheckLater = function(options) {
            var check = expect(function(iframe, iframeDiv) {
              if (options.shouldSubmit)
                test.isTrue(iframe.DID_CHANGE_PAGE);
              else
                test.isFalse(iframe.DID_CHANGE_PAGE);
  
              // must do this inside expect() so it happens in time
              killLater(iframeDiv);
            });
            var checkLater = function(frameName, iframe, iframeDiv) {
              Meteor.setTimeout(function() {
                check(iframe, iframeDiv);
              }, 500); // wait for frame to unload
            };
            return checkLater;
          };
          var buttonFormHtml = function(frameName) {
            return '<div style="height:0;overflow:hidden">'+
              '<form action="'+IFRAME_URL_2+'" target="'+
              frameName+'">'+
              '<span><input type="submit"></span>'+
              '</form></div>';
          };
  
          // test that form submission by click fires event,
          // and also actually submits
          withIframe(function(frameName, iframe) {
            var form = make_input_tester(
              buttonFormHtml(frameName), 'submit form');
            test.equal(form.click(), ['submit form']);
            killLater(form);
          }, expectCheckLater({shouldSubmit:true}));
  
          // submit bubbles up
          withIframe(function(frameName, iframe) {
            var form = make_input_tester(
              buttonFormHtml(frameName), 'submit form', 'submit div');
            test.equal(form.click(), ['submit form', 'submit div']);
            killLater(form);
          }, expectCheckLater({shouldSubmit:true}));
  
          // preventDefault works, still bubbles
          withIframe(function(frameName, iframe) {
            var form = make_input_tester(
              buttonFormHtml(frameName), {
                'submit form': function(evt) {
                  test.equal(evt.type, 'submit');
                  test.equal(evt.target.nodeName, 'FORM');
                  this.push('submit form');
                  evt.preventDefault();
                },
                'submit div': function(evt) {
                  test.equal(evt.type, 'submit');
                  test.equal(evt.target.nodeName, 'FORM');
                  this.push('submit div');
                },
                'submit a': function(evt) {
                  this.push('submit a');
                }
              }
            );
            test.equal(form.click(), ['submit form', 'submit div']);
            killLater(form);
          }, expectCheckLater({shouldSubmit:false}));
  
        },
        function(test, expect) {
          _.each(hitlist, function(thing) {
            thing.kill();
          });
          Deps.flush();
        }
      ];
    })());
  
  
  Tinytest.add("spark - controls - radio", function(test) {
    var R = ReactiveVar("");
    var R2 = ReactiveVar("");
    var change_buf = [];
    var div = OnscreenDiv(renderWithPreservation(function() {
      // Re-render when R2 is changed, even though it doesn't affect HTML.
      R2.get();
  
      var buf = [];
      buf.push("Band: ");
      _.each(["AM", "FM", "XM"], function(band) {
        var checked = (R.get() === band) ? 'checked="checked"' : '';
        buf.push('<input type="radio" name="bands" '+
                 'value="'+band+'" '+checked+'/>');
      });
      buf.push(R.get());
      var html = buf.join('');
  
      html = Spark.attachEvents({
        'change input': function(event) {
          // IE 7 is known to fire change events on all
          // the radio buttons with checked=false, as if
          // each button were deselected before selecting
          // the new one.  (Meteor doesn't normalize this
          // behavior.)
          // However, browsers are consistent if we are
          // getting a checked=true notification.
          var btn = event.target;
          if (btn.checked) {
            var band = btn.value;
            change_buf.push(band);
            R.set(band);
          }
        }
      }, html);
      return html;
    }));
  
    Deps.flush();
  
    // get the three buttons; they should be considered 'labeled'
    // by the patcher and not change identities!
    var btns = nodesToArray(div.node().getElementsByTagName("INPUT"));
  
    test.equal(_.pluck(btns, 'checked'), [false, false, false]);
    test.equal(div.text(), "Band: ");
  
    clickElement(btns[0]);
    test.equal(change_buf, ['AM']);
    change_buf.length = 0;
    Deps.flush();
    test.equal(_.pluck(btns, 'checked'), [true, false, false]);
    test.equal(div.text(), "Band: AM");
  
    R2.set("change");
    Deps.flush();
    test.length(change_buf, 0);
    test.equal(_.pluck(btns, 'checked'), [true, false, false]);
    test.equal(div.text(), "Band: AM");
  
    clickElement(btns[1]);
    test.equal(change_buf, ['FM']);
    change_buf.length = 0;
    Deps.flush();
    test.equal(_.pluck(btns, 'checked'), [false, true, false]);
    test.equal(div.text(), "Band: FM");
  
    clickElement(btns[2]);
    test.equal(change_buf, ['XM']);
    change_buf.length = 0;
    Deps.flush();
    test.equal(_.pluck(btns, 'checked'), [false, false, true]);
    test.equal(div.text(), "Band: XM");
  
    clickElement(btns[1]);
    test.equal(change_buf, ['FM']);
    change_buf.length = 0;
    Deps.flush();
    test.equal(_.pluck(btns, 'checked'), [false, true, false]);
    test.equal(div.text(), "Band: FM");
  
    div.kill();
  });
  
  Tinytest.add("spark - controls - checkbox", function(test) {
    var labels = ["Foo", "Bar", "Baz"];
    var Rs = {};
    _.each(labels, function (label) {
      Rs[label] = ReactiveVar(false);
    });
    var changeBuf = [];
    var div = OnscreenDiv(renderWithPreservation(function() {
      var buf = [];
      _.each(labels, function (label) {
        var checked = Rs[label].get() ? 'checked="checked"' : '';
        buf.push('<input type="checkbox" name="checky" '+
                 'value="'+label+'" '+checked+'/>');
      });
      return buf.join('');
    }));
  
    Deps.flush();
  
    // get the three boxes; they should be considered 'labeled' by the patcher and
    // not change identities!
    var boxes = nodesToArray(div.node().getElementsByTagName("INPUT"));
  
    test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
  
    // Re-render with first one checked.
    Rs.Foo.set(true);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [true, false, false]);
  
    // Re-render with first one unchecked again.
    Rs.Foo.set(false);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
  
    // User clicks the second one.
    clickElement(boxes[1]);
    test.equal(_.pluck(boxes, 'checked'), [false, true, false]);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [false, true, false]);
  
    // Re-render with third one checked. Second one should stay checked because
    // it's a user update!
    Rs.Baz.set(true);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [false, true, true]);
  
    // User turns second and third off.
    clickElement(boxes[1]);
    clickElement(boxes[2]);
    test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
  
    // Re-render with first one checked. Third should stay off because it's a user
    // update!
    Rs.Foo.set(true);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [true, false, false]);
  
    // Re-render with first one unchecked. Third should still stay off.
    Rs.Foo.set(false);
    Deps.flush();
    test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
  
    div.kill();
  });
  
  _.each(['textarea', 'text', 'password', 'submit', 'button',
          'reset', 'select', 'hidden'], function (type) {
    Tinytest.add("spark - controls - " + type, function(test) {
      var R = ReactiveVar({x:"test"});
      var R2 = ReactiveVar("");
      var div = OnscreenDiv(renderWithPreservation(function() {
        // Re-render when R2 is changed, even though it doesn't affect HTML.
        R2.get();
        if (type === 'textarea') {
          return '<textarea id="someId">This is a ' + R.get().x + '</textarea>';
        } else if (type === 'select') {
          var options = ['This is a test', 'This is a fridge',
                         'This is a frog', 'foobar', 'This is a photograph',
                         'This is a monkey', 'This is a donkey'];
          return '<select id="someId">' + _.map(options, function (o) {
            var maybeSel = ('This is a ' + R.get().x) === o ? 'selected' : '';
            return '<option ' + maybeSel + '>' + o + '</option>';
          }).join('') + '</select>';
        } else {
          return '<input type="' + type + '" id="someId" value="This is a ' +
            R.get().x + '">';
        }
      }));
      div.show(true);
      var canFocus = (type !== 'hidden');
  
      var input = div.node().firstChild;
      if (type === 'textarea' || type === 'select') {
        test.equal(input.nodeName, type.toUpperCase());
      } else {
        test.equal(input.nodeName, 'INPUT');
        test.equal(input.type, type);
      }
      test.equal(DomUtils.getElementValue(input), "This is a test");
      test.equal(input._sparkOriginalRenderedValue, ["This is a test"]);
  
      // value updates reactively
      R.set({x:"fridge"});
      Deps.flush();
      test.equal(DomUtils.getElementValue(input), "This is a fridge");
      test.equal(input._sparkOriginalRenderedValue, ["This is a fridge"]);
  
      if (canFocus) {
        // ...unless focused
        focusElement(input);
        R.set({x:"frog"});
        Deps.flush();
        test.equal(DomUtils.getElementValue(input), "This is a fridge");
        test.equal(input._sparkOriginalRenderedValue, ["This is a fridge"]);
  
        // blurring and re-setting works
        blurElement(input);
        Deps.flush();
        test.equal(DomUtils.getElementValue(input), "This is a fridge");
        test.equal(input._sparkOriginalRenderedValue, ["This is a fridge"]);
      }
      R.set({x:"frog"});
      Deps.flush();
      test.equal(DomUtils.getElementValue(input), "This is a frog");
      test.equal(input._sparkOriginalRenderedValue, ["This is a frog"]);
  
      // Setting a value (similar to user typing) should prevent value from being
      // reverted if the div is re-rendered but the rendered value (ie, R) does
      // not change.
      DomUtils.setElementValue(input, "foobar");
      R2.set("change");
      Deps.flush();
      test.equal(DomUtils.getElementValue(input), "foobar");
      test.equal(input._sparkOriginalRenderedValue, ["This is a frog"]);
  
      // ... but if the actual rendered value changes, that should take effect.
      R.set({x:"photograph"});
      Deps.flush();
      test.equal(DomUtils.getElementValue(input), "This is a photograph");
      test.equal(input._sparkOriginalRenderedValue, ["This is a photograph"]);
  
      // If the rendered value and user value change in the same way (eg, the user
      // changed it and then invoked a menthod that set the database value based
      // on what they changed), make sure that the _sparkOriginalRenderedValue
      // gets updated too.
      DomUtils.setElementValue(input, "This is a monkey");
      R.set({x:"monkey"});
      Deps.flush();
      test.equal(DomUtils.getElementValue(input), "This is a monkey");
      test.equal(input._sparkOriginalRenderedValue, ["This is a monkey"]);
  
      if (canFocus) {
        // The same as the previous test... except make sure that it still works
        // if the input is focused. ie, imagine that the user edited the field and
        // hit enter with the field still focused, updating the database to match
        // the field and keeping the field focused.
        DomUtils.setElementValue(input, "This is a donkey");
        focusElement(input);
        R.set({x:"donkey"});
        Deps.flush();
        test.equal(DomUtils.getElementValue(input), "This is a donkey");
        test.equal(input._sparkOriginalRenderedValue, ["This is a donkey"]);
      }
  
      div.kill();
    });
  });
  
  Tinytest.add("spark - oldschool landmark matching", function(test) {
  
    // basic created / onscreen / offscreen callback flow
    // (ported from old chunk-matching API)
  
    var buf;
    var counts;
  
    var testCallbacks = function(theNum /*, extend opts*/) {
      return _.extend.apply(_, [{
        created: function() {
          this.num = String(theNum);
          var howManyBefore = counts[this.num] || 0;
          counts[this.num] = howManyBefore + 1;
          for(var i=0;i<howManyBefore;i++)
            this.num += "*"; // add stars
          buf.push("c"+this.num);
        },
        rendered: function(start, end, range) {
          buf.push("r"+this.num);
        },
        destroyed: function() {
          buf.push("d"+this.num);
        }
      }].concat(_.toArray(arguments).slice(1)));
    };
  
    buf = [];
    counts = {};
    var R = ReactiveVar("A");
    var div = OnscreenDiv(Meteor.render(function() {
      var html = Spark.createLandmark(testCallbacks(0), function () {
        return String(R.get());
      });
      return html;
    }, testCallbacks(0)));
  
    test.equal(buf, ["c0"]);
  
    test.equal(div.html(), "A");
    Deps.flush();
    test.equal(buf, ["c0", "r0"]);
    test.equal(div.html(), "A");
  
    R.set("B");
    Deps.flush();
    test.equal(buf, ["c0", "r0", "r0"]);
    test.equal(div.html(), "B");
  
  
    div.kill();
    Deps.flush();
    test.equal(buf, ["c0", "r0", "r0", "d0"]);
  
    // with a branch
  
    buf = [];
    counts = {};
    R = ReactiveVar("A");
    div = OnscreenDiv(Meteor.render(function() {
      R.get();
      return Spark.createLandmark(testCallbacks(0), function () {
        var html = Spark.labelBranch("foo", function () {
          return Spark.createLandmark(testCallbacks(1),
                                      function () { return "HI"; });
        });
        return "<div>" + html + "</div>";
      });
    }));
  
    test.equal(buf, ["c0", "c1"]);
    Deps.flush();
    // what order of chunks {0,1} is preferable??
    // should be consistent but I'm not sure what makes most sense.
    test.equal(buf, "c0,c1,r1,r0".split(','));
    buf.length = 0;
  
    R.set("B");
    Deps.flush();
    test.equal(buf, "r1,r0".split(','));
    buf.length = 0;
  
    div.kill();
    Deps.flush();
    buf.sort();
    test.equal(buf, "d0,d1".split(','));
  });
  
  
  Tinytest.add("spark - oldschool branch keys", function(test) {
  
    var R, div;
  
    // Re-rendered Meteor.render keeps same landmark state
  
    var objs = [];
    R = ReactiveVar("foo");
    div = OnscreenDiv(Meteor.render(function() {
      return Spark.createLandmark({
        rendered: function () { objs.push(true); }
      }, function () { return R.get(); });
    }));
  
    Deps.flush();
    R.set("bar");
    Deps.flush();
    R.set("baz");
    Deps.flush();
  
    test.equal(objs.length, 3);
    test.isTrue(objs[0] === objs[1]);
    test.isTrue(objs[1] === objs[2]);
  
    div.kill();
    Deps.flush();
  
    // track chunk matching / re-rendering in detail
  
    var buf;
    var counts;
  
    var testCallbacks = function(theNum /*, extend opts*/) {
      return _.extend.apply(_, [{
        created: function() {
          this.num = String(theNum);
          var howManyBefore = counts[this.num] || 0;
          counts[this.num] = howManyBefore + 1;
          for(var i=0;i<howManyBefore;i++)
            this.num += "*"; // add stars
          buf.push("c"+this.num);
        },
        rendered: function(start, end, range) {
          buf.push("on"+this.num);
        },
        destroyed: function() {
          buf.push("off"+this.num);
        }
      }].concat(_.toArray(arguments).slice(1)));
    };
  
    var counter = 1;
    var chunk = function(contents, num, branch) {
      if (branch === null)
        branch = "unique_branch_" + (counter++);
  
      return Spark.labelBranch(branch, function () {
        return Spark.createLandmark(
          testCallbacks(num),
          function () {
            if (typeof contents === "string")
              return contents;
            else if (_.isArray(contents))
              return _.map(contents, function(x) {
                if (typeof x === 'string')
                  return x;
                return chunk(x[0], x[1], x[2]);
              }).join('');
            else
              return contents();
          });
      });
    };
  
    ///// Chunk 1 contains 2,3,4, all should be matched
  
    buf = [];
    counts = {};
  
    R = ReactiveVar("foo");
    div = OnscreenDiv(Meteor.render(function() {
      if (R.get() === 'nothing')
        return "no chunk!";
      else
        return chunk([['<span>apple</span>', 2, 'x'],
                      ['<span>banana</span>', 3, 'y'],
                      ['<span>kiwi</span>', 4, 'z']
                     ], 1, 'fruit');
    }));
  
    Deps.flush();
    buf.sort();
    test.equal(buf, ['c1', 'c2', 'c3', 'c4', 'on1', 'on2', 'on3', 'on4']);
    buf.length = 0;
  
    R.set("bar");
    Deps.flush();
    buf.sort();
    test.equal(buf, ['on1', 'on2', 'on3', 'on4']);
    buf.length = 0;
  
    R.set("nothing");
    Deps.flush();
    buf.sort();
    test.equal(buf, ['off1', 'off2', 'off3', 'off4']);
    buf.length = 0;
  
    div.kill();
    Deps.flush();
  
    ///// Chunk 3 has no branch key, should be recreated
  
    buf = [];
    counts = {};
  
    R = ReactiveVar("foo");
    div = OnscreenDiv(Meteor.render(function() {
      if (R.get() === 'nothing')
        return "no chunk!";
      else
        return chunk([['<span>apple</span>', 2, 'x'],
                      ['<span>banana</span>', 3, null],
                      ['<span>kiwi</span>', 4, 'z']
                     ], 1, 'fruit');
    }));
  
    Deps.flush();
    buf.sort();
    test.equal(buf, ['c1', 'c2', 'c3', 'c4', 'on1', 'on2', 'on3', 'on4']);
    buf.length = 0;
  
    R.set("bar");
    Deps.flush();
    buf.sort();
    test.equal(buf, ['c3*', 'off3', 'on1', 'on2', 'on3*', 'on4']);
    buf.length = 0;
  
    div.kill();
    Deps.flush();
    buf.sort();
    // killing the div should have given us offscreen calls for 1,2,3*,4
    test.equal(buf, ['off1', 'off2', 'off3*', 'off4']);
    buf.length = 0;
  
  
    // XXX test intermediate unkeyed chunks;
    // duplicate branch keys; different order
  });
  
  Tinytest.add("spark - isolate inside landmark", function (test) {
  
    // test that preservation maps from all landmarks are honored when
    // an isolate is re-rendered, even the landmarks that are outside
    // the isolate and therefore not involved in the re-render.
  
    var R = ReactiveVar(1);
    var d = OnscreenDiv(Spark.render(function () {
      return Spark.createLandmark(
        { preserve: ['.foo'] },
        function () {
          return Spark.isolate(function () {
            return '<hr class="foo"/>' + R.get();
          });
        });
    }));
  
    var foo1 = d.node().firstChild;
    test.equal(d.node().firstChild.nextSibling.nodeValue, '1');
    R.set(2);
    Deps.flush();
    var foo2 = d.node().firstChild;
    test.equal(d.node().firstChild.nextSibling.nodeValue, '2');
    test.isTrue(foo1 === foo2);
    d.kill();
    Deps.flush();
  
    // test that selectors in a landmark preservation map are resolved
    // relative to the landmark, not relative to the re-rendered
    // fragment.  the selector may refer to nodes that are outside the
    // re-rendered fragment, and the selector will still match.
  
    R = ReactiveVar(1);
    d = OnscreenDiv(Spark.render(function () {
      return Spark.createLandmark(
        { preserve: ['div .foo'] },
        function () {
          return "<div>"+Spark.isolate(function () {
            return '<hr class="foo"/>' + R.get();
          })+"</div>";
        });
    }));
  
    var foo1 = DomUtils.find(d.node(), '.foo');
    test.equal(foo1.nodeName, 'HR');
    test.equal(foo1.nextSibling.nodeValue, '1');
    R.set(2);
    Deps.flush();
    var foo2 = DomUtils.find(d.node(), '.foo');
    test.equal(foo2.nodeName, 'HR');
    test.equal(foo2.nextSibling.nodeValue, '2');
    test.isTrue(foo1 === foo2);
    d.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - nested onscreen processing", function (test) {
    var cursor = {
      observeChanges: function () { return { stop: function () {} }; }
    };
  
    var x = [];
    var d = OnscreenDiv(Spark.render(function () {
      return Spark.list(cursor, function () {}, function () {
        return Spark.list(cursor, function () {}, function () {
          return Spark.list(cursor, function () {}, function () {
            return Spark.createLandmark({
              created: function () { x.push('c'); },
              rendered: function () { x.push('r'); },
              destroyed: function () { x.push('d'); }
            }, function () { return "hi"; });
          });
        });
      });
    }));
  
    Deps.flush();
    test.equal(x.join(''), 'cr');
    x = [];
    d.kill();
    Deps.flush();
    test.equal(x.join(''), 'd');
  });
  
  Tinytest.add("spark - current landmark", function (test) {
    var R = ReactiveVar(1);
    var callbacks = 0;
    var d = OnscreenDiv(Meteor.render(function () {
      var html = Spark.createLandmark({
        created: function () {
          this.a = 1;
          this.renderCount = 0;
          test.isFalse('b' in this);
          callbacks++;
        },
        rendered: function () {
          test.equal(this.a, 9);
          test.equal(this.b, 2);
          if (this.renderCount === 0)
            test.isFalse('c' in this);
          else
            test.isTrue('c' in this);
          this.renderCount++;
          callbacks++;
        },
        destroyed: function () {
          test.equal(this.a, 9);
          test.equal(this.b, 2);
          test.equal(this.c, 3);
          callbacks++;
        }
      }, function (lm) {
        var html = '<span>hi</span>';
  
        if (R.get() === 1) {
          test.equal(callbacks, 1);
          test.equal(lm.a, 1);
          lm.a = 9;
          lm.b = 2;
          test.isFalse('c' in lm);
          test.equal(callbacks, 1);
          lm = null;
        }
  
        if (R.get() === 2) {
          test.equal(callbacks, 2);
          test.equal(lm.a, 9);
          test.equal(lm.b, 2);
          test.equal(lm.c, 3);
          test.equal(lm.renderCount, 1);
        }
  
        return html;
      });
  
  
      if (R.get() >= 3) {
        html += Spark.labelBranch('branch', function () {
          var html = Spark.createLandmark({
            created: function () {
              this.outer = true;
            },
            rendered: function () {
              this.renderCount = (this.renderCount || 0) + 1;
            }
          }, function (lm) {
            var html = '<span>outer</span>';
            test.isTrue(lm.outer);
            test.equal(R.get() - 3, lm.renderCount || 0);
            html += Spark.labelBranch("a", function () {
              var html = Spark.createLandmark({
                created: function () {
                  this.innerA = true;
                },
                rendered: function () {
                  this.renderCount = (this.renderCount || 0) + 1;
                }
              }, function (lm) {
                var html = '<span>innerA</span>';
                test.isTrue(lm.innerA);
                return html;
              });
              return html;
            });
            return html;
          });
  
          if (R.get() === 3 || R.get() >= 5) {
            html += Spark.labelBranch("b", function () {
              var html = Spark.createLandmark({
                created: function () {
                  this.innerB = true;
                },
                rendered: function () {
                  this.renderCount = (this.renderCount || 0) + 1;
                }
              }, function (lm) {
                var html = '<span>innerB</span>';
                test.isTrue(lm.innerB);
                test.equal(R.get() === 3 ? 0 : R.get() - 5,
                           lm.renderCount || 0);
                return html;
              });
              return html;
            });
          }
          return html;
        });
      }
      return html;
    }));
  
    var findOuter = function () {
      return d.node().firstChild.nextSibling;
    };
  
    var findInnerA = function () {
      return findOuter().nextSibling;
    };
  
    var findInnerB = function () {
      return findInnerA().nextSibling;
    };
  
    test.equal(callbacks, 1);
    Deps.flush();
    test.equal(callbacks, 2);
    test.equal(null, Spark._getEnclosingLandmark(d.node()));
    var enc = Spark._getEnclosingLandmark(d.node().firstChild);
    test.equal(enc.a, 9);
    test.equal(enc.b, 2);
    test.isFalse('c' in enc);
    enc.c = 3;
    Deps.flush();
    test.equal(callbacks, 2);
  
    R.set(2)
    Deps.flush();
    test.equal(callbacks, 3);
  
    R.set(3)
    Deps.flush();
    test.equal(callbacks, 4);
  
    test.isTrue(Spark._getEnclosingLandmark(findOuter()).outer);
    test.isTrue(Spark._getEnclosingLandmark(findInnerA()).innerA);
    test.isTrue(Spark._getEnclosingLandmark(findInnerB()).innerB);
    test.equal(1, Spark._getEnclosingLandmark(findOuter()).renderCount);
    test.equal(1, Spark._getEnclosingLandmark(findInnerA()).renderCount);
    test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount);
  
    R.set(4)
    Deps.flush();
    test.equal(callbacks, 5);
    test.equal(2, Spark._getEnclosingLandmark(findOuter()).renderCount);
    test.equal(2, Spark._getEnclosingLandmark(findInnerA()).renderCount);
  
    R.set(5)
    Deps.flush();
    test.equal(callbacks, 6);
    test.equal(3, Spark._getEnclosingLandmark(findOuter()).renderCount);
    test.equal(3, Spark._getEnclosingLandmark(findInnerA()).renderCount);
    test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount);
  
    R.set(6)
    Deps.flush();
    test.equal(callbacks, 7);
    test.equal(4, Spark._getEnclosingLandmark(findOuter()).renderCount);
    test.equal(4, Spark._getEnclosingLandmark(findInnerA()).renderCount);
    test.equal(2, Spark._getEnclosingLandmark(findInnerB()).renderCount);
  
    d.kill();
    Deps.flush();
    test.equal(callbacks, 8);
  
    Deps.flush();
    test.equal(callbacks, 8);
  });
  
  Tinytest.add("spark - find/findAll on landmark", function (test) {
    var l1, l2;
    var R = ReactiveVar(1);
  
    var d = OnscreenDiv(Spark.render(function () {
      return "<div id=1>k</div><div id=2>" +
        Spark.labelBranch("a", function () {
          return Spark.createLandmark({
            created: function () {
              test.instanceOf(this, Spark.Landmark);
              if (l1)
                test.equal(l1, this);
              l1 = this;
            }
          }, function () {
            return "<span class='a' id=3>a" +
              Spark.labelBranch("b", function () {
                return Spark.isolate(
                  function () {
                    R.get();
                    return Spark.createLandmark(
                      {
                        created: function () {
                          test.instanceOf(this, Spark.Landmark);
                          if (l2)
                            test.equal(l2, this);
                          l2 = this;
                        }
                      }, function () {
                        return "<span class='b' id=4>b4</span>" +
                          "<span class='b' id=6>b6</span>";
                      });
                  });
              }) + "</span>";
          });
        }) + "<span class='c' id=5>c</span></div>";
    }));
  
    var ids = function (nodes) {
      if (!(nodes instanceof Array))
        nodes = nodes ? [nodes] : [];
      return _.pluck(nodes, 'id').join('');
    };
  
    var check = function (all) {
      var f = all ? 'findAll' : 'find';
  
      test.equal(ids(l1[f]('.kitten')), '');
      test.equal(ids(l2[f]('.kitten')), '');
  
      test.equal(ids(l1[f]('.a')), '3');
      test.equal(ids(l2[f]('.a')), '');
  
      test.equal(ids(l1[f]('.b')), all ? '46' : '4');
      test.equal(ids(l2[f]('.b')), all ? '46' : '4');
  
      test.equal(ids(l1[f]('.c')), '');
      test.equal(ids(l2[f]('.c')), '');
  
      test.equal(ids(l1[f]('.a .b')), all ? '46' : '4');
      test.equal(ids(l2[f]('.a .b')), '');
    };
  
    check(false);
    check(true);
    R.set(2);
    Deps.flush();
    check(false);
    check(true);
  
    d.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - landmark clean-up", function (test) {
  
    var crd;
    var makeCrd = function () {
      var crd = [0,0,0];
      crd.callbacks = {
        created: function () { crd[0]++; },
        rendered: function () { crd[1]++; },
        destroyed: function () { crd[2]++; }
      };
      return crd;
    };
  
    // not inside render
    crd = makeCrd();
    Spark.createLandmark(crd.callbacks, function () { return 'hi'; });
    test.equal(crd, [1,0,1]);
  
    // landmark never materialized
    crd = makeCrd();
    Spark.render(function() {
      var html =
            Spark.createLandmark(crd.callbacks, function () { return 'hi'; });
      return '';
    });
    test.equal(crd, [1,0,1]);
    Deps.flush();
    test.equal(crd, [1,0,1]);
  
    // two landmarks, only one materialized at a time.
    // one replaces the other
    var crd1 = makeCrd();
    var crd2 = makeCrd();
    var R = ReactiveVar(1);
    var div = OnscreenDiv(Meteor.render(function() {
      return (R.get() === 1 ?
              Spark.createLandmark(crd1.callbacks, function() { return 'hi'; }) :
              Spark.createLandmark(crd2.callbacks, function() { return 'hi'; }));
    }));
    test.equal(crd1, [1,0,0]); // created
    test.equal(crd2, [0,0,0]);
    Deps.flush();
    test.equal(crd1, [1,1,0]); // rendered
    test.equal(crd2, [0,0,0]);
    R.set(2);
    Deps.flush();
    test.equal(crd1, [1,1,0]); // not destroyed (callback replaced)
    test.equal(crd2, [0,1,0]); // matched
  
    div.kill();
    Deps.flush();
    test.equal(crd1, [1,1,0]);
    test.equal(crd2, [0,1,1]); // destroyed
  });
  
  Tinytest.add("spark - bubbling render", function (test) {
    var makeCrd = function () {
      var crd = [0,0,0];
      crd.callbacks = {
        created: function () { crd[0]++; },
        rendered: function () { crd[1]++; },
        destroyed: function () { crd[2]++; }
      };
      return crd;
    };
  
    var crd1 = makeCrd();
    var crd2 = makeCrd();
  
    var R = ReactiveVar('foo');
    var div = OnscreenDiv(Spark.render(function () {
      return Spark.createLandmark(crd1.callbacks, function () {
        return Spark.labelBranch('fred', function () {
          return Spark.createLandmark(crd2.callbacks, function () {
            return Spark.isolate(function () {
              return R.get();
            });
          });
        });
      });
    }));
  
    Deps.flush();
    test.equal(div.html(), 'foo');
    test.equal(crd1, [1,1,0]);
    test.equal(crd2, [1,1,0]);
  
    R.set('bar');
    Deps.flush();
    test.equal(div.html(), 'bar');
    test.equal(crd1, [1,2,0]);
    test.equal(crd2, [1,2,0]);
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - landmark arg", function (test) {
    var div = OnscreenDiv(Spark.render(function () {
      return Spark.createLandmark({
        created: function () {
          test.isFalse(this.hasDom());
        },
        rendered: function () {
          var landmark = this;
          landmark.firstNode().innerHTML = 'Greetings';
          landmark.lastNode().innerHTML = 'Line';
          landmark.find('i').innerHTML =
            (landmark.findAll('b').length)+"-bold";
          test.isTrue(landmark.hasDom());
        },
        destroyed: function () {
          test.isFalse(this.hasDom());
        }
      }, function () {
        return Spark.attachEvents({
          'click': function (event, landmark) {
            landmark.firstNode().innerHTML = 'Hello';
            landmark.lastNode().innerHTML = 'World';
            landmark.find('i').innerHTML =
              (landmark.findAll('*').length)+"-element";
          }
        }, '<b>Foo</b> <i>Bar</i> <u>Baz</u>');
      });
    }));
  
    test.equal(div.text(), "Foo Bar Baz");
    Deps.flush();
    test.equal(div.text(), "Greetings 1-bold Line");
    clickElement(DomUtils.find(div.node(), 'i'));
    test.equal(div.text(), "Hello 3-element World");
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - landmark preserve", function (test) {
    var R = ReactiveVar("foo");
  
    var lmhr = function () {
      return Spark.createLandmark({preserve:['hr']}, function () {
        return '<hr/>';
      });
    };
  
    var div = OnscreenDiv(Meteor.render(function () {
      return "<div><span>" + R.get() + "</span>" +
        Spark.labelBranch('A', lmhr) + Spark.labelBranch('B', lmhr) +
        "</div>";
    }));
  
    test.equal(div.html(), '<div><span>foo</span><hr><hr></div>');
    var hrs1 = DomUtils.findAll(div.node(), 'hr');
    R.set("bar");
    Deps.flush();
    test.equal(div.html(), '<div><span>bar</span><hr><hr></div>');
    var hrs2 = DomUtils.findAll(div.node(), 'hr');
  
    test.isTrue(hrs1[0] === hrs2[0]);
    test.isTrue(hrs1[1] === hrs2[1]);
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - branch annotation is optional", function (test) {
    // test that labelBranch works on HTML that isn't element-balanced
    // and doesn't fail by trying to emit an annotation when it contains
    // no landmarks.
  
    var R = ReactiveVar("foo");
  
    var Rget = function () { return R.get(); };
    var cnst = function (c) { return function () { return c; }; };
    var lmhr = function () {
      return Spark.createLandmark({preserve:['hr']}, function () {
        return '<hr/>';
      });
    };
  
    var div = OnscreenDiv(Meteor.render(function () {
      return '<div class="' + Spark.labelBranch('A', Rget) + '">' +
        Spark.labelBranch('B', cnst('</div><div>')) +
        Spark.labelBranch('C', lmhr) + Spark.labelBranch('D', lmhr) +
        '</div>';
    }));
  
    test.equal(div.html(), '<div class="foo"></div><div><hr><hr></div>');
    var div1 = div.node().firstChild;
    var hrs1 = DomUtils.findAll(div.node(), 'hr');
    R.set("bar");
    Deps.flush();
    test.equal(div.html(), '<div class="bar"></div><div><hr><hr></div>');
    var div2 = div.node().firstChild;
    var hrs2 = DomUtils.findAll(div.node(), 'hr');
  
    test.isFalse(div1 === div2);
    test.isTrue(hrs1[0] === hrs2[0]);
    test.isTrue(hrs1[1] === hrs2[1]);
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - unique label", function (test) {
    var buf = [];
    var bufstr = function () {
      buf.sort();
      var str = buf.join('');
      buf.length = 0;
      return str;
    };
  
    var ublm = function () {
      return Spark.labelBranch(Spark.UNIQUE_LABEL, function () {
        return Spark.createLandmark({created: function () { buf.push('c'); },
                                     rendered: function () { buf.push('r'); },
                                     destroyed: function () { buf.push('d'); }},
                                    function () { return 'x'; });
      });
    };
  
    var R = ReactiveVar("foo");
  
    var div = OnscreenDiv(Meteor.render(function () {
      return ublm() + ublm() + ublm() + R.get();
    }));
    Deps.flush();
    test.equal(bufstr(), 'cccrrr');
    R.set('bar');
    Deps.flush();
    test.equal(bufstr(), 'cccdddrrr');
  
    div.kill();
    Deps.flush();
    test.equal(bufstr(), 'ddd');
  
  });
  
  Tinytest.add("spark - list update", function (test) {
    var R = ReactiveVar('foo');
  
    var lst = [];
    lst.callbacks = [];
    lst.observeChanges = function(callbacks) {
      lst.callbacks.push(callbacks);
      _.each(lst, function(x) {
        callbacks.addedBefore(x._id, x, null);
      });
      return {
        stop: function() {
          lst.callbacks = _.without(lst.callbacks, callbacks);
        }
      };
    };
    lst.another = function () {
      var i = lst.length;
      lst.push({_id:'item'+i});
      _.each(lst.callbacks, function (callbacks) {
        callbacks.addedBefore(lst[i]._id, lst[i], null);
      });
    };
    var div = OnscreenDiv(Meteor.render(function() {
      return R.get() + Spark.list(lst, function () {
        return '<hr>';
      });
    }));
  
    lst.another();
    Deps.flush();
    test.equal(div.html(), "foo<hr>");
  
    lst.another();
    R.set('bar');
    Deps.flush();
    test.equal(div.html(), "bar<hr><hr>");
  
    R.set('baz');
    lst.another();
    Deps.flush();
    test.equal(div.html(), "baz<hr><hr><hr>");
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - callback context", function (test) {
    // Test that context in template callbacks is null.
  
    var cxs = [];
    var buf = [];
  
    var R = ReactiveVar("foo");
    var getCx = function () { return Deps.currentComputation; };
    var callbackFunc = function (ltr) {
      return function () {
        buf.push(ltr);
        cxs.push(getCx());
      };
    };
    var div = OnscreenDiv(Meteor.render(function () {
      var cx = getCx();
      test.isTrue(cx); // sanity check for getCx
      var makeLandmark = function () {
        return Spark.createLandmark({created: callbackFunc('c'),
                                     rendered: callbackFunc('r'),
                                     destroyed: callbackFunc('d')},
                                    function () {
                                      return '<span>'+R.get()+'</span>';
                                    });
      };
      if (R.get() === 'foo')
        var unused = makeLandmark(); // will cause created/destroyed
      var html = Spark.labelBranch("foo", makeLandmark);
      test.isTrue(getCx() === cx); // test that context was restored
      return html;
    }));
    Deps.flush();
    R.set('bar');
    Deps.flush();
    div.kill();
    Deps.flush();
  
    test.equal(buf.join(''), 'ccdrrd');
    test.equal(cxs.length, 6);
    test.isFalse(cxs[0]);
    test.isFalse(cxs[1]);
    test.isFalse(cxs[2]);
    test.isFalse(cxs[3]);
    test.isFalse(cxs[4]);
    test.isFalse(cxs[5]);
  
  });
  
  Tinytest.add("spark - legacy preserve names", function (test) {
    var R = ReactiveVar("foo");
    var R2 = ReactiveVar("apple");
  
    var div = OnscreenDiv(renderWithPreservation(function () {
      R.get(); // create dependency
      return ('<div id="aaa"><div><input name="field"></div></div>' +
              '<div id="bbb"><div><input name="field"></div></div>' +
              '<div id="ccc"><div>' + Spark.isolate(function () {
                R2.get();
                return '<input name="field">'; }) + '</div></div>' +
              '<input type="text">');
    }));
  
  
    var inputs1 = nodesToArray(div.node().getElementsByTagName('input'));
    R.set('bar');
    Deps.flush();
    var inputs2 = nodesToArray(div.node().getElementsByTagName('input'));
    test.isTrue(inputs1[0] === inputs2[0]);
    test.isTrue(inputs1[1] === inputs2[1]);
    test.isTrue(inputs1[2] === inputs2[2]);
    test.isTrue(inputs1[3] !== inputs2[3]);
  
    R2.set('banana');
    Deps.flush();
    var inputs3 = nodesToArray(div.node().getElementsByTagName('input'));
    test.isTrue(inputs1[2] === inputs3[2]);
  
    div.kill();
    Deps.flush();
  });
  
  Tinytest.add("spark - update defunct range", function (test) {
    // Test that Spark doesn't freak out if it tries to update
    // a LiveRange on nodes that have been taken out of the document.
    //
    // See https://github.com/meteor/meteor/issues/392.
  
    var R = ReactiveVar("foo");
  
    var div = OnscreenDiv(Spark.render(function () {
      return "<p>" + Spark.isolate(function() {
        return R.get();
      }) + "</p>";
    }));
  
    test.equal(div.html(), "<p>foo</p>");
    R.set("bar");
    Deps.flush();
    test.equal(R.numListeners(), 1);
    test.equal(div.html(), "<p>bar</p>");
    test.equal(div.node().firstChild.nodeName, "P");
    div.node().firstChild.innerHTML = '';
    R.set("baz");
    Deps.flush(); // should throw no errors
    // will be 1 if our isolate func was run.
    test.equal(R.numListeners(), 0);
  
    /////
  
    R = ReactiveVar("foo");
  
    div = OnscreenDiv(Spark.render(function () {
      return "<p>" + Spark.setDataContext(
        {},
        "<span>" + Spark.isolate(function() {
          return R.get();
        }) + "</span>") + "</p>";
    }));
  
    test.equal(div.html(), "<p><span>foo</span></p>");
    R.set("bar");
    Deps.flush();
    test.equal(R.numListeners(), 1);
    test.equal(div.html(), "<p><span>bar</span></p>");
    test.equal(div.node().firstChild.nodeName, "P");
    div.node().firstChild.innerHTML = '';
    R.set("baz");
    Deps.flush(); // should throw no errors
    // will be 1 if our isolate func was run.
    test.equal(R.numListeners(), 0);
  
  });
  
  })();
  