Files
meteor/packages/spacebars-tests/template_tests.js
David Greenspan ab92f117ae Expand fix for #3130
If you Blaze.remove a View that is a template rendered by Blaze.renderWithData, or included with an implicit “with” as in `{{> myTemplate someData}}`, Blaze will now remove the DOM of the template, and also remove the implicit “with” (in both cases).

As background, Blaze.remove only works on Views that were attached directly under a DOM element, not inside another View.  Blaze.render always attaches the resulting View directly under a DOM element, but Blaze.renderWithData creates a “with” View around the template View.  Previously, you could Blaze.remove the “with” View (which is returned by renderWithData), but if you got access to the template’s View some other way and tried to remove it directly, nothing would happen.  Now, the correct thing happens (the View is destroyed and the DOM is removed).

In the future, we should consider whether Blaze.remove should work on arbitrary Views, not just Views attached under a DOM element.
2014-12-09 13:17:12 -08:00

3067 lines
93 KiB
JavaScript

var divRendersTo = function (test, div, html) {
Tracker.flush({_throwFirstError: true});
var actual = canonicalizeHtml(div.innerHTML);
test.equal(actual, html);
};
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);
};
var clickIt = function (elem) {
// jQuery's bubbling change event polyfill for IE 8 seems
// to require that the element in question have focus when
// it receives a simulated click.
if (elem.focus)
elem.focus();
clickElement(elem);
};
// maybe use created callback on the template instead of this?
var extendTemplateWithInit = function (template, initFunc) {
var tmpl = new Template(template.viewName+'-extended', template.renderFunction);
tmpl.constructView = function (/*args*/) {
var view = Template.prototype.constructView.apply(this, arguments);
initFunc(view);
return view;
};
return tmpl;
};
// Make a "clone" of origTemplate (but not its helpers)
var copyTemplate = function (origTemplate) {
return new Template(origTemplate.viewName, origTemplate.renderFunction);
};
Tinytest.add("spacebars-tests - template_tests - simple helper", function (test) {
var baseTmpl = Template.spacebars_template_test_simple_helper;
var tmpl1 = copyTemplate(baseTmpl);
var R = ReactiveVar(1);
tmpl1.helpers({
foo: function (x) {
return x + R.get();
},
bar: function () {
return 123;
}
});
var div = renderToDiv(tmpl1);
test.equal(canonicalizeHtml(div.innerHTML), "124");
R.set(2);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "125");
// Test that `{{foo bar}}` throws if `foo` is missing or not a function.
var tmpl2 = copyTemplate(baseTmpl);
tmpl2.helpers({foo: 3});
test.throws(function () {
renderToDiv(tmpl2);
}, /Can't call non-function/);
var tmpl3 = copyTemplate(baseTmpl);
test.throws(function () {
renderToDiv(tmpl3);
}, /No such function/);
var tmpl4 = copyTemplate(baseTmpl);
tmpl4.helpers({foo: function () {}});
// doesn't throw
div = renderToDiv(tmpl4);
test.equal(canonicalizeHtml(div.innerHTML), '');
// now make "foo" is a function in the data context
var tmpl5 = copyTemplate(baseTmpl);
tmpl5.helpers({
bar: function () {
return 123;
}
});
R = ReactiveVar(1);
div = renderToDiv(tmpl5, { foo: function (x) {
return x + R.get();
} });
test.equal(canonicalizeHtml(div.innerHTML), "124");
R.set(2);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "125");
test.throws(function () {
renderToDiv(tmpl5, {foo: 3});
}, /Can't call non-function/);
test.throws(function () {
renderToDiv(tmpl5, {foo: null});
}, /No such function/);
test.throws(function () {
renderToDiv(tmpl5, {});
}, /No such function/);
});
Tinytest.add("spacebars-tests - template_tests - dynamic template", function (test) {
var tmpl = Template.spacebars_template_test_dynamic_template;
var aaa = Template.spacebars_template_test_aaa;
var bbb = Template.spacebars_template_test_bbb;
var R = ReactiveVar("aaa");
tmpl.helpers({foo: function () {
return R.get() === 'aaa' ? aaa : bbb;
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "aaa");
R.set('bbb');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "bbb");
});
Tinytest.add("spacebars-tests - template_tests - interpolate attribute", function (test) {
var tmpl = Template.spacebars_template_test_interpolate_attribute;
tmpl.helpers({
foo: function (x) {
return x+1;
},
bar: function () {
return 123;
}
});
var div = renderToDiv(tmpl);
test.equal($(div).find('div')[0].className, "aaa124zzz");
});
Tinytest.add("spacebars-tests - template_tests - dynamic attrs", function (test) {
var tmpl = Template.spacebars_template_test_dynamic_attrs;
var R2 = ReactiveVar({x: "X"});
var R3 = ReactiveVar('selected');
tmpl.helpers({
attrsObj: function () { return R2.get(); },
singleAttr: function () { return R3.get(); }
});
var div = renderToDiv(tmpl);
var span = $(div).find('span')[0];
test.equal(span.innerHTML, 'hi');
test.isTrue(span.hasAttribute('selected'));
test.equal(span.getAttribute('x'), 'X');
R2.set({y: "Y", z: "Z"});
R3.set('');
Tracker.flush();
test.equal(canonicalizeHtml(span.innerHTML), 'hi');
test.isFalse(span.hasAttribute('selected'));
test.isFalse(span.hasAttribute('x'));
test.equal(span.getAttribute('y'), 'Y');
test.equal(span.getAttribute('z'), 'Z');
});
Tinytest.add("spacebars-tests - template_tests - triple", function (test) {
var tmpl = Template.spacebars_template_test_triple;
var R = ReactiveVar('<span class="hi">blah</span>');
tmpl.helpers({
html: function () { return R.get(); }
});
var div = renderToDiv(tmpl);
var elems = $(div).find("> *");
test.equal(elems.length, 1);
test.equal(elems[0].nodeName, 'SPAN');
var span = elems[0];
test.equal(span.className, 'hi');
test.equal(span.innerHTML, 'blah');
R.set('asdf');
Tracker.flush();
elems = $(div).find("> *");
test.equal(elems.length, 0);
test.equal(canonicalizeHtml(div.innerHTML), 'asdf');
R.set('<span class="hi">blah</span>');
Tracker.flush();
elems = $(div).find("> *");
test.equal(elems.length, 1);
test.equal(elems[0].nodeName, 'SPAN');
span = elems[0];
test.equal(span.className, 'hi');
test.equal(canonicalizeHtml(span.innerHTML), 'blah');
var tmpl = Template.spacebars_template_test_triple2;
tmpl.helpers({
html: function () {},
html2: function () { return null; }
});
// no tmpl.html3
div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'xy');
});
Tinytest.add("spacebars-tests - template_tests - inclusion args", function (test) {
var tmpl = Template.spacebars_template_test_inclusion_args;
var R = ReactiveVar(Template.spacebars_template_test_aaa);
tmpl.helpers({foo: function () { return R.get(); }});
var div = renderToDiv(tmpl);
// `{{> foo bar}}`, with `foo` resolving to Template.aaa,
// which consists of "aaa"
test.equal(canonicalizeHtml(div.innerHTML), 'aaa');
R.set(Template.spacebars_template_test_bbb);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'bbb');
////// Ok, now `foo` *is* Template.aaa
tmpl.helpers({foo: Template.spacebars_template_test_aaa});
div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'aaa');
////// Ok, now `foo` is a template that takes an argument; bar is a string.
tmpl.helpers({
foo: Template.spacebars_template_test_bracketed_this,
bar: 'david'
});
div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '[david]');
////// Now `foo` is a template that takes an arg; bar is a function.
tmpl.helpers({foo: Template.spacebars_template_test_span_this});
R = ReactiveVar('david');
tmpl.helpers({bar: function () { return R.get(); }});
div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '<span>david</span>');
var span1 = div.querySelector('span');
R.set('avi');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span>avi</span>');
var span2 = div.querySelector('span');
test.isTrue(span1 === span2);
});
Tinytest.add("spacebars-tests - template_tests - inclusion args 2", function (test) {
// `{{> foo bar q=baz}}`
var tmpl = Template.spacebars_template_test_inclusion_args2;
tmpl.helpers({
foo: Template.spacebars_template_test_span_this,
bar: function (options) {
return options.hash.q;
}
});
var R = ReactiveVar('david!');
tmpl.helpers({ baz: function () { return R.get().slice(0,5); } });
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '<span>david</span>');
var span1 = div.querySelector('span');
R.set('brillo');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span>brill</span>');
var span2 = div.querySelector('span');
test.isTrue(span1 === span2);
});
Tinytest.add("spacebars-tests - template_tests - inclusion dotted args", function (test) {
// `{{> foo bar.baz}}`
var tmpl = Template.spacebars_template_test_inclusion_dotted_args;
var initCount = 0;
tmpl.helpers({
foo: extendTemplateWithInit(
Template.spacebars_template_test_bracketed_this,
function () { initCount++; })
});
var R = ReactiveVar('david');
tmpl.helpers({
bar: function () {
// make sure `this` is bound correctly
return { baz: this.symbol + R.get() };
}
});
var div = renderToDiv(tmpl, {symbol:'%'});
test.equal(initCount, 1);
test.equal(canonicalizeHtml(div.innerHTML), '[%david]');
R.set('avi');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '[%avi]');
// check that invalidating the argument to `foo` doesn't require
// creating a new `foo`.
test.equal(initCount, 1);
});
Tinytest.add("spacebars-tests - template_tests - inclusion slashed args", function (test) {
// `{{> foo bar/baz}}`
var tmpl = Template.spacebars_template_test_inclusion_dotted_args;
var initCount = 0;
tmpl.helpers({foo: extendTemplateWithInit(
Template.spacebars_template_test_bracketed_this,
function () { initCount++; }) });
var R = ReactiveVar('david');
tmpl.helpers({bar: function () {
// make sure `this` is bound correctly
return { baz: this.symbol + R.get() };
}});
var div = renderToDiv(tmpl, {symbol:'%'});
test.equal(initCount, 1);
test.equal(canonicalizeHtml(div.innerHTML), '[%david]');
});
Tinytest.add("spacebars-tests - template_tests - block helper", function (test) {
// test the case where `foo` is a calculated template that changes
// reactively.
// `{{#foo}}bar{{else}}baz{{/foo}}`
var tmpl = Template.spacebars_template_test_block_helper;
var R = ReactiveVar(Template.spacebars_template_test_content);
tmpl.helpers({foo: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "bar");
R.set(Template.spacebars_template_test_elsecontent);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "baz");
});
Tinytest.add("spacebars-tests - template_tests - block helper function with one string arg", function (test) {
// `{{#foo "bar"}}content{{/foo}}`
var tmpl = Template.spacebars_template_test_block_helper_function_one_string_arg;
tmpl.helpers({foo: function () {
if (String(this) === "bar")
return Template.spacebars_template_test_content;
else
return null;
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "content");
});
Tinytest.add("spacebars-tests - template_tests - block helper function with one helper arg", function (test) {
var tmpl = Template.spacebars_template_test_block_helper_function_one_helper_arg;
var R = ReactiveVar("bar");
tmpl.helpers({
bar: function () { return R.get(); },
foo: function () {
if (String(this) === "bar")
return Template.spacebars_template_test_content;
else
return null;
}
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "content");
R.set("baz");
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "");
});
Tinytest.add("spacebars-tests - template_tests - block helper component with one helper arg", function (test) {
var tmpl = Template.spacebars_template_test_block_helper_component_one_helper_arg;
var R = ReactiveVar(true);
tmpl.helpers({bar: function () { return R.get(); }});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "content");
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "");
});
Tinytest.add("spacebars-tests - template_tests - block helper component with three helper args", function (test) {
var tmpl = Template.spacebars_template_test_block_helper_component_three_helper_args;
var R = ReactiveVar("bar");
tmpl.helpers({
bar_or_baz: function () {
return R.get();
},
equals: function (x, y) {
return x === y;
}
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "content");
R.set("baz");
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "");
});
Tinytest.add("spacebars-tests - template_tests - block helper with dotted arg", function (test) {
var tmpl = Template.spacebars_template_test_block_helper_dotted_arg;
var R1 = ReactiveVar(1);
var R2 = ReactiveVar(10);
var R3 = ReactiveVar(100);
var initCount = 0;
tmpl.helpers({
foo: extendTemplateWithInit(
Template.spacebars_template_test_bracketed_this,
function () { initCount++; }),
bar: function () {
return {
r1: R1.get(),
baz: function (r3) {
return this.r1 + R2.get() + r3;
}
};
},
qux: function () { return R3.get(); }
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "[111]");
test.equal(initCount, 1);
R1.set(2);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[112]");
test.equal(initCount, 1);
R2.set(20);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[122]");
test.equal(initCount, 1);
R3.set(200);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[222]");
test.equal(initCount, 1);
R2.set(30);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[232]");
test.equal(initCount, 1);
R1.set(3);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[233]");
test.equal(initCount, 1);
R3.set(300);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "[333]");
test.equal(initCount, 1);
});
Tinytest.add("spacebars-tests - template_tests - nested content", function (test) {
// Test that `{{> Template.contentBlock}}` in an `{{#if}}` works.
// ```
// <template name="spacebars_template_test_iftemplate">
// {{#if condition}}
// {{> Template.contentBlock}}
// {{else}}
// {{> Template.elseBlock}}
// {{/if}}
// </template>
// ```
// ```
// {{#spacebars_template_test_iftemplate flag}}
// hello
// {{else}}
// world
// {{/spacebars_template_test_iftemplate}}
// ```
var tmpl = Template.spacebars_template_test_nested_content;
var R = ReactiveVar(true);
tmpl.helpers({
flag: function () {
return R.get();
}
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'world');
R.set(true);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
// Also test that `{{> Template.contentBlock}}` in a custom block helper works.
tmpl = Template.spacebars_template_test_nested_content2;
R = ReactiveVar(true);
tmpl.helpers({
x: function () {
return R.get();
}
});
div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'world');
R.set(true);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
});
Tinytest.add("spacebars-tests - template_tests - if", function (test) {
var tmpl = Template.spacebars_template_test_if;
var R = ReactiveVar(true);
tmpl.helpers({
foo: function () {
return R.get();
},
bar: 1,
baz: 2
});
var div = renderToDiv(tmpl);
var rendersTo = function (html) { divRendersTo(test, div, html); };
rendersTo("1");
R.set(false);
rendersTo("2");
});
Tinytest.add("spacebars-tests - template_tests - if in with", function (test) {
var tmpl = Template.spacebars_template_test_if_in_with;
tmpl.helpers({foo: {bar: "bar"}});
var div = renderToDiv(tmpl);
divRendersTo(test, div, "bar bar");
});
Tinytest.add("spacebars-tests - template_tests - each on cursor", function (test) {
var tmpl = Template.spacebars_template_test_each;
var coll = new Mongo.Collection(null);
tmpl.helpers({
items: function () {
return coll.find({}, {sort: {pos: 1}});
}
});
var div = renderToDiv(tmpl);
var rendersTo = function (html) { divRendersTo(test, div, html); };
rendersTo("else-clause");
coll.insert({text: "one", pos: 1});
rendersTo("one");
coll.insert({text: "two", pos: 2});
rendersTo("one two");
coll.update({text: "two"}, {$set: {text: "three"}});
rendersTo("one three");
coll.update({text: "three"}, {$set: {pos: 0}});
rendersTo("three one");
coll.remove({});
rendersTo("else-clause");
});
Tinytest.add("spacebars-tests - template_tests - each on array", function (test) {
var tmpl = Template.spacebars_template_test_each;
var R = new ReactiveVar([]);
tmpl.helpers({
items: function () {
return R.get();
},
text: function () {
return this;
}
});
var div = renderToDiv(tmpl);
var rendersTo = function (html) { divRendersTo(test, div, html); };
rendersTo("else-clause");
R.set([""]);
rendersTo("");
R.set(["x", "", "toString"]);
rendersTo("x toString");
R.set(["toString"]);
rendersTo("toString");
R.set([]);
rendersTo("else-clause");
R.set([0, 1, 2]);
rendersTo("0 1 2");
R.set([]);
rendersTo("else-clause");
});
Tinytest.add("spacebars-tests - template_tests - ..", function (test) {
var tmpl = Template.spacebars_template_test_dots;
Template.spacebars_template_test_dots_subtemplate.helpers({
getTitle: function (from) {
return from.title;
}
});
tmpl.helpers({
foo: {
title: "foo",
bar: {title: "bar",
items: [{title: "item"}]}
}
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), [
"A", "B", "C", "D",
// {{> spacebars_template_test_dots_subtemplate}}
"TITLE", "1item", "2item", "3bar", "4foo", "GETTITLE", "5item", "6bar", "7foo",
// {{> spacebars_template_test_dots_subtemplate ..}}
"TITLE", "1bar", "2bar", "3item", "4bar", "GETTITLE", "5bar", "6item", "7bar"].join(" "));
});
Tinytest.add("spacebars-tests - template_tests - select tags", function (test) {
var tmpl = Template.spacebars_template_test_select_tag;
// {label: (string)}
var optgroups = new Mongo.Collection(null);
// {optgroup: (id), value: (string), selected: (boolean), label: (string)}
var options = new Mongo.Collection(null);
tmpl.helpers({
optgroups: function () { return optgroups.find(); },
options: function () { return options.find({optgroup: this._id}); },
selectedAttr: function () { return this.selected ? {selected: true} : {}; }
});
var div = renderToDiv(tmpl);
var selectEl = $(div).find('select')[0];
// returns canonicalized contents of `div` in the form eg
// ["<select>", "</select>"]. strip out selected attributes -- we
// verify correctness by observing the `selected` property
var divContent = function () {
return canonicalizeHtml(
div.innerHTML.replace(/selected="[^"]*"/g, '').replace(/selected/g, ''))
.replace(/\>\s*\</g, '>\n<')
.split('\n');
};
test.equal(divContent(), ["<select>", "</select>"]);
var optgroup1 = optgroups.insert({label: "one"});
var optgroup2 = optgroups.insert({label: "two"});
test.equal(divContent(), [
'<select>',
'<optgroup label="one">',
'</optgroup>',
'<optgroup label="two">',
'</optgroup>',
'</select>'
]);
options.insert({optgroup: optgroup1, value: "value1", selected: false, label: "label1"});
options.insert({optgroup: optgroup1, value: "value2", selected: true, label: "label2"});
test.equal(divContent(), [
'<select>',
'<optgroup label="one">',
'<option value="value1">label1</option>',
'<option value="value2">label2</option>',
'</optgroup>',
'<optgroup label="two">',
'</optgroup>',
'</select>'
]);
test.equal(selectEl.value, "value2");
test.equal($(selectEl).find('option')[0].selected, false);
test.equal($(selectEl).find('option')[1].selected, true);
// swap selection
options.update({value: "value1"}, {$set: {selected: true}});
options.update({value: "value2"}, {$set: {selected: false}});
Tracker.flush();
test.equal(divContent(), [
'<select>',
'<optgroup label="one">',
'<option value="value1">label1</option>',
'<option value="value2">label2</option>',
'</optgroup>',
'<optgroup label="two">',
'</optgroup>',
'</select>'
]);
test.equal(selectEl.value, "value1");
test.equal($(selectEl).find('option')[0].selected, true);
test.equal($(selectEl).find('option')[1].selected, false);
// change value and label
options.update({value: "value1"}, {$set: {value: "value1.0"}});
options.update({value: "value2"}, {$set: {label: "label2.0"}});
Tracker.flush();
test.equal(divContent(), [
'<select>',
'<optgroup label="one">',
'<option value="value1.0">label1</option>',
'<option value="value2">label2.0</option>',
'</optgroup>',
'<optgroup label="two">',
'</optgroup>',
'</select>'
]);
test.equal(selectEl.value, "value1.0");
test.equal($(selectEl).find('option')[0].selected, true);
test.equal($(selectEl).find('option')[1].selected, false);
// unselect and then select both options. normally, the second is
// selected (since it got selected later). then switch to <select
// multiple="">. both should be selected.
options.update({}, {$set: {selected: false}}, {multi: true});
Tracker.flush();
options.update({}, {$set: {selected: true}}, {multi: true});
Tracker.flush();
test.equal($(selectEl).find('option')[0].selected, false);
test.equal($(selectEl).find('option')[1].selected, true);
selectEl.multiple = true; // allow multiple selection
options.update({}, {$set: {selected: false}}, {multi: true});
Tracker.flush();
options.update({}, {$set: {selected: true}}, {multi: true});
Tracker.flush();
test.equal($(selectEl).find('option')[0].selected, true);
test.equal($(selectEl).find('option')[1].selected, true);
});
Tinytest.add('spacebars-tests - template_tests - {{#with}} falsy; issue #770', function (test) {
Template.test_template_issue770.helpers({
value1: function () { return "abc"; },
value2: function () { return false; }
});
var div = renderToDiv(Template.test_template_issue770);
test.equal(canonicalizeHtml(div.innerHTML),
"abc xxx abc");
});
Tinytest.add("spacebars-tests - template_tests - tricky attrs", function (test) {
var tmpl = Template.spacebars_template_test_tricky_attrs;
var R = ReactiveVar('foo');
tmpl.helpers({
theType: function () { return 'text'; },
theClass: function () { return R.get(); }
});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML).slice(0, 30),
'<input type="text"><input class="foo" type="checkbox">'.slice(0, 30));
R.set('bar');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML),
'<input type="text"><input class="bar" type="checkbox">');
});
Tinytest.add('spacebars-tests - template_tests - no data context', function (test) {
var tmpl = Template.spacebars_template_test_no_data;
// failure is if an exception is thrown here
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'asdf');
});
Tinytest.add('spacebars-tests - template_tests - textarea', function (test) {
var tmpl = Template.spacebars_template_test_textarea;
var R = ReactiveVar('hello');
tmpl.helpers({foo: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
var textarea = div.querySelector('textarea');
test.equal(textarea.value, 'hello');
R.set('world');
Tracker.flush();
test.equal(textarea.value, 'world');
});
Tinytest.add('spacebars-tests - template_tests - textarea 2', function (test) {
var tmpl = Template.spacebars_template_test_textarea2;
var R = ReactiveVar(true);
tmpl.helpers({foo: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
var textarea = div.querySelector('textarea');
test.equal(textarea.value, '</not a tag>');
R.set(false);
Tracker.flush();
test.equal(textarea.value, '<also not a tag>');
R.set(true);
Tracker.flush();
test.equal(textarea.value, '</not a tag>');
});
Tinytest.add('spacebars-tests - template_tests - textarea 3', function (test) {
var tmpl = Template.spacebars_template_test_textarea3;
var R = ReactiveVar('hello');
tmpl.helpers({foo: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
var textarea = div.querySelector('textarea');
test.equal(textarea.id, 'myTextarea');
test.equal(textarea.value, 'hello');
R.set('world');
Tracker.flush();
test.equal(textarea.value, 'world');
});
Tinytest.add('spacebars-tests - template_tests - textarea each', function (test) {
var tmpl = Template.spacebars_template_test_textarea_each;
var R = ReactiveVar(['APPLE', 'BANANA']);
tmpl.helpers({foo: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
var textarea = div.querySelector('textarea');
test.equal(textarea.value, '<not a tag APPLE <not a tag BANANA ');
R.set([]);
Tracker.flush();
test.equal(textarea.value, '<>');
R.set(['CUCUMBER']);
Tracker.flush();
test.equal(textarea.value, '<not a tag CUCUMBER ');
});
// Ensure that one can call `Meteor.defer` within a rendered callback
// triggered by a document insertion that happend in a method stub.
//
// Why do we have this test? Because you generally can't call
// `Meteor.defer` inside a method stub (see
// packages/meteor/timers.js). This test verifies that rendered
// callbacks don't fire synchronously as part of a method stub.
testAsyncMulti('spacebars-tests - template_tests - defer in rendered callbacks', [function (test, expect) {
var tmpl = Template.spacebars_template_test_defer_in_rendered;
var coll = new Mongo.Collection(null);
Meteor.methods({
spacebarsTestInsertEmptyObject: function () {
// cause a new instance of `subtmpl` to be placed in the
// DOM. verify that it's not fired directly within a method
// stub, in which `Meteor.defer` is not allowed.
coll.insert({});
}
});
tmpl.helpers({
items: function () {
return coll.find();
}
});
var subtmpl = Template.spacebars_template_test_defer_in_rendered_subtemplate;
subtmpl.rendered = expect(function () {
// will throw if called in a method stub
Meteor.defer(function () {});
});
var div = renderToDiv(tmpl);
// not defined on the server, but it's fine since the stub does
// the relevant work
Meteor._suppress_log(1);
Meteor.call("spacebarsTestInsertEmptyObject");
}]);
testAsyncMulti('spacebars-tests - template_tests - rendered template is DOM in rendered callbacks', [
function (test, expect) {
var tmpl = Template.spacebars_template_test_aaa;
tmpl.rendered = expect(function () {
test.equal(canonicalizeHtml(div.innerHTML), "aaa");
});
var div = renderToDiv(tmpl);
Tracker.flush();
}
]);
// Test that in:
//
// ```
// {{#with someData}}
// {{foo}} {{bar}}
// {{/with}}
// ```
//
// ... we run `someData` once even if `foo` re-renders.
Tinytest.add('spacebars-tests - template_tests - with someData', function (test) {
var tmpl = Template.spacebars_template_test_with_someData;
var foo = ReactiveVar('AAA');
var someDataRuns = 0;
tmpl.helpers({
someData: function () {
someDataRuns++;
return {};
},
foo: function () {
return foo.get();
},
bar: function () {
return 'YO';
}
});
var div = renderToDiv(tmpl);
test.equal(someDataRuns, 1);
test.equal(canonicalizeHtml(div.innerHTML), 'AAA YO');
foo.set('BBB');
Tracker.flush();
test.equal(someDataRuns, 1);
test.equal(canonicalizeHtml(div.innerHTML), 'BBB YO');
foo.set('CCC');
Tracker.flush();
test.equal(someDataRuns, 1);
test.equal(canonicalizeHtml(div.innerHTML), 'CCC YO');
});
Tinytest.add('spacebars-tests - template_tests - #each stops when rendered element is removed', function (test) {
var tmpl = Template.spacebars_template_test_each_stops;
var coll = new Mongo.Collection(null);
coll.insert({});
tmpl.helpers({items: function () { return coll.find(); }});
var div = renderToDiv(tmpl);
divRendersTo(test, div, 'x');
// trigger #each component destroyed
$(div).remove();
// insert another document. cursor should no longer be observed so
// should have no effect.
coll.insert({});
divRendersTo(test, div, 'x');
});
Tinytest.add('spacebars-tests - template_tests - block helpers in attribute', function (test) {
var tmpl = Template.spacebars_template_test_block_helpers_in_attribute;
var coll = new Mongo.Collection(null);
tmpl.helpers({
classes: function () {
return coll.find({}, {sort: {name: 1}});
},
startsLowerCase: function (name) {
return /^[a-z]/.test(name);
}
});
coll.insert({name: 'David'});
coll.insert({name: 'noodle'});
coll.insert({name: 'donut'});
coll.insert({name: 'frankfurter'});
coll.insert({name: 'Steve'});
var containerDiv = renderToDiv(tmpl);
var div = containerDiv.querySelector('div');
var shouldBe = function (className) {
Tracker.flush();
test.equal(div.innerHTML, "Smurf");
test.equal(div.className, className);
var result = canonicalizeHtml(containerDiv.innerHTML);
if (result === '<div>Smurf</div>')
result = '<div class="">Smurf</div>'; // e.g. IE 9 and 10
test.equal(result, '<div class="' + className + '">Smurf</div>');
};
shouldBe('donut frankfurter noodle');
coll.remove({name: 'frankfurter'}); // (it was kind of a mouthful)
shouldBe('donut noodle');
coll.remove({name: 'donut'});
shouldBe('noodle');
coll.remove({name: 'noodle'});
shouldBe(''); // 'David' and 'Steve' appear in the #each but fail the #if
coll.remove({});
shouldBe('none'); // now the `{{else}}` case kicks in
coll.insert({name: 'bubblegum'});
shouldBe('bubblegum');
});
Tinytest.add('spacebars-tests - template_tests - block helpers in attribute 2', function (test) {
var tmpl = Template.spacebars_template_test_block_helpers_in_attribute_2;
var R = ReactiveVar(true);
tmpl.helpers({foo: function () { return R.get(); }});
var div = renderToDiv(tmpl);
var input = div.querySelector('input');
test.equal(input.value, '"');
R.set(false);
Tracker.flush();
test.equal(input.value, '&<></x>');
});
// Test that if the argument to #each is a constant, it doesn't establish a
// dependency on the data context, so when the context changes, items of
// the #each are not "changed" and helpers do not rerun.
Tinytest.add('spacebars-tests - template_tests - constant #each argument', function (test) {
var tmpl = Template.spacebars_template_test_constant_each_argument;
var justReturnRuns = 0; // how many times `justReturn` is called
var R = ReactiveVar(1);
tmpl.helpers({
someData: function () {
return R.get();
},
anArray: ['foo', 'bar'],
justReturn: function (x) {
justReturnRuns++;
return String(x);
}
});
var div = renderToDiv(tmpl);
test.equal(justReturnRuns, 2);
test.equal(canonicalizeHtml(div.innerHTML).replace(/\s+/g, ' '),
'foo bar 1');
R.set(2);
Tracker.flush();
test.equal(justReturnRuns, 2); // still 2, no new runs!
test.equal(canonicalizeHtml(div.innerHTML).replace(/\s+/g, ' '),
'foo bar 2');
});
Tinytest.addAsync('spacebars-tests - template_tests - #markdown - basic', function (test, onComplete) {
var tmpl = Template.spacebars_template_test_markdown_basic;
tmpl.helpers({
obj: {snippet: "<i>hi</i>"},
hi: function () {
return this.snippet;
}
});
var div = renderToDiv(tmpl);
Meteor.call("getAsset", "markdown_basic.html", function (err, html) {
test.isFalse(err);
test.equal(canonicalizeHtml(div.innerHTML),
canonicalizeHtml(html));
onComplete();
});
});
testAsyncMulti('spacebars-tests - template_tests - #markdown - if', [
function (test, expect) {
var self = this;
Meteor.call("getAsset", "markdown_if1.html", expect(function (err, html) {
test.isFalse(err);
self.html1 = html;
}));
Meteor.call("getAsset", "markdown_if2.html", expect(function (err, html) {
test.isFalse(err);
self.html2 = html;
}));
},
function (test, expect) {
var self = this;
var tmpl = Template.spacebars_template_test_markdown_if;
var R = new ReactiveVar(false);
tmpl.helpers({cond: function () { return R.get(); }});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html1));
R.set(true);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html2));
}
]);
testAsyncMulti('spacebars-tests - template_tests - #markdown - each', [
function (test, expect) {
var self = this;
Meteor.call("getAsset", "markdown_each1.html", expect(function (err, html) {
test.isFalse(err);
self.html1 = html;
}));
Meteor.call("getAsset", "markdown_each2.html", expect(function (err, html) {
test.isFalse(err);
self.html2 = html;
}));
},
function (test, expect) {
var self = this;
var tmpl = Template.spacebars_template_test_markdown_each;
var R = new ReactiveVar([]);
tmpl.helpers({seq: function () { return R.get(); }});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html1));
R.set(["item"]);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), canonicalizeHtml(self.html2));
}
]);
Tinytest.add('spacebars-tests - template_tests - #markdown - inclusion', function (test) {
var tmpl = Template.spacebars_template_test_markdown_inclusion;
var subtmpl = Template.spacebars_template_test_markdown_inclusion_subtmpl;
subtmpl.helpers({foo: "bar"});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "<p><span>Foo is bar.</span></p>");
});
Tinytest.add('spacebars-tests - template_tests - #markdown - block helpers', function (test) {
var tmpl = Template.spacebars_template_test_markdown_block_helpers;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "<p>Hi there!</p>");
});
// Test that when a simple helper re-runs due to a dependency changing
// but the return value is the same, the DOM text node is not
// re-rendered.
Tinytest.add('spacebars-tests - template_tests - simple helpers are isolated', function (test) {
var runs = [{
helper: function () { return "foo"; },
nodeValue: "foo"
}, {
helper: function () { return new Spacebars.SafeString("bar"); },
nodeValue: "bar"
}];
_.each(runs, function (run) {
var tmpl = Template.spacebars_template_test_simple_helpers_are_isolated;
var dep = new Tracker.Dependency;
tmpl.helpers({foo: function () {
dep.depend();
return run.helper();
}});
var div = renderToDiv(tmpl);
var fooTextNode = _.find(div.childNodes, function (node) {
return node.nodeValue === run.nodeValue;
});
test.isTrue(fooTextNode);
dep.changed();
Tracker.flush();
var newFooTextNode = _.find(div.childNodes, function (node) {
return node.nodeValue === run.nodeValue;
});
test.equal(fooTextNode, newFooTextNode);
});
});
// Test that when a helper in an element attribute re-runs due to a
// dependency changing but the return value is the same, the attribute
// value is not set.
Tinytest.add('spacebars-tests - template_tests - attribute helpers are isolated', function (test) {
var tmpl = Template.spacebars_template_test_attr_helpers_are_isolated;
var dep = new Tracker.Dependency;
tmpl.helpers({foo: function () {
dep.depend();
return "foo";
}});
var div = renderToDiv(tmpl);
var pElement = div.querySelector('p');
test.equal(pElement.getAttribute('attr'), 'foo');
// set the attribute to something else, afterwards check that it
// hasn't been updated back to the correct value.
pElement.setAttribute('attr', 'not-foo');
dep.changed();
Tracker.flush();
test.equal(pElement.getAttribute('attr'), 'not-foo');
});
// A helper can return an object with a set of element attributes via
// `<p {{attrs}}>`. When it re-runs due to a dependency changing the
// value for a given attribute might stay the same. Test that the
// attribute is not set on the DOM element.
Tinytest.add('spacebars-tests - template_tests - attribute object helpers are isolated', function (test) {
var tmpl = Template.spacebars_template_test_attr_object_helpers_are_isolated;
var dep = new Tracker.Dependency;
tmpl.helpers({attrs: function () {
dep.depend();
return {foo: "bar"};
}});
var div = renderToDiv(tmpl);
var pElement = div.querySelector('p');
test.equal(pElement.getAttribute('foo'), 'bar');
// set the attribute to something else, afterwards check that it
// hasn't been updated back to the correct value.
pElement.setAttribute('foo', 'not-bar');
dep.changed();
Tracker.flush();
test.equal(pElement.getAttribute('foo'), 'not-bar');
});
// Test that when a helper in an inclusion directive (`{{> foo }}`)
// re-runs due to a dependency changing but the return value is the
// same, the template is not re-rendered.
//
// Also, verify that an error is thrown if the return value from such
// a helper is not a component.
Tinytest.add('spacebars-tests - template_tests - inclusion helpers are isolated', function (test) {
var tmpl = Template.spacebars_template_test_inclusion_helpers_are_isolated;
var dep = new Tracker.Dependency;
var subtmpl = Template.spacebars_template_test_inclusion_helpers_are_isolated_subtemplate;
// make a copy so we can set "rendered" without mutating the original
var subtmplCopy = copyTemplate(subtmpl);
var R = new ReactiveVar(subtmplCopy);
tmpl.helpers({foo: function () {
dep.depend();
return R.get();
}});
var div = renderToDiv(tmpl);
subtmplCopy.rendered = function () {
test.fail("shouldn't re-render when same value returned from helper");
};
dep.changed();
Tracker.flush({_throwFirstError: true}); // `subtmplCopy.rendered` not called
R.set(null);
Tracker.flush({_throwFirstError: true}); // no error thrown
R.set("neither a component nor null");
test.throws(function () {
Tracker.flush({_throwFirstError: true});
}, /Expected template or null/);
});
Tinytest.add('spacebars-tests - template_tests - nully attributes', function (test) {
var tmpls = {
0: Template.spacebars_template_test_nully_attributes0,
1: Template.spacebars_template_test_nully_attributes1,
2: Template.spacebars_template_test_nully_attributes2,
3: Template.spacebars_template_test_nully_attributes3,
4: Template.spacebars_template_test_nully_attributes4,
5: Template.spacebars_template_test_nully_attributes5,
6: Template.spacebars_template_test_nully_attributes6
};
var run = function (whichTemplate, data, expectTrue) {
var div = renderToDiv(tmpls[whichTemplate], data);
var input = div.querySelector('input');
var descr = JSON.stringify([whichTemplate, data, expectTrue]);
if (expectTrue) {
test.isTrue(input.checked, descr);
test.equal(typeof input.getAttribute('stuff'), 'string', descr);
} else {
test.isFalse(input.checked);
test.equal(JSON.stringify(input.getAttribute('stuff')), 'null', descr);
}
var html = Blaze.toHTML(Blaze.With(data, function () {
return tmpls[whichTemplate];
}));
test.equal(/ checked="[^"]*"/.test(html), !! expectTrue);
test.equal(/ stuff="[^"]*"/.test(html), !! expectTrue);
};
run(0, {}, true);
var truthies = [true, ''];
var falsies = [false, null, undefined];
_.each(truthies, function (x) {
run(1, {foo: x}, true);
});
_.each(falsies, function (x) {
run(1, {foo: x}, false);
});
_.each(truthies, function (x) {
_.each(truthies, function (y) {
run(2, {foo: x, bar: y}, true);
});
_.each(falsies, function (y) {
run(2, {foo: x, bar: y}, true);
});
});
_.each(falsies, function (x) {
_.each(truthies, function (y) {
run(2, {foo: x, bar: y}, true);
});
_.each(falsies, function (y) {
run(2, {foo: x, bar: y}, false);
});
});
run(3, {foo: true}, false);
run(3, {foo: false}, false);
});
Tinytest.add("spacebars-tests - template_tests - double", function (test) {
var tmpl = Template.spacebars_template_test_double;
var run = function (foo, expectedResult) {
tmpl.helpers({foo: foo});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), expectedResult);
};
run('asdf', 'asdf');
run(1.23, '1.23');
run(0, '0');
run(true, 'true');
run(false, '');
run(null, '');
run(undefined, '');
});
Tinytest.add("spacebars-tests - template_tests - inclusion lookup order", function (test) {
// test that {{> foo}} looks for a helper named 'foo', then a
// template named 'foo', then a 'foo' field in the data context.
var tmpl = Template.spacebars_template_test_inclusion_lookup;
var tmplData = function () {
return {
// shouldn't have an effect since we define a helper with the
// same name.
spacebars_template_test_inclusion_lookup_subtmpl: Template.
spacebars_template_test_inclusion_lookup_subtmpl3,
dataContextSubtmpl: Template.
spacebars_template_test_inclusion_lookup_subtmpl3};
};
tmpl.helpers({
spacebars_template_test_inclusion_lookup_subtmpl:
Template.spacebars_template_test_inclusion_lookup_subtmpl2
});
test.equal(canonicalizeHtml(renderToDiv(tmpl, tmplData).innerHTML),
["This is generated by a helper with the same name.",
"This is a template passed in the data context."].join(' '));
});
Tinytest.add("spacebars-tests - template_tests - content context", function (test) {
var tmpl = Template.spacebars_template_test_content_context;
var R = ReactiveVar(true);
tmpl.helpers({foo: {
firstLetter: 'F',
secondLetter: 'O',
bar: {
cond: function () { return R.get(); },
firstLetter: 'B',
secondLetter: 'A'
}
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'BO');
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'FA');
});
_.each(['textarea', 'text', 'password', 'submit', 'button',
'reset', 'select', 'hidden'], function (type) {
Tinytest.add("spacebars-tests - template_tests - controls - " + type, function(test) {
var R = ReactiveVar({x:"test"});
var R2 = ReactiveVar("");
var tmpl;
if (type === 'select') {
tmpl = Template.spacebars_test_control_select;
tmpl.helpers({
options: ['This is a test', 'This is a fridge',
'This is a frog', 'This is a new frog', 'foobar',
'This is a photograph', 'This is a monkey',
'This is a donkey'],
selected: function () {
R2.get(); // Re-render when R2 is changed, even though it
// doesn't affect HTML.
return ('This is a ' + R.get().x) === this.toString();
}
});
} else if (type === 'textarea') {
tmpl = Template.spacebars_test_control_textarea;
tmpl.helpers({
value: function () {
R2.get(); // Re-render when R2 is changed, even though it
// doesn't affect HTML.
return 'This is a ' + R.get().x;
}
});
} else {
tmpl = Template.spacebars_test_control_input;
tmpl.helpers({
value: function () {
R2.get(); // Re-render when R2 is changed, even though it
// doesn't affect HTML.
return 'This is a ' + R.get().x;
},
type: type
});
};
var div = renderToDiv(tmpl);
document.body.appendChild(div);
var canFocus = (type !== 'hidden');
// find first element child, ignoring any marker nodes
var input = div.firstChild;
while (input.nodeType !== 1)
input = input.nextSibling;
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");
// value updates reactively
R.set({x:"fridge"});
Tracker.flush();
test.equal(DomUtils.getElementValue(input), "This is a fridge");
if (canFocus) {
// ...if focused, it still updates but focus isn't lost.
focusElement(input);
DomUtils.setElementValue(input, "something else");
R.set({x:"frog"});
Tracker.flush();
test.equal(DomUtils.getElementValue(input), "This is a frog");
test.equal(document.activeElement, input);
}
// 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");
Tracker.flush();
test.equal(DomUtils.getElementValue(input), "foobar");
// ... but if the actual rendered value changes, that should take effect.
R.set({x:"photograph"});
Tracker.flush();
test.equal(DomUtils.getElementValue(input), "This is a photograph");
document.body.removeChild(div);
});
});
Tinytest.add("spacebars-tests - template_tests - radio", function(test) {
var R = ReactiveVar("");
var R2 = ReactiveVar("");
var change_buf = [];
var tmpl = Template.spacebars_test_control_radio;
tmpl.helpers({
bands: ["AM", "FM", "XM"],
isChecked: function () {
return R.get() === this.toString();
},
band: function () {
return R.get();
}
});
tmpl.events({
'change input': function (event) {
var btn = event.target;
var band = btn.value;
change_buf.push(band);
R.set(band);
}
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
// get the three buttons; they should not change identities!
var btns = nodesToArray(div.getElementsByTagName("INPUT"));
var text = function () {
var text = div.innerText || div.textContent;
return text.replace(/[ \n\r]+/g, " ");
};
test.equal(_.pluck(btns, 'checked'), [false, false, false]);
test.equal(text(), "Band: ");
clickIt(btns[0]);
test.equal(change_buf, ['AM']);
change_buf.length = 0;
Tracker.flush();
test.equal(_.pluck(btns, 'checked'), [true, false, false]);
test.equal(text(), "Band: AM");
R2.set("change");
Tracker.flush();
test.length(change_buf, 0);
test.equal(_.pluck(btns, 'checked'), [true, false, false]);
test.equal(text(), "Band: AM");
clickIt(btns[1]);
test.equal(change_buf, ['FM']);
change_buf.length = 0;
Tracker.flush();
test.equal(_.pluck(btns, 'checked'), [false, true, false]);
test.equal(text(), "Band: FM");
clickIt(btns[2]);
test.equal(change_buf, ['XM']);
change_buf.length = 0;
Tracker.flush();
test.equal(_.pluck(btns, 'checked'), [false, false, true]);
test.equal(text(), "Band: XM");
clickIt(btns[1]);
test.equal(change_buf, ['FM']);
change_buf.length = 0;
Tracker.flush();
test.equal(_.pluck(btns, 'checked'), [false, true, false]);
test.equal(text(), "Band: FM");
document.body.removeChild(div);
});
Tinytest.add("spacebars-tests - template_tests - checkbox", function(test) {
var tmpl = Template.spacebars_test_control_checkbox;
var labels = ["Foo", "Bar", "Baz"];
var Rs = {};
_.each(labels, function (label) {
Rs[label] = ReactiveVar(false);
});
tmpl.helpers({
labels: labels,
isChecked: function () {
return Rs[this.toString()].get();
}
});
var changeBuf = [];
var div = renderToDiv(tmpl);
document.body.appendChild(div);
var boxes = nodesToArray(div.getElementsByTagName("INPUT"));
test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
// Re-render with first one checked.
Rs.Foo.set(true);
Tracker.flush();
test.equal(_.pluck(boxes, 'checked'), [true, false, false]);
// Re-render with first one unchecked again.
Rs.Foo.set(false);
Tracker.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]);
Tracker.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);
Tracker.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]);
Tracker.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);
Tracker.flush();
test.equal(_.pluck(boxes, 'checked'), [true, false, false]);
// Re-render with first one unchecked. Third should still stay off.
Rs.Foo.set(false);
Tracker.flush();
test.equal(_.pluck(boxes, 'checked'), [false, false, false]);
document.body.removeChild(div);
});
Tinytest.add('spacebars-tests - template_tests - unfound template', function (test) {
test.throws(function () {
renderToDiv(Template.spacebars_test_nonexistent_template);
}, /No such template/);
});
Tinytest.add('spacebars-tests - template_tests - helper passed to #if called exactly once when invalidated', function (test) {
var tmpl = Template.spacebars_test_if_helper;
var foo;
var count = 0;
var d = new Tracker.Dependency;
tmpl.helpers({foo: function () {
d.depend();
count++;
return foo;
}});
foo = false;
var div = renderToDiv(tmpl);
divRendersTo(test, div, "false");
test.equal(count, 1);
foo = true;
d.changed();
divRendersTo(test, div, "true");
test.equal(count, 2);
});
Tinytest.add('spacebars-tests - template_tests - custom block helper functions called exactly once when invalidated', function (test) {
var tmpl = Template.spacebars_test_block_helper_function;
var foo;
var count = 0;
var d = new Tracker.Dependency;
tmpl.helpers({foo: function () {
d.depend();
count++;
return Template.spacebars_template_test_aaa;
}});
foo = false;
renderToDiv(tmpl);
Tracker.flush();
test.equal(count, 1);
foo = true;
d.changed();
Tracker.flush();
test.equal(count, 2);
});
var runOneTwoTest = function (test, subTemplateName, optionsData) {
_.each([Template.spacebars_test_helpers_stop_onetwo,
Template.spacebars_test_helpers_stop_onetwo_attribute],
function (tmpl) {
var sub1 = Template[subTemplateName + '1'];
var sub2 = Template[subTemplateName + '2'];
tmpl.helpers({
one: sub1,
two: sub2
});
var buf = '';
var showOne = ReactiveVar(true);
var dummy = ReactiveVar(0);
tmpl.helpers({showOne: function () { return showOne.get(); }});
sub1.helpers({options: function () {
var x = dummy.get();
buf += '1';
if (optionsData)
return optionsData[x];
else
return ['something'];
}});
sub2.helpers({options: function () {
var x = dummy.get();
buf += '2';
if (optionsData)
return optionsData[x];
else
return ['something'];
}});
var div = renderToDiv(tmpl);
Tracker.flush();
test.equal(buf, '1');
showOne.set(false);
dummy.set(1);
Tracker.flush();
test.equal(buf, '12');
showOne.set(true);
dummy.set(2);
Tracker.flush();
test.equal(buf, '121');
// clean up the div
$(div).remove();
test.equal(showOne._numListeners(), 0);
test.equal(dummy._numListeners(), 0);
});
};
Tinytest.add('spacebars-tests - template_tests - with stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_with');
});
Tinytest.add('spacebars-tests - template_tests - each stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_each');
});
Tinytest.add('spacebars-tests - template_tests - each inside with stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_with_each');
});
Tinytest.add('spacebars-tests - template_tests - if stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_if', ['a', 'b', 'a']);
});
Tinytest.add('spacebars-tests - template_tests - unless stops without re-running helper', function (test) {
runOneTwoTest(test, 'spacebars_test_helpers_stop_unless', ['a', 'b', 'a']);
});
Tinytest.add('spacebars-tests - template_tests - inclusion stops without re-running function', function (test) {
var t = Template.spacebars_test_helpers_stop_inclusion3;
runOneTwoTest(test, 'spacebars_test_helpers_stop_inclusion', [t, t, t]);
});
Tinytest.add('spacebars-tests - template_tests - template with callbacks inside with stops without recalculating data', function (test) {
var tmpl = Template.spacebars_test_helpers_stop_with_callbacks3;
tmpl.created = function () {};
tmpl.rendered = function () {};
tmpl.destroyed = function () {};
runOneTwoTest(test, 'spacebars_test_helpers_stop_with_callbacks');
});
Tinytest.add('spacebars-tests - template_tests - no data context is seen as an empty object', function (test) {
var tmpl = Template.spacebars_test_no_data_context;
var dataInHelper = 'UNSET';
var dataInRendered = 'UNSET';
var dataInCreated = 'UNSET';
var dataInDestroyed = 'UNSET';
var dataInEvent = 'UNSET';
tmpl.helpers({foo: function () {
dataInHelper = this;
}});
tmpl.created = function () {
dataInCreated = this.data;
};
tmpl.rendered = function () {
dataInRendered = this.data;
};
tmpl.destroyed = function () {
dataInDestroyed = this.data;
};
tmpl.events({
'click': function () {
dataInEvent = this;
}
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
clickElement(div.querySelector('button'));
Tracker.flush(); // rendered gets called afterFlush
$(div).remove();
test.isFalse(dataInHelper === window);
test.equal(dataInHelper, {});
test.equal(dataInCreated, null);
test.equal(dataInRendered, null);
test.equal(dataInDestroyed, null);
test.isFalse(dataInEvent === window);
test.equal(dataInEvent, {});
});
Tinytest.add('spacebars-tests - template_tests - falsy with', function (test) {
var tmpl = Template.spacebars_test_falsy_with;
var R = ReactiveVar(null);
tmpl.helpers({obj: function () { return R.get(); }});
var div = renderToDiv(tmpl);
divRendersTo(test, div, "");
R.set({greekLetter: 'alpha'});
divRendersTo(test, div, "alpha");
R.set(null);
divRendersTo(test, div, "");
R.set({greekLetter: 'alpha'});
divRendersTo(test, div, "alpha");
});
Tinytest.add("spacebars-tests - template_tests - helpers don't leak", function (test) {
var tmpl = Template.spacebars_test_helpers_dont_leak;
tmpl.foo = "wrong";
tmpl.bar = function () { return "WRONG"; };
// Also test that custom block helpers (implemented as templates) do NOT
// interfere with helper lookup in the current template
Template.spacebars_test_helpers_dont_leak2.helpers({
bonus: function () { return 'BONUS'; }});
var div = renderToDiv(tmpl);
divRendersTo(test, div, "correct BONUS");
});
Tinytest.add("spacebars-tests - template_tests - event handler returns false",
function (test) {
var tmpl = Template.spacebars_test_event_returns_false;
var elemId = "spacebars_test_event_returns_false_link";
tmpl.events({
'click a': function (evt) { return false; }
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
clickIt(document.getElementById(elemId));
// NOTE: This failure can stick across test runs! Try
// removing '#bad-url' from the location bar and run
// the tests again. :)
test.isFalse(/#bad-url/.test(window.location.hash));
document.body.removeChild(div);
}
);
// Make sure that if you bind an event on "div p", for example,
// both the div and the p need to be in the template. jQuery's
// `$(elem).find(...)` works this way, but the browser's
// querySelector doesn't.
Tinytest.add(
"spacebars-tests - template_tests - event map selector scope",
function (test) {
var tmpl = Template.spacebars_test_event_selectors1;
var tmpl2 = Template.spacebars_test_event_selectors2;
var buf = [];
tmpl2.events({
'click div p': function (evt) { buf.push(evt.currentTarget.className); }
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
test.equal(buf.join(), '');
clickIt(div.querySelector('.p1'));
test.equal(buf.join(), '');
clickIt(div.querySelector('.p2'));
test.equal(buf.join(), 'p2');
document.body.removeChild(div);
}
);
if (document.addEventListener) {
// see note about non-bubbling events in the "capuring events"
// templating test for why we use the VIDEO tag. (It would be
// nice to get rid of the network dependency, though.)
// We skip this test in IE 8.
Tinytest.add(
"spacebars-tests - template_tests - event map selector scope (capturing)",
function (test) {
var tmpl = Template.spacebars_test_event_selectors_capturing1;
var tmpl2 = Template.spacebars_test_event_selectors_capturing2;
var buf = [];
tmpl2.events({
'play div video': function (evt) { buf.push(evt.currentTarget.className); }
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
test.equal(buf.join(), '');
simulateEvent(div.querySelector(".video1"),
"play", {}, {bubbles: false});
test.equal(buf.join(), '');
simulateEvent(div.querySelector(".video2"),
"play", {}, {bubbles: false});
test.equal(buf.join(), 'video2');
document.body.removeChild(div);
}
);
}
Tinytest.add("spacebars-tests - template_tests - tables", function (test) {
var tmpl1 = Template.spacebars_test_tables1;
var div = renderToDiv(tmpl1);
test.equal(_.pluck(div.querySelectorAll('*'), 'tagName'),
['TABLE', 'TR', 'TD']);
divRendersTo(test, div, '<table><tr><td>Foo</td></tr></table>');
var tmpl2 = Template.spacebars_test_tables2;
tmpl2.helpers({foo: 'Foo'});
div = renderToDiv(tmpl2);
test.equal(_.pluck(div.querySelectorAll('*'), 'tagName'),
['TABLE', 'TR', 'TD']);
divRendersTo(test, div, '<table><tr><td>Foo</td></tr></table>');
});
Tinytest.add("spacebars-tests - template_tests - jQuery.trigger extraParameters are passed to the event callback",
function (test) {
var tmpl = Template.spacebars_test_jquery_events;
var captured = false;
var args = ["param1", "param2", {option: 1}, 1, 2, 3];
tmpl.events({
'someCustomEvent': function (event, template) {
var i;
for (i=0; i<args.length; i++) {
// expect the arguments to be just after template
test.equal(arguments[i+2], args[i]);
}
captured = true;
}
});
tmpl.rendered = function () {
$(this.find('button')).trigger('someCustomEvent', args);
};
renderToDiv(tmpl);
Tracker.flush();
test.equal(captured, true);
}
);
Tinytest.add("spacebars-tests - template_tests - toHTML", function (test) {
// run once, verifying that autoruns are stopped
var once = function (tmplToRender, tmplForHelper, helper, val) {
var count = 0;
var R = new ReactiveVar;
var getR = function () {
count++;
return R.get();
};
R.set(val);
var helpers = {};
helpers[helper] = getR;
tmplForHelper.helpers(helpers);
test.equal(canonicalizeHtml(Blaze.toHTML(tmplToRender)), "bar");
test.equal(count, 1);
R.set("");
Tracker.flush();
test.equal(count, 1); // all autoruns stopped
};
once(Template.spacebars_test_tohtml_basic,
Template.spacebars_test_tohtml_basic, "foo", "bar");
once(Template.spacebars_test_tohtml_if,
Template.spacebars_test_tohtml_if, "foo", "bar");
once(Template.spacebars_test_tohtml_with,
Template.spacebars_test_tohtml_with, "foo", "bar");
once(Template.spacebars_test_tohtml_each,
Template.spacebars_test_tohtml_each, "foos", ["bar"]);
once(Template.spacebars_test_tohtml_include_with,
Template.spacebars_test_tohtml_with, "foo", "bar");
once(Template.spacebars_test_tohtml_include_each,
Template.spacebars_test_tohtml_each, "foos", ["bar"]);
});
Tinytest.add("spacebars-tests - template_tests - block comments should not be displayed",
function (test) {
var tmpl = Template.spacebars_test_block_comment;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '');
}
);
// Originally reported at https://github.com/meteor/meteor/issues/2046
Tinytest.add("spacebars-tests - template_tests - {{#with}} with mutated data context",
function (test) {
var tmpl = Template.spacebars_test_with_mutated_data_context;
var foo = {value: 0};
var dep = new Tracker.Dependency;
tmpl.helpers({foo: function () {
dep.depend();
return foo;
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '0');
foo.value = 1;
dep.changed();
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '1');
});
Tinytest.add("spacebars-tests - template_tests - javascript scheme urls",
function (test) {
var tmpl = Template.spacebars_test_url_attribute;
var sessionKey = "foo-" + Random.id();
tmpl.helpers({foo: function () {
return Session.get(sessionKey);
}});
var numUrlAttrs = 4;
var div = renderToDiv(tmpl);
// [tag name, attr name, is a url attribute]
var attrsList = [["A", "href", true], ["FORM", "action", true],
["IMG", "src", true], ["INPUT", "value", false]];
var checkAttrs = function (url, isJavascriptProtocol) {
if (isJavascriptProtocol) {
Meteor._suppress_log(numUrlAttrs);
}
Session.set(sessionKey, url);
Tracker.flush();
_.each(
attrsList,
function (attrInfo) {
var normalizedUrl;
var elem = document.createElement(attrInfo[0]);
try {
elem[attrInfo[1]] = url;
} catch (err) {
// IE throws an exception if you set an img src to a
// javascript: URL. Blaze can't override this behavior;
// whether you've called Blaze._javascriptUrlsAllowed() or not,
// you won't be able to set a javascript: URL in an img
// src. So we only test img tags in other browsers.
if (attrInfo[0] === "IMG") {
return;
}
throw err;
}
document.body.appendChild(elem);
normalizedUrl = elem[attrInfo[1]];
document.body.removeChild(elem);
_.each(
div.getElementsByTagName(attrInfo[0]),
function (elem) {
test.equal(
elem[attrInfo[1]],
isJavascriptProtocol && attrInfo[2] ? "" : normalizedUrl
);
}
);
}
);
};
test.equal(Blaze._javascriptUrlsAllowed(), false);
checkAttrs("http://www.meteor.com", false);
checkAttrs("javascript:alert(1)", true);
checkAttrs("jAvAsCrIpT:alert(1)", true);
checkAttrs(" javascript:alert(1)", true);
Blaze._allowJavascriptUrls();
test.equal(Blaze._javascriptUrlsAllowed(), true);
checkAttrs("http://www.meteor.com", false);
checkAttrs("javascript:alert(1)", false);
checkAttrs("jAvAsCrIpT:alert(1)", false);
checkAttrs(" javascript:alert(1)", false);
}
);
Tinytest.add("spacebars-tests - template_tests - event handlers get cleaned up when template is removed",
function (test) {
var tmpl = Template.spacebars_test_event_handler_cleanup;
var subtmpl = Template.spacebars_test_event_handler_cleanup_sub;
var rv = new ReactiveVar(true);
tmpl.helpers({foo: function () {
return rv.get();
}});
subtmpl.events({
"click/mouseover": function () { }
});
var div = renderToDiv(tmpl);
test.equal(div.$blaze_events["click"].handlers.length, 1);
test.equal(div.$blaze_events["mouseover"].handlers.length, 1);
rv.set(false);
Tracker.flush();
test.equal(div.$blaze_events["click"].handlers.length, 0);
test.equal(div.$blaze_events["mouseover"].handlers.length, 0);
}
);
// This test makes sure that Blaze correctly finds the controller
// heirarchy surrounding an element that itself doesn't have a
// controller.
Tinytest.add(
"spacebars-tests - template_tests - data context in event handlers on elements inside {{#if}}",
function (test) {
var tmpl = Template.spacebars_test_data_context_for_event_handler_in_if;
var data = null;
tmpl.events({
'click span': function () {
data = this;
}
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
clickIt(div.querySelector('span'));
test.equal(data, {foo: "bar"});
document.body.removeChild(div);
});
// https://github.com/meteor/meteor/issues/2156
Tinytest.add(
"spacebars-tests - template_tests - each with inserts inside autorun",
function (test) {
var tmpl = Template.spacebars_test_each_with_autorun_insert;
var coll = new Mongo.Collection(null);
var rv = new ReactiveVar;
tmpl.helpers({items: function () {
return coll.find();
}});
var div = renderToDiv(tmpl);
Tracker.autorun(function () {
if (rv.get()) {
coll.insert({ name: rv.get() });
}
});
rv.set("foo1");
Tracker.flush();
var firstId = coll.findOne()._id;
rv.set("foo2");
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "foo1 foo2");
coll.update(firstId, { $set: { name: "foo3" } });
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "foo3 foo2");
}
);
Tinytest.add(
"spacebars-tests - template_tests - ui hooks",
function (test) {
var tmpl = Template.spacebars_test_ui_hooks;
var rv = new ReactiveVar([]);
tmpl.helpers({items: function () {
return rv.get();
}});
var div = renderToDiv(tmpl);
var hooks = [];
var container = div.querySelector(".test-ui-hooks");
// Before we attach the ui hooks, put two items in the DOM.
var origVal = [{ _id: 'foo1' }, { _id: 'foo2' }];
rv.set(origVal);
Tracker.flush();
container._uihooks = {
insertElement: function (n, next) {
hooks.push("insert");
// check that the element hasn't actually been added yet
test.isTrue((! n.parentNode) ||
n.parentNode.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/);
},
removeElement: function (n) {
hooks.push("remove");
// check that the element hasn't actually been removed yet
test.isTrue(n.parentNode === container);
},
moveElement: function (n, next) {
hooks.push("move");
// check that the element hasn't actually been moved yet
test.isFalse(n.nextNode === next);
}
};
var testDomUnchanged = function () {
var items = div.querySelectorAll(".item");
test.equal(items.length, 2);
test.equal(canonicalizeHtml(items[0].innerHTML), "foo1");
test.equal(canonicalizeHtml(items[1].innerHTML), "foo2");
};
var newVal = _.clone(origVal);
newVal.push({ _id: 'foo3' });
rv.set(newVal);
Tracker.flush();
test.equal(hooks, ['insert']);
testDomUnchanged();
newVal.reverse();
rv.set(newVal);
Tracker.flush();
test.equal(hooks, ['insert', 'move']);
testDomUnchanged();
newVal = [origVal[0]];
rv.set(newVal);
Tracker.flush();
test.equal(hooks, ['insert', 'move', 'remove']);
testDomUnchanged();
}
);
Tinytest.add(
"spacebars-tests - template_tests - ui hooks - nested domranges",
function (test) {
var tmpl = Template.spacebars_test_ui_hooks_nested;
var rv = new ReactiveVar(true);
tmpl.helpers({foo: function () {
return rv.get();
}});
var subtmpl = Template.spacebars_test_ui_hooks_nested_sub;
var uiHookCalled = false;
subtmpl.rendered = function () {
this.firstNode.parentNode._uihooks = {
removeElement: function (node) {
uiHookCalled = true;
}
};
};
var div = renderToDiv(tmpl);
document.body.appendChild(div);
Tracker.flush();
var htmlBeforeRemove = canonicalizeHtml(div.innerHTML);
rv.set(false);
Tracker.flush();
test.isTrue(uiHookCalled);
var htmlAfterRemove = canonicalizeHtml(div.innerHTML);
test.equal(htmlBeforeRemove, htmlAfterRemove);
document.body.removeChild(div);
}
);
Tinytest.add(
"spacebars-tests - template_tests - Template.instance from helper",
function (test) {
// Set a property on the template instance; check that it's still
// there from a helper.
var tmpl = Template.spacebars_test_template_instance_helper;
var value = Random.id();
var instanceFromHelper;
tmpl.created = function () {
this.value = value;
};
tmpl.helpers({foo: function () {
instanceFromHelper = Template.instance();
}});
var div = renderToDiv(tmpl);
test.equal(instanceFromHelper.value, value);
}
);
Tinytest.add(
"spacebars-tests - template_tests - Template.instance from helper, " +
"template instance is kept up-to-date",
function (test) {
var tmpl = Template.spacebars_test_template_instance_helper;
var rv = new ReactiveVar("");
var instanceFromHelper;
tmpl.helpers({foo: function () {
return Template.instance().data;
}});
var div = renderToDiv(tmpl, function () { return rv.get(); });
rv.set("first");
divRendersTo(test, div, "first");
rv.set("second");
Tracker.flush();
divRendersTo(test, div, "second");
// Template.instance() returns null when no template instance
test.isTrue(Template.instance() === null);
}
);
Tinytest.add(
"spacebars-tests - template_tests - {{#with}} autorun is cleaned up",
function (test) {
var tmpl = Template.spacebars_test_with_cleanup;
var rv = new ReactiveVar("");
var helperCalled = false;
tmpl.helpers({foo: function () {
helperCalled = true;
return rv.get();
}});
var div = renderToDiv(tmpl);
rv.set("first");
Tracker.flush();
test.equal(helperCalled, true);
helperCalled = false;
$(div).find(".test-with-cleanup").remove();
rv.set("second");
Tracker.flush();
test.equal(helperCalled, false);
}
);
Tinytest.add(
"spacebars-tests - template_tests - Template.parentData from helpers",
function (test) {
var childTmpl = Template.spacebars_test_template_parent_data_helper_child;
var parentTmpl = Template.spacebars_test_template_parent_data_helper;
var height = new ReactiveVar(0);
var bar = new ReactiveVar("bar");
childTmpl.helpers({
a: ["a"],
b: function () { return bar.get(); },
c: ["c"],
foo: function () {
var a = Template.parentData(height.get());
var b = UI._parentData(height.get()); // back-compat
test.equal(a, b);
return a;
}
});
var div = renderToDiv(parentTmpl);
test.equal(canonicalizeHtml(div.innerHTML), "d");
height.set(1);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "bar");
// Test Template.parentData() reactivity
bar.set("baz");
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "baz");
height.set(2);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "a");
height.set(3);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "parent");
// Test that calling Template.parentData() without any arguments is the same as Template.parentData(1)
height.set(null);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), "baz");
}
);
Tinytest.add(
"spacebars - SVG <a> elements",
function (test) {
if (! document.createElementNS) {
// IE 8
return;
}
var tmpl = Template.spacebars_test_svg_anchor;
var div = renderToDiv(tmpl);
var anchNamespace = $(div).find("a").get(0).namespaceURI;
test.equal(anchNamespace, "http://www.w3.org/2000/svg");
}
);
Tinytest.add(
"spacebars-tests - template_tests - created/rendered/destroyed by each",
function (test) {
var outerTmpl =
Template.spacebars_test_template_created_rendered_destroyed_each;
var innerTmpl =
Template.spacebars_test_template_created_rendered_destroyed_each_sub;
var buf = '';
innerTmpl.created = function () { buf += 'C' + String(this.data).toLowerCase(); };
innerTmpl.rendered = function () { buf += 'R' + String(this.data).toLowerCase(); };
innerTmpl.destroyed = function () { buf += 'D' + String(this.data).toLowerCase(); };
var R = ReactiveVar([{_id: 'A'}]);
outerTmpl.helpers({items: function () {
return R.get();
}});
var div = renderToDiv(outerTmpl);
divRendersTo(test, div, '<div>A</div>');
test.equal(buf, 'CaRa');
R.set([{_id: 'B'}]);
divRendersTo(test, div, '<div>B</div>');
test.equal(buf, 'CaRaDaCbRb');
R.set([{_id: 'C'}]);
divRendersTo(test, div, '<div>C</div>');
test.equal(buf, 'CaRaDaCbRbDbCcRc');
$(div).remove();
test.equal(buf, 'CaRaDaCbRbDbCcRcDc');
});
Tinytest.add(
"spacebars-tests - template_tests - Blaze.render/Blaze.remove",
function (test) {
var div = document.createElement("DIV");
document.body.appendChild(div);
var created = false, rendered = false, destroyed = false;
var R = ReactiveVar('aaa');
var tmpl = Template.spacebars_test_ui_render;
tmpl.helpers({
greeting: function () { return this.greeting || 'Hello'; },
r: function () { return R.get(); }
});
tmpl.created = function () { created = true; };
tmpl.rendered = function () { rendered = true; };
tmpl.destroyed = function () { destroyed = true; };
test.equal([created, rendered, destroyed], [false, false, false]);
var renderedTmpl = Blaze.render(tmpl, div);
test.equal([created, rendered, destroyed], [true, false, false]);
// Flush now. We fire the rendered callback in an afterFlush block,
// to ensure that the DOM is completely updated.
Tracker.flush();
test.equal([created, rendered, destroyed], [true, true, false]);
var otherDiv = document.createElement("DIV");
// can run a second time without throwing
var x = Blaze.render(tmpl, otherDiv);
// note: we'll have clean up `x` below
var renderedTmpl2 = Blaze.renderWithData(
tmpl, {greeting: 'Bye'}, div);
test.equal(canonicalizeHtml(div.innerHTML),
"<span>Hello aaa</span><span>Bye aaa</span>");
R.set('bbb');
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML),
"<span>Hello bbb</span><span>Bye bbb</span>");
test.equal([created, rendered, destroyed], [true, true, false]);
test.equal(R._numListeners(), 3);
Blaze.remove(renderedTmpl);
Blaze.remove(renderedTmpl); // test that double-remove doesn't throw
Blaze.remove(renderedTmpl2);
Blaze.remove(x);
test.equal([created, rendered, destroyed], [true, true, true]);
test.equal(R._numListeners(), 0);
test.equal(canonicalizeHtml(div.innerHTML), "");
});
Tinytest.add(
"spacebars-tests - template_tests - Blaze.render fails on jQuery objects",
function (test) {
var tmpl = Template.spacebars_test_ui_render;
test.throws(function () {
Blaze.render(tmpl, $('body'));
}, /'parentElement' must be a DOM node/);
test.throws(function () {
Blaze.render(tmpl, document.body, $('body'));
}, /'nextNode' must be a DOM node/);
});
Tinytest.add(
"spacebars-tests - template_tests - UI.getElementData",
function (test) {
var div = document.createElement("DIV");
var tmpl = Template.spacebars_test_ui_getElementData;
Blaze.renderWithData(tmpl, {foo: "bar"}, div);
var span = div.querySelector('SPAN');
test.isTrue(span);
test.equal(UI.getElementData(span), {foo: "bar"});
test.equal(Blaze.getData(span), {foo: "bar"});
});
Tinytest.add(
"spacebars-tests - template_tests - autorun cleanup",
function (test) {
var tmpl = Template.spacebars_test_parent_removal;
var Acalls = '';
var A = ReactiveVar('hi');
tmpl.helpers({A: function (chr) {
Acalls += chr;
return A.get();
}});
var Bcalls = 0;
var B = ReactiveVar(['one', 'two']);
tmpl.helpers({B: function () {
Bcalls++;
return B.get();
}});
// Assert how many times A and B were accessed (since last time)
// and how many autoruns are listening to them.
var assertCallsAndListeners =
function (a_calls, b_calls, a_listeners, b_listeners) {
test.equal('A calls: ' + Acalls.length,
'A calls: ' + a_calls,
Acalls);
test.equal('B calls: ' + Bcalls,
'B calls: ' + b_calls);
test.equal('A listeners: ' + A._numListeners(),
'A listeners: ' + a_listeners);
test.equal('B listeners: ' + B._numListeners(),
'B listeners: ' + b_listeners);
Acalls = '';
Bcalls = 0;
};
var div = renderToDiv(tmpl);
assertCallsAndListeners(10, 1, 10, 1);
A.set('');
Tracker.flush();
// Confirm that #4, #5, #6, and #9 are not re-run.
// #a is newly run, for a total of 10 - 4 + 1 = 7,
assertCallsAndListeners(7, 0, 7, 1);
A.set('hi');
Tracker.flush();
assertCallsAndListeners(10, 0, 10, 1);
// Now see that removing the DOM with jQuery, below
// the level of the entire template, stops everything.
$(div.querySelector('.toremove')).remove();
assertCallsAndListeners(0, 0, 0, 0);
});
Tinytest.add(
"spacebars-tests - template_tests - focus/blur with clean-up",
function (test) {
var tmpl = Template.spacebars_test_focus_blur_outer;
var cond = ReactiveVar(true);
tmpl.helpers({cond: function () {
return cond.get();
}});
var buf = [];
Template.spacebars_test_focus_blur_inner.events({
'focus input': function () {
buf.push('FOCUS');
},
'blur input': function () {
buf.push('BLUR');
}
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
// check basic focus and blur to make sure
// everything is sane
test.equal(div.querySelectorAll('input').length, 1);
var input;
focusElement(input = div.querySelector('input'));
// We don't get focus events when the Chrome Dev Tools are focused,
// unfortunately, as of Chrome 35. I think this is a regression in
// Chrome 34. So, the goal is to work whether or not focus is
// "borken," where "working" means always failing if DOMBackend isn't
// correctly unbinding the old event handlers when we switch the IF,
// and always passing if it is. To cause the problem in DOMBackend,
// delete the '**' argument to jQuery#off in
// DOMBackend.Events.undelegateEvents. The only compromise we are
// making here is that if some unrelated bug in Blaze makes
// focus/blur not work, the failure might be masked while the Dev
// Tools are open.
var borken = false;
if (buf.length === 0 && document.activeElement === input) {
test.ok({note:"You might need to defocus the Chrome Dev Tools to get a more accurate run of this test!"});
borken = true;
$(input).trigger('focus');
}
test.equal(buf.join(), 'FOCUS');
blurElement(div.querySelector('input'));
if (buf.length === 1)
$(input).trigger('blur');
test.equal(buf.join(), 'FOCUS,BLUR');
// now switch the IF and check again. The failure mode
// we observed was that DOMBackend would not correctly
// unbind the old event listener at the jQuery level,
// so the old event listener would fire and cause an
// exception inside Blaze ("Must be attached" in
// DOMRange#containsElement), which would show up in
// the console and cause our handler not to fire.
cond.set(false);
buf.length = 0;
Tracker.flush();
test.equal(div.querySelectorAll('input').length, 1);
focusElement(input = div.querySelector('input'));
if (borken)
$(input).trigger('focus');
test.equal(buf.join(), 'FOCUS');
blurElement(div.querySelector('input'));
if (! borken)
test.equal(buf.join(), 'FOCUS,BLUR');
document.body.removeChild(div);
});
// We used to remove event handlers on DOMRange detached, but when
// tearing down a view, we don't "detach" all the DOMRanges recursively.
// Mainly, we destroy the View. Destroying a View should remove its
// event listeners. (In practice, however, it's hard to think of
// consequences to not removing event handlers on removed DOM nodes,
// which will probably be GCed anyway.)
Tinytest.add(
"spacebars-tests - template_tests - event cleanup on destroyed",
function (test) {
var tmpl = Template.spacebars_test_event_cleanup_on_destroyed_outer;
var cond = ReactiveVar(true);
tmpl.helpers({cond: function () {
return cond.get();
}});
Template.spacebars_test_event_cleanup_on_destroyed_inner.events({
'click span': function () {}});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
var eventDiv = div.querySelector('div');
test.equal(eventDiv.$blaze_events.click.handlers.length, 1);
cond.set(false);
Tracker.flush();
test.equal(eventDiv.$blaze_events.click.handlers.length, 0);
document.body.removeChild(div);
});
_.each([1, 2, 3], function (n) {
Tinytest.add(
"spacebars-tests - template_tests - lookup is isolated " + n,
function (test) {
var buf = "";
var inclusion = Template.spacebars_test_isolated_lookup_inclusion;
inclusion.created = function () { buf += 'C'; };
inclusion.destroyed = function () { buf += 'D'; };
var tmpl = Template['spacebars_test_isolated_lookup' + n];
var R = ReactiveVar(Template.spacebars_template_test_aaa);
tmpl.helpers({bar: function () {
return R.get();
}});
var div = renderToDiv(
tmpl,
function () {
return { foo: R.get() };
});
test.equal(canonicalizeHtml(div.innerHTML), 'aaa--x');
test.equal(buf, 'C');
R.set(Template.spacebars_template_test_bbb);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'bbb--x');
test.equal(buf, 'C');
}
);
});
Tinytest.add('spacebars-tests - template_tests - current view in event handler', function (test) {
var tmpl = Template.spacebars_test_current_view_in_event;
var currentView;
var currentData;
tmpl.events({
'click span': function () {
currentView = Blaze.getView();
currentData = Blaze.getData();
}
});
var div = renderToDiv(tmpl, 'blah');
test.equal(canonicalizeHtml(div.innerHTML), '<span>blah</span>');
document.body.appendChild(div);
clickElement(div.querySelector('span'));
$(div).remove();
test.isTrue(currentView);
test.equal(currentData, 'blah');
});
Tinytest.add(
"spacebars-tests - template_tests - textarea attrs", function (test) {
var tmplNoContents = {
tmpl: Template.spacebars_test_textarea_attrs,
hasTextAreaContents: false
};
var tmplWithContents = {
tmpl: Template.spacebars_test_textarea_attrs_contents,
hasTextAreaContents: true
};
var tmplWithContentsAndMoreAttrs = {
tmpl: Template.spacebars_test_textarea_attrs_array_contents,
hasTextAreaContents: true
};
_.each(
[tmplNoContents, tmplWithContents,
tmplWithContentsAndMoreAttrs],
function (tmplInfo) {
var id = new ReactiveVar("textarea-" + Random.id());
var name = new ReactiveVar("one");
var attrs = new ReactiveVar({
id: "textarea-" + Random.id()
});
var div = renderToDiv(tmplInfo.tmpl, {
attrs: function () {
return attrs.get();
},
name: function () {
return name.get();
}
});
// Check that the id and value attribute are as we expect.
// We can't check div.innerHTML because Chrome at least doesn't
// appear to put textarea value attributes in innerHTML.
var textarea = div.querySelector("textarea");
test.equal(textarea.id, attrs.get().id);
test.equal(
textarea.value, tmplInfo.hasTextAreaContents ? "Hello one" : "");
// One of the templates has a separate attribute in addition to
// an attributes dictionary.
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
test.equal($(textarea).attr("class"), "bar");
}
// Change the id, check that the attribute updates reactively.
attrs.set({ id: "textarea-" + Random.id() });
Tracker.flush();
test.equal(textarea.id, attrs.get().id);
// Change the name variable, check that the textarea value
// updates reactively.
name.set("two");
Tracker.flush();
test.equal(
textarea.value, tmplInfo.hasTextAreaContents ? "Hello two" : "");
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
test.equal($(textarea).attr("class"), "bar");
}
});
});
Tinytest.add(
"spacebars-tests - template_tests - this.autorun",
function (test) {
var tmpl = Template.spacebars_test_autorun;
var tmplInner = Template.spacebars_test_autorun_inner;
// Keep track of the value of `Template.instance()` inside the
// autorun each time it runs.
var autorunTemplateInstances = [];
var actualTemplateInstance;
var returnedComputation;
var computationArg;
var show = new ReactiveVar(true);
var rv = new ReactiveVar("foo");
tmplInner.created = function () {
actualTemplateInstance = this;
returnedComputation = this.autorun(function (c) {
computationArg = c;
rv.get();
autorunTemplateInstances.push(Template.instance());
});
};
tmpl.helpers({
show: function () {
return show.get();
}
});
var div = renderToDiv(tmpl);
test.equal(autorunTemplateInstances.length, 1);
test.equal(autorunTemplateInstances[0], actualTemplateInstance);
// Test that the autorun returned a computation and received a
// computation as an argument.
test.isTrue(returnedComputation instanceof Tracker.Computation);
test.equal(returnedComputation, computationArg);
// Make sure the autorun re-runs when `rv` changes, and that it has
// the correct current view.
rv.set("bar");
Tracker.flush();
test.equal(autorunTemplateInstances.length, 2);
test.equal(autorunTemplateInstances[1], actualTemplateInstance);
// If the inner template is destroyed, the autorun should be stopped.
show.set(false);
Tracker.flush();
rv.set("baz");
Tracker.flush();
test.equal(autorunTemplateInstances.length, 2);
test.equal(rv._numListeners(), 0);
}
);
// Test that argument in {{> Template.contentBlock arg}} is evaluated in
// the proper data context.
Tinytest.add(
"spacebars-tests - template_tests - contentBlock argument",
function (test) {
var tmpl = Template.spacebars_test_contentBlock_arg;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'AAA BBB');
});
// Test that when Blaze sets an input field to the same value,
// we don't lose the insertion point position.
Tinytest.add(
"spacebars-tests - template_tests - input field to same value",
function (test) {
var tmpl = Template.spacebars_template_test_input_field_to_same_value;
var R = ReactiveVar("BLAH");
tmpl.helpers({foo: function () { return R.get(); }});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
var input = div.querySelector('input');
test.equal(input.value, "BLAH");
var setSelection = function (startEnd) {
startEnd = startEnd.split(' ');
if (typeof input.selectionStart === 'number') {
// all but IE < 9
input.selectionStart = startEnd[0];
input.selectionEnd = startEnd[1];
} else {
// IE 8
input.focus();
var r = input.createTextRange();
// move the start and end of the range to the beginning
// of the input field
r.moveStart('textedit', -1);
r.moveEnd('textedit', -1);
// move the start and end a certain number of characters
// (relative to their current position)
r.moveEnd('character', startEnd[1]);
r.moveStart('character', startEnd[0]);
r.select();
}
};
var getSelection = function () {
if (typeof input.selectionStart === 'number') {
// all but IE < 9
return input.selectionStart + " " + input.selectionEnd;
} else {
// IE 8
input.focus();
var r = document.selection.createRange();
var fullText = input.value;
var start, end;
if (r.text) {
// one or more characters are selected.
// this is kind of hacky! Relies on fullText
// not having duplicate letters, for example.
start = fullText.indexOf(r.text);
end = start + r.text.length;
} else {
r.moveStart('textedit', -1);
start = end = r.text.length;
}
return start + " " + end;
}
};
setSelection("2 3");
test.equal(getSelection(), "2 3");
// At this point, we COULD confirm that setting input.value to
// the same thing as before ("BLAH") loses the insertion
// point (per browser behavior). However, it doesn't on Firefox.
// So we set it to something different, which verifies that our
// test machinery is correct.
input.value = "BLAN";
// test that insertion point is lost
var selectionAfterSet = getSelection();
if (selectionAfterSet !== "0 0") // IE 8
test.equal(getSelection(), "4 4");
// now make the input say "BLAH" but the AttributeHandler
// says "OTHER" (so we can make it do the no-op update)
R.set("OTHER");
Tracker.flush();
test.equal(input.value, "OTHER");
input.value = "BLAH";
setSelection("2 2");
R.set("BLAH");
Tracker.flush();
test.equal(input.value, "BLAH");
// test that didn't lose insertion point!
test.equal(getSelection(), "2 2");
// clean up after ourselves
document.body.removeChild(div);
}
);
Tinytest.add("spacebars-tests - template_tests - contentBlock back-compat", function (test) {
// adapted from another test, but this time make sure `UI.contentBlock`
// and `UI.elseBlock` correctly behave as `Template.contentBlock`
// and `Template.elseBlock`.
var tmpl = Template.spacebars_template_test_content_backcompat;
var R = ReactiveVar(true);
tmpl.helpers({flag: function () {
return R.get();
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'world');
R.set(true);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
});
// For completeness (of coverage), make sure the code that calls
// `Template.contentBlock` in the correct scope also causes
// the old `UI.contentBlock` to be called in the correct scope.
Tinytest.add("spacebars-tests - template_tests - content context back-compat", function (test) {
var tmpl = Template.spacebars_template_test_content_context_backcompat;
var R = ReactiveVar(true);
tmpl.helpers({foo: {
firstLetter: 'F',
secondLetter: 'O',
bar: {
cond: function () { return R.get(); },
firstLetter: 'B',
secondLetter: 'A'
}
}});
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'BO');
R.set(false);
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), 'FA');
});
Tinytest.add("spacebars-tests - template_tests - falsy helper", function (test) {
var tmpl = Template.spacebars_template_test_falsy_helper;
tmpl.helpers({foo: 0});
Template.registerHelper('GLOBAL_ZERO', 0);
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'foo:0 GLOBAL_ZERO:0');
});
Tinytest.add("spacebars-tests - template_tests - old-style helpers", function (test) {
var tmpl = Template.spacebars_template_test_oldstyle_helpers;
tmpl._NOWARN_OLDSTYLE_HELPERS = true;
// Test old-style helper
tmpl.foo = 'hello';
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'hello');
// Test that replacing a helper still works (i.e. we don't cache them).
// We can change this behavior if we need to, but it is more breaking
// to do so. It breaks some unit tests, for example.
tmpl.foo = 'world';
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'world');
// Test that you can delete an old-style helper with `delete`.
// As with the previous case, we can break this functionality, but
// we should do it intentionally.
delete tmpl.foo;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '');
});
Tinytest.add("spacebars-tests - template_tests - with data remove (#3130)", function (test) {
var tmpl = Template.spacebars_template_test_with_data_remove;
var div = document.createElement("DIV");
var theWith = Blaze.renderWithData(tmpl, { foo: 3130 }, div);
test.equal(canonicalizeHtml(div.innerHTML), '<b>some data - 3130</b>');
var view = Blaze.getView(div.querySelector('b'));
test.isFalse(theWith.isDestroyed);
Blaze.remove(view);
test.isTrue(theWith.isDestroyed);
test.equal(div.innerHTML, "");
});
Tinytest.add("spacebars-tests - template_tests - inclusion with data remove (#3130)", function (test) {
var tmpl = Template.spacebars_template_test_inclusion_with_data_remove;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), '<span><b>stuff</b></span>');
var view = Blaze.getView(div.querySelector('b'));
var parentView = view.parentView;
test.isTrue(parentView.__isTemplateWith);
test.isFalse(parentView.isDestroyed);
Blaze.remove(view);
test.isTrue(parentView.isDestroyed);
test.equal(canonicalizeHtml(div.innerHTML), "<span></span>");
});