From 4fcd32766863b0c692d22a3dda2a385797918e66 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Wed, 1 Aug 2012 21:13:55 -0700 Subject: [PATCH] attachEvents works! --- packages/liveui/liveui_tests.js | 502 +++++-------------------------- packages/spark/spark.js | 155 +++++++--- packages/spark/spark_tests.js | 517 ++++++++++++++++++++++++++++---- 3 files changed, 642 insertions(+), 532 deletions(-) diff --git a/packages/liveui/liveui_tests.js b/packages/liveui/liveui_tests.js index bb52d37d8d..66d22c449f 100644 --- a/packages/liveui/liveui_tests.js +++ b/packages/liveui/liveui_tests.js @@ -29,6 +29,78 @@ var legacyLabels = { ///// TESTS ///// +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; +}; + +Tinytest.add("liveui - listChunk 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.observe = function(callbacks) { + lst.callbacks = callbacks; + return { + stop: function() { + lst.callbacks = null; + } + }; + }; + div = OnscreenDiv(Meteor.ui.render(function() { + var chkbx = function(doc) { + return ''+(doc ? doc._id : 'else'); + }; + return '

'+ + Meteor.ui.listChunk(lst, chkbx, chkbx, + {events: eventmap('click input', event_buf), + event_data:event_buf}) + + '

'; + }, { events: eventmap('change b', 'change input', event_buf), + event_data:event_buf })); + Meteor.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.added(lst[0], 0); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'foo'); + doClick(); + // remove item, back to "else" case + lst.callbacks.removed(lst[0], 0); + lst.pop(); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'else'); + doClick(); + // cleanup + div.kill(); + Meteor.flush(); + +}); Tinytest.add("liveui - tables", function(test) { @@ -756,437 +828,7 @@ Tinytest.add("liveui - events on preserved nodes", function(test) { Meteor.flush(); }); -Tinytest.add("liveui - basic 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.ui.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"); - Meteor.flush(); - test.equal(this.div.html(), "three four five six"); - }); - - // work inside a table - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a tbody - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a tr - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a ul - - do_onscreen(function() { - R = ReactiveVar("
  • HI!
  • "); - this.render(function() { - return ""; - }); - - test.equal($(this.node()).find("#morphing li").text(), "HI!"); - R.set("
  • BUH BYE!
  • "); - Meteor.flush(); - test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!"); - }); - - // work inside a select - - do_onscreen(function() { - R = ReactiveVar(""); - this.render(function() { - return ""; - }); - - test.equal($(this.node()).find("#morphing option").text(), "HI!"); - R.set(""); - Meteor.flush(); - test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!"); - }); - -}); - -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; -}; - -Tinytest.add("liveui - event handling", function(test) { - var event_buf = []; - var getid = function(id) { - return document.getElementById(id); - }; - - var div; - - // clicking on a div at top level - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click']); - div.kill(); - Meteor.flush(); - - // selector that specifies a top-level div - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click div"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click div']); - div.kill(); - Meteor.flush(); - - // selector that specifies a second-level span - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click span"), event_data:event_buf})); - clickElement(getid("foozy").firstChild); - test.equal(event_buf, ['click span']); - div.kill(); - Meteor.flush(); - - // replaced top-level elements still have event handlers - // even if not replaced by the chunk wih the handlers - var R = ReactiveVar("p"); - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return '<'+R.get()+' id="foozy">Hello'; - }); - }, {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 - Meteor.flush(); - clickElement(getid("foozy")); // still clickable? - test.equal(event_buf, ['click']); - event_buf.length = 0; - R.set("p"); - Meteor.flush(); - clickElement(getid("foozy")); - test.equal(event_buf, ['click']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // bubbling from event on descendent of element matched - // by selector - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo'+ - 'Bar
    '; - }, {events: eventmap("click span"), event_data:event_buf})); - clickElement( - getid("foozy").firstChild.firstChild.firstChild); - test.equal(event_buf, ['click span']); - div.kill(); - Meteor.flush(); - - // bubbling order (for same event, same render node, different selector nodes) - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo'+ - 'Bar
    '; - }, {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(); - Meteor.flush(); - - // "bubbling" order for handlers at same level - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {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(); - Meteor.flush(); - - // stopPropagation doesn't prevent other event maps from - // handling same node - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {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(); - Meteor.flush(); - - // stopImmediatePropagation DOES - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {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(); - Meteor.flush(); - - // bubbling continues even with DOM change - event_buf.length = 0; - R = ReactiveVar(true); - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return '
    '+(R.get()?'abcd':'')+'
    '; - }, {events: { 'click span': function() { - event_buf.push('click span'); - R.set(false); - Meteor.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(); - Meteor.flush(); - - // "deep reach" from high node down to replaced low node. - // Tests that attach_secondary_events actually does the - // right thing in IE. Also tests change event bubbling - // and proper interpretation of event maps. - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return '

    '+ - Meteor.ui.chunk(function() { - return ''+R.get(); - }, {events: eventmap('click input'), event_data:event_buf}) + - '

    '; - }, { events: eventmap('change b', 'change input'), event_data:event_buf })); - R.set('bar'); - Meteor.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(); - Meteor.flush(); - - // same thing, but with events wired by listChunk "added" and "removed" - event_buf.length = 0; - var lst = []; - lst.observe = function(callbacks) { - lst.callbacks = callbacks; - return { - stop: function() { - lst.callbacks = null; - } - }; - }; - div = OnscreenDiv(Meteor.ui.render(function() { - var chkbx = function(doc) { - return ''+(doc ? doc._id : 'else'); - }; - return '

    '+ - Meteor.ui.listChunk(lst, chkbx, chkbx, - {events: eventmap('click input', event_buf), - event_data:event_buf}) + - '

    '; - }, { events: eventmap('change b', 'change input', event_buf), - event_data:event_buf })); - Meteor.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.added(lst[0], 0); - Meteor.flush(); - test.equal(div.text().match(/\S+/)[0], 'foo'); - doClick(); - // remove item, back to "else" case - lst.callbacks.removed(lst[0], 0); - lst.pop(); - Meteor.flush(); - test.equal(div.text().match(/\S+/)[0], 'else'); - doClick(); - // cleanup - div.kill(); - Meteor.flush(); - - // test that 'click *' fires on bubble - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return '

    '+ - Meteor.ui.chunk(function() { - return ''+R.get(); - }, {events: eventmap('click input'), event_data:event_buf}) + - '

    '; - }, { events: eventmap('click *'), event_data:event_buf })); - R.set('bar'); - Meteor.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(); - Meteor.flush(); - - // clicking on a div in a nested chunk (without patching) - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return R.get() + Meteor.ui.chunk(function() { - return 'ism'; - }, {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'); - Meteor.flush(); - test.equal(div.text(), 'barism'); - clickElement(div.node().getElementsByTagName('SPAN')[0]); - test.equal(event_buf, ['click']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // Test that reactive fragments manually inserted inside - // a reactive fragment eventually get wired. - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return "
    "; - }, { events: eventmap("click span", event_buf) })); - Meteor.flush(); - div.node().firstChild.appendChild(Meteor.ui.render(function() { - return 'hello'; - })); - clickElement(getid("foozy")); - // implementation has no way to know we've inserted the fragment - test.equal(event_buf, []); - event_buf.length = 0; - Meteor.flush(); - clickElement(getid("foozy")); - // now should be wired up - test.equal(event_buf, ['click span']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // Event data comes from event.currentTarget, not event.target - var data_buf = []; - div = OnscreenDiv(Meteor.ui.render(function() { - return ""; - }, { event_data: {x:'ulstuff'}, - events: { 'click ul': function() { data_buf.push(this); }}})); - clickElement(getid("funyard")); - test.equal(data_buf, [{x:'ulstuff'}]); - div.kill(); - Meteor.flush(); -}); Tinytest.add("liveui - cleanup", function(test) { diff --git a/packages/spark/spark.js b/packages/spark/spark.js index fa86ff3f1f..6f054174d7 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -4,13 +4,39 @@ Spark = {}; Spark._currentRenderer = new Meteor.EnvironmentVariable; +Spark._TAG = "_spark_"+Meteor.uuid(); // XXX document contract for each type of annotation? -Spark._ANNOTATION_DATA = "_spark_data"; -Spark._ANNOTATION_ISOLATE = "_spark_isolate"; -Spark._ANNOTATION_EVENTS = "_spark_events"; -Spark._ANNOTATION_WATCH = "_spark_watch"; -Spark._ANNOTATIONS = [Spark._ANNOTATION_DATA, Spark._ANNOTATION_ISOLATE, - Spark._ANNOTATION_EVENTS, Spark._ANNOTATION_WATCH]; +Spark._ANNOTATION_NOTIFY = "notify"; +Spark._ANNOTATION_DATA = "data"; +Spark._ANNOTATION_ISOLATE = "isolate"; +Spark._ANNOTATION_EVENTS = "events"; +Spark._ANNOTATION_WATCH = "watch"; + +// Set in tests to turn on extra UniversalEventListener sanity checks +Spark._checkIECompliance = false; + +var makeRange = function(type, start, end, inner) { + var range = new LiveRange(Spark._TAG, start, end, inner); + range.type = type; + return range; +}; + +var findRangeOfType = function(type, node) { + var range = LiveRange.findRange(Spark._TAG, node); + while (range && range.type !== type) + range = range.findParent(); + + return range; +}; + +var findParentOfType = function (type, range) { + do { + range = range.findParent(); + } while (range && range.type !== type); + + return range; +}; + Spark._Renderer = function () { // Map from annotation ID to an annotation function, which is called @@ -39,12 +65,12 @@ _.extend(Spark._Renderer.prototype, { // `what` can be a function that takes a LiveRange, or just a set of // attributes to add to the liverange. tag and what are optional. // if no tag is passed, no liverange will be created. - annotate: function (html, tag, what) { - var id = tag + "-" + this.createId(); + annotate: function (html, type, what) { + var id = type + "-" + this.createId(); this.annotations[id] = function (start, end) { - if (! tag) + if (! type) return; - var range = new LiveRange(tag, start, end); + var range = makeRange(type, start, end); if (what instanceof Function) what(range); else @@ -60,7 +86,34 @@ _.extend(Spark._Renderer.prototype, { Spark.render = function (htmlFunc) { var renderer = new Spark._Renderer; var html = Spark._currentRenderer.withValue(renderer, function () { - return renderer.annotate(htmlFunc()); + return renderer.annotate( + htmlFunc(), Spark._ANNOTATION_NOTIFY, function (range) { + // This is a heuristic to benefit users that manually insert + // nodes into regions of the DOM that were rendered by Spark + // and have event maps. The heuristic is: at every flush, look + // for any nodes that were rendered by Spark since the last + // flush, and call notifyWatchers on them so that any + // *enclosing* event maps can wire up event handlers on the + // newly inserted nodes. + // + // This heuristic is only of relevance on IE6-8 and only if + // the user manually inserts nodes in the DOM (eg, through + // jQuery or appendChild). We're not sure it's the right thing + // and it could be removed at any time. + var finalized = false; + range.finalize = function () { + finalized = true; + }; + + var ctx = new Meteor.deps.Context; + ctx.on_invalidate(function () { + if (!finalized) { + notifyWatchers(range.firstNode(), range.lastNode()); + range.destroy(); + } + }); + ctx.invalidate(); + }); }); var fragById = {}; @@ -145,8 +198,7 @@ Spark.setDataContext = withRenderer(function (dataContext, html, _renderer) { }); Spark.getDataContext = function (node) { - var range = LiveRange.findRange( - Spark._ANNOTATION_DATA, node); + var range = findRangeOfType(Spark._ANNOTATION_DATA, node); return range && range.data; }; @@ -155,20 +207,24 @@ var getListener = function () { if (!universalListener) universalListener = new UniversalEventListener(function (event) { // Handle a currently-propagating event on a particular node. - // We walk all enclosing liveranges of the node, from the inside - // out, looking for matching handlers. If the app calls - // stopPropagation(), we still call all handlers in all event - // maps for the current node. If the app calls - // "stopImmediatePropagation()", we don't call any more - // handlers. + // We walk each enclosing liverange of the node and offer it the + // chance to handle the event. It's range.handler's + // responsibility to check isImmediatePropagationStopped() + // before delivering events to the user. We precompute the list + // of enclosing liveranges to defend against the case where user + // event handlers change the DOM. - var range = LiveRange.findRange(Spark._ANNOTATION_EVENTS, - event.currentTarget); - while (range && !event.isImmediatePropagationStopped()) { - range.handler(event); - range = range.findParent(); + var ranges = []; + var walk = findRangeOfType(Spark._ANNOTATION_EVENTS, + event.currentTarget); + while (walk) { + ranges.push(walk); + walk = findParentOfType(Spark._ANNOTATION_EVENTS, walk); } - }); + _.each(ranges, function (r) { + r.handler(event); + }); + }, Spark._checkIECompliance); return universalListener; }; @@ -214,6 +270,8 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { } }); + var finalized = false; + html = _renderer.annotate( html, Spark._ANNOTATION_EVENTS, function (range) { _.each(eventTypes, function (t) { @@ -221,10 +279,17 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { }); installHandlers(range); + range.finalize = function () { + finalized = true; + }; + range.handler = function (event) { var handlers = handlerMap[event.type] || []; for (var i = 0; i < handlers.length; i++) { + if (finalized || event.isImmediatePropagationStopped()) + return; + var handler = handlers[i]; var callback = handler.callback; var selector = handler.selector; @@ -246,8 +311,12 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { continue; } - // Found a matching handler. + // Found a matching handler. Call it. var eventData = Spark.getDataContext(event.currentTarget); + // Note that the handler can do arbitrary things, like call + // Meteor.flush() or otherwise remove and finalize parts of + // the DOM. We can't assume `range` is valid past this point, + // and we'll check the `finalized` flag at the top of the loop. var returnValue = callback.call(eventData, event); // allow app to `return false` from event handler, just like @@ -256,8 +325,6 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { event.stopImmediatePropagation(); event.preventDefault(); } - if (event.isImmediatePropagationStopped()) - break; // don't let any other handlers in this event map fire } }; }); @@ -309,8 +376,8 @@ Spark.isolate = function (htmlFunc) { return Spark.isolate(htmlFunc); }); - var tempRange = new LiveRange(Spark._ANNOTATION_ISOLATE, frag, null, - true /* inner */); + var tempRange = makeRange(Spark._ANNOTATION_ISOLATE, frag, null, + true /* inner */); tempRange.operate(function (start, end) { // Wrap contents of frag, *inside* the ISOLATE annotation, // as appropriate for insertion into `range`. We want the @@ -324,7 +391,7 @@ Spark.isolate = function (htmlFunc) { var oldContents = range.replace_contents(frag); // XXX should patch Spark.finalize(oldContents); - notifyWatchers(range); + notifyWatchers(range.firstNode(), range.lastNode()); range.destroy(); }); }); @@ -332,18 +399,12 @@ Spark.isolate = function (htmlFunc) { return html; }; -var notifyWatchers = function (range) { - // find the innermost WATCH annotation containing the nodes in `range` - var tempRange = new LiveRange(Spark._ANNOTATION_WATCH, range.firstNode(), - range.lastNode(), true /* innermost */); - var walk = tempRange.findParent(); +var notifyWatchers = function (start, end) { + var tempRange = new LiveRange(Spark._TAG, start, end, true /* innermost */); + for (var walk = tempRange; walk; walk = walk.findParent()) + if (walk.type === Spark._ANNOTATION_WATCH) + walk.notify(); tempRange.destroy(); - - // tell all enclosing WATCH annotations that their contents changed - while (walk) { - walk.notify(); - walk = walk.findParent(); - } }; // Delete all of the liveranges in the range of nodes between `start` @@ -358,13 +419,11 @@ Spark.finalize = function (start, end) { start = frag; end = null; } - _.each(Spark._ANNOTATIONS, function (tag) { - var wrapper = new LiveRange(tag, start, end); - wrapper.visit(function (isStart, range) { - isStart && range.finalize && range.finalize(); - }); - wrapper.destroy(true /* recursive */); + var wrapper = new LiveRange(Spark._TAG, start, end); + wrapper.visit(function (isStart, range) { + isStart && range.finalize && range.finalize(); }); + wrapper.destroy(true /* recursive */); }; })(); diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js index daaa158487..f911510d6b 100644 --- a/packages/spark/spark_tests.js +++ b/packages/spark/spark_tests.js @@ -1,15 +1,19 @@ -// XXX when testing spark, set the checkIECompliance flag on universal-events somehow +// XXX make sure that when tests use id="..." to trigger patching, "preserve" happens + +Spark._checkIECompliance = true; + +(function () { Tinytest.add("spark - assembly", function (test) { - var doTest = function(calc) { - var frag = Spark.render(function() { - return calc(function(str, expected) { + 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) { + var html = calc(function (str, expected, noRange) { if (arguments.length > 1) str = expected; if (! noRange) @@ -20,41 +24,41 @@ Tinytest.add("spark - assembly", function (test) { test.equal(f.html(), html); var actualGroups = []; - var tempRange = new LiveRange(Spark._ANNOTATION_DATA, frag); - tempRange.visit(function(isStart, rng) { - if (! isStart) + var tempRange = new LiveRange(Spark._TAG, frag); + tempRange.visit(function (isStart, rng) { + if (! isStart && rng.type === Spark._ANNOTATION_DATA) actualGroups.push(rangeToHtml(rng)); }); test.equal(actualGroups.join(','), groups.join(',')); }; - doTest(function(A) { return "

    Hello

    "; }); - doTest(function(A) { return "HelloWorld"; }); - doTest(function(A) { return ""+A("Hello")+""; }); - doTest(function(A) { return A(""+A("Hello")+""); }); - doTest(function(A) { return A(A(A(A(A(A("foo")))))); }); + doTest(function (A) { return "

    Hello

    "; }); + doTest(function (A) { return "HelloWorld"; }); + doTest(function (A) { return ""+A("Hello")+""; }); + doTest(function (A) { return A(""+A("Hello")+""); }); + doTest(function (A) { return A(A(A(A(A(A("foo")))))); }); doTest( - function(A) { return "
    Yo"+A("

    Hello "+A(A("World")),"

    Hello World

    ")+ + function (A) { return "
    Yo"+A("

    Hello "+A(A("World")),"

    Hello World

    ")+ "
    "; }); - doTest(function(A) { + doTest(function (A) { return A("