Files
meteor/packages/spacebars-tests/template_tests.js
2015-02-11 18:10:33 -08:00

3227 lines
98 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 inDocument = function (elem) {
while ((elem = elem.parentNode)) {
if (elem == document) {
return true;
}
}
return false;
};
var clickIt = function (elem) {
if (!inDocument(elem))
throw new Error("Can't click on elements without first adding them to the document");
// 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");
};
subtmplCopy.onRendered(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>");
});
Tinytest.add("spacebars-tests - template_tests - custom block helper doesn't break Template.instance() (#3540)", function (test) {
var tmpl = Template.spacebars_template_test_template_instance_wrapper_outer;
tmpl.helpers({
thisShouldOutputHello: function () {
return Template.instance().customProp;
}
});
tmpl.created = function () {
this.customProp = "hello";
};
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), "hello hello");
});
Tinytest.add(
"spacebars-tests - template_tests - currentData and parentData in event handlers and helpers",
function (test) {
var tmpl = Template.spacebars_template_test_currentData_and_parentData_in_events;
var clicked = false;
var currentInEvent;
var parentInEvent;
var currentInHelper;
var parentInHelper;
tmpl.events({
'click button': function () {
currentInEvent = Template.currentData();
parentInEvent = Template.parentData(1);
}
});
tmpl.helpers({
label: function () {
currentInHelper = Template.currentData();
parentInHelper = Template.parentData(1);
}
});
var div = renderToDiv(tmpl);
var button = div.querySelector('button');
document.body.appendChild(div);
clickIt(button);
test.equal(currentInEvent, {y: 2});
test.equal(parentInEvent, {x: 1});
test.equal(currentInHelper, {y: 2});
test.equal(parentInHelper, {x: 1});
document.body.removeChild(div);
});
testAsyncMulti("spacebars-tests - template_tests - template-level subscriptions", [
function (test, expect) {
var tmpl = Template.spacebars_template_test_template_level_subscriptions;
var tmplInstance;
var div;
// Make sure the subscriptions stop when the template is destroyed
var stopCallback = expect();
var stopCallback2 = expect();
// Make sure the HTML is what we expect when one subscription is ready
var checkOneReady = expect(function () {
test.equal(canonicalizeHtml(div.innerHTML), "");
});
// Make sure the HTML is what we expect when both subscriptions are ready
var checkBothReady = expect(function () {
test.equal(canonicalizeHtml(div.innerHTML), "ready! true");
// This will call the template's destroyed callback
Blaze.remove(tmplInstance.view);
});
var subscriptionsFinished = 0;
// Manually use the subscribe ready callback to make sure the template is
// doing the right thing
var subscribeCallback = expect(function () {
subscriptionsFinished++;
if (subscriptionsFinished === 1) {
// We need to use Tracker.afterFlush here and Tracker.flush doesn't work
// because we need to wait for the other callback to fire (the one that
// makes ready return true) _and_ we need the UI to rerender
Tracker.afterFlush(checkOneReady);
}
if (subscriptionsFinished === 2) {
Tracker.afterFlush(checkBothReady);
}
});
tmpl.onCreated(function () {
subHandle = this.subscribe("items", subscribeCallback);
subHandle2 = this.subscribe("items", 50, subscribeCallback);
subHandle.stop = stopCallback;
subHandle2.stop = stopCallback2;
tmplInstance = this;
});
// Insertion point
div = renderToDiv(tmpl);
// To start, there is no content because the template isn't ready
test.equal(canonicalizeHtml(div.innerHTML), "");
}
]);
testAsyncMulti("spacebars-tests - template_tests - template-level subscriptions don't resubscribe unnecessarily", [
function (test, expect) {
var tmpl = Template.spacebars_template_test_template_level_subs_resubscribe;
var subHandle;
var trueThenFalse = new ReactiveVar(true);
tmpl.helpers({
ifArg: function () {
return trueThenFalse.get();
},
subscribingHelper1: expect(function () {
subHandle = Template.instance().subscribe("items", 100);
}),
subscribingHelper2: expect(function () {
var subHandle2 = Template.instance().subscribe("items", 100);
test.isTrue(subHandle.subscriptionId === subHandle2.subscriptionId);
// Make sure we didn't add two subscription handles to our internal list
test.equal(_.keys(Template.instance()._subscriptionHandles).length, 1);
})
});
renderToDiv(tmpl);
Tracker.flush();
trueThenFalse.set(false);
}
]);