module("basic context"); Handlebars.registerHelper('helperMissing', function(helper, context) { if(helper === "link_to") { return new Handlebars.SafeString("" + context + ""); } }); var shouldCompileTo = function(string, hashOrArray, expected, message) { shouldCompileToWithPartials(string, hashOrArray, false, expected, message); }; var shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { helpers = hashOrArray[1]; if(helpers) { for(var prop in Handlebars.helpers) { helpers[prop] = Handlebars.helpers[prop]; } } ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); } else { ary = [hashOrArray]; } result = template.apply(this, ary); equal(result, expected, "'" + expected + "' should === '" + result + "': " + message); }; var shouldThrow = function(fn, exception, message) { var caught = false; try { fn(); } catch (e) { if (e instanceof exception) { caught = true; } } ok(caught, message || null); } test("most basic", function() { shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); }); test("compiling with a basic context", function() { shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", "It works if all the required keys are provided"); }); test("comments", function() { shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", "comments are ignored"); }); test("boolean", function() { var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", "booleans show the contents when true"); shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", "booleans do not show the contents when false"); }); test("zeros", function() { shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, "num1: 42, num2: 0"); shouldCompileTo("num: {{.}}", 0, "num: 0"); shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); }); test("newlines", function() { shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest"); shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); }); test("escaping text", function() { shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes"); shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored"); shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes"); shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes"); shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); }); test("escaping expressions", function() { shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', "expressions with 3 handlebars aren't escaped"); shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', "expressions with {{& handlebars aren't escaped"); shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', "by default expressions should be escaped"); }); test("functions returning safestrings shouldn't be escaped", function() { var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; shouldCompileTo("{{awesome}}", hash, '&\"\\<>', "functions returning safestrings aren't escaped"); }); test("functions", function() { shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", "functions are called and render their output"); }); test("paths with hyphens", function() { shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); }); test("nested paths", function() { shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, "Goodbye beautiful world!", "Nested paths access nested objects"); }); test("nested paths with empty string value", function() { shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, "Goodbye world!", "Nested paths access nested objects with empty string"); }); test("literal paths", function() { shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}}, "Goodbye beautiful world!", "Literal paths can be used"); }); test("--- TODO --- bad idea nested paths", function() { return; var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldThrow(function() { CompilerContext.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash); }, Handlebars.Exception, "Cannot jump (..) into previous context after moving into a context."); var string = "{{#goodbyes}}{{.././world}} {{/goodbyes}}"; shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths"); }); test("that current context path ({{.}}) doesn't hit helpers", function() { shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: "); }); test("complex but empty paths", function() { shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); shouldCompileTo("{{person/name}}", {person: {}}, ""); }); test("this keyword in paths", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}"; var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", "This keyword in paths evaluates to current context"); string = "{{#hellos}}{{this/text}}{{/hellos}}" hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); }); module("inverted sections"); test("inverted sections with unset value", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var hash = {}; shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set."); }); test("inverted section with false value", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var hash = {goodbyes: false}; shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false."); }); test("inverted section with empty set", function() { var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var hash = {goodbyes: []}; shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set."); }); module("blocks"); test("array", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!" var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", "Arrays iterate over the contents when not empty"); shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", "Arrays ignore the contents when empty"); }); test("empty block", function() { var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!" var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldCompileTo(string, hash, "cruel world!", "Arrays iterate over the contents when not empty"); shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", "Arrays ignore the contents when empty"); }); test("nested iteration", function() { }); test("block with complex lookup", function() { var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", "Templates can access variables in contexts up the stack with relative path syntax"); }); test("helper with complex lookup$", function() { var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var helpers = {link: function(prefix) { return "" + this.text + ""; }}; shouldCompileTo(string, [hash, helpers], "Goodbye"); }); test("helper block with complex lookup expression", function() { var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; var hash = {name: "Alan"}; var helpers = {goodbyes: function(options) { var out = ""; var byes = ["Goodbye", "goodbye", "GOODBYE"]; for (var i = 0,j = byes.length; i < j; i++) { out += byes[i] + " " + options.fn(this) + "! "; } return out; }}; shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! "); }); test("helper with complex lookup and nested template", function() { var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var helpers = {link: function (prefix, options) { return "" + options.fn(this) + ""; }}; shouldCompileToWithPartials(string, [hash, helpers], false, "Goodbye"); }); test("helper with complex lookup and nested template in VM+Compiler", function() { var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var helpers = {link: function (prefix, options) { return "" + options.fn(this) + ""; }}; shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye"); }); test("block with deep nested complex lookup", function() { var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; shouldCompileTo(string, hash, "Goodbye cruel OMG!"); }); test("block helper", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; var template = CompilerContext.compile(string); result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); equal(result, "GOODBYE! cruel world!", "Block helper executed"); }); test("block helper staying in the same context", function() { var string = "{{#form}}
{{name}}
{{/form}}"; var template = CompilerContext.compile(string); result = template({name: "Yehuda"}, {helpers: {form: function(options) { return ""; } }}); equal(result, "", "Block helper executed with current context"); }); test("block helper should have context in this", function() { var source = "{{name}}
{{/form}}"; var template = CompilerContext.compile(string); result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return ""; }}}); equal(result, "", "Context variable resolved"); }); test("block helper passing a complex path context", function() { var string = "{{#form yehuda/cat}}{{name}}
{{/form}}"; var template = CompilerContext.compile(string); result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return ""; }}}); equal(result, "", "Complex path variable resolved"); }); test("nested block helpers", function() { var string = "{{#form yehuda}}{{name}}
{{#link}}Hello{{/link}}{{/form}}"; var template = CompilerContext.compile(string); result = template({ yehuda: {name: "Yehuda" } }, { helpers: { link: function(options) { return "" + options.fn(this) + ""; }, form: function(context, options) { return ""; } } }); equal(result, "", "Both blocks executed"); }); test("block inverted sections", function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, "No people"); }); test("block inverted sections with empty arrays", function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, "No people"); }); test("block helper inverted sections", function() { var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; var list = function(context, options) { if (context.length > 0) { var out = "" + options.inverse(this) + "
"; } }; var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]}; var empty = {people: []}; var rootMessage = { people: [], message: "Nobody's here" }; var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}"; // the meaning here may be kind of hard to catch, but list.not is always called, // so we should see the output of both shouldCompileTo(string, [hash, { list: list }], "Nobody's here
", "an inverse wrapper can be optionally called"); shouldCompileTo(messageString, [rootMessage, { list: list }], "Nobody's here
", "the context of an inverse is the parent of the block"); }); module("helpers hash"); test("providing a helpers hash", function() { shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", "helpers hash is available"); shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], "Goodbye cruel world!", "helpers hash is available inside other blocks"); }); test("in cases of conflict, the explicit hash wins", function() { }); test("the helpers hash is available is nested contexts", function() { }); module("partials"); test("basic partials", function() { var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; var partial = "{{name}} ({{url}}) "; var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", "Basic partials output based on current context."); }); test("partials with context", function() { var string = "Dudes: {{>dude dudes}}"; var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", "Partials can be passed a context"); }); test("partial in a partial", function() { var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; var dude = "{{name}} {{> url}} "; var url = "{{url}}"; var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda http://yehuda Alan http://alan ", "Partials are rendered inside of other partials"); }); test("rendering undefined partial throws an exception", function() { shouldThrow(function() { var template = CompilerContext.compile("{{> whatever}}"); template(); }, Handlebars.Exception, "Should throw exception"); }); test("rendering template partial in vm mode throws an exception", function() { shouldThrow(function() { var template = CompilerContext.compile("{{> whatever}}"); var string = "Dudes: {{>dude}} {{another_dude}}"; var dude = "{{name}}"; var hash = {name:"Jeepers", another_dude:"Creepers"}; template(); }, Handlebars.Exception, "Should throw exception"); }); test("rendering function partial in vm mode", function() { var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; var partial = function(context) { return context.name + ' (' + context.url + ') '; }; var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", "Function partials output based in VM."); }); test("GH-14: a partial preceding a selector", function() { var string = "Dudes: {{>dude}} {{another_dude}}"; var dude = "{{name}}"; var hash = {name:"Jeepers", another_dude:"Creepers"}; shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); }); test("Partials with literal paths", function() { var string = "Dudes: {{> [dude]}}"; var dude = "{{name}}"; var hash = {name:"Jeepers", another_dude:"Creepers"}; shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); }); module("String literal parameters"); test("simple literals work", function() { var string = 'Message: {{hello "world" 12 true false}}'; var hash = {}; var helpers = {hello: function(param, times, bool1, bool2) { if(typeof times !== 'number') { times = "NaN"; } if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2; }} shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal"); }); test("using a quote in the middle of a parameter raises an error", function() { shouldThrow(function() { var string = 'Message: {{hello wo"rld"}}'; CompilerContext.compile(string); }, Error, "should throw exception"); }); test("escaping a String is possible", function(){ var string = 'Message: {{{hello "\\"world\\""}}}'; var hash = {} var helpers = {hello: function(param) { return "Hello " + param; }} shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal"); }); test("it works with ' marks", function() { var string = 'Message: {{{hello "Alan\'s world"}}}'; var hash = {}; var helpers = {hello: function(param) { return "Hello " + param; }}; shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); }); module("multiple parameters"); test("simple multi-params work", function() { var string = 'Message: {{goodbye cruel world}}'; var hash = {cruel: "cruel", world: "world"}; var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); }); test("block multi-params work", function() { var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; var hash = {cruel: "cruel", world: "world"}; var helpers = {goodbye: function(cruel, world, options) { return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); }}; shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); }); module("safestring"); test("constructing a safestring from a string and checking its type", function() { var safe = new Handlebars.SafeString("testing 1, 2, 3"); ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString"); equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string"); }); module("helperMissing"); test("if a context is not found, helperMissing is used", function() { var string = "{{hello}} {{link_to world}}"; var context = { hello: "Hello", world: "world" }; shouldCompileTo(string, context, "Hello world"); }); module("knownHelpers"); test("Known helper should render helper", function() { var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}}) var result = template({}, {helpers: {hello: function() { return "foo"; }}}); equal(result, "foo", "'foo' should === '" + result); }); test("Unknown helper in knownHelpers only mode should be passed as undefined", function() { var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true}) var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}}); equal(result, "undefined", "'undefined' should === '" + result); }); test("Builtin helpers available in knownHelpers only mode", function() { var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true}) var result = template({}); equal(result, "bar", "'bar' should === '" + result); }); test("Field lookup works in knownHelpers only mode", function() { var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}) var result = template({foo: 'bar'}); equal(result, "bar", "'bar' should === '" + result); }); test("Conditional blocks work in knownHelpers only mode", function() { var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true}) var result = template({foo: 'baz'}); equal(result, "bar", "'bar' should === '" + result); }); test("Invert blocks work in knownHelpers only mode", function() { var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true}) var result = template({foo: false}); equal(result, "bar", "'bar' should === '" + result); }); module("blockHelperMissing"); test("lambdas are resolved by blockHelperMissing, not handlebars proper", function() { var string = "{{#truthy}}yep{{/truthy}}"; var data = { truthy: function() { return true; } }; shouldCompileTo(string, data, "yep"); }); var teardown; module("built-in helpers", { setup: function(){ teardown = null; }, teardown: function(){ if (teardown) { teardown(); } } }); test("with", function() { var string = "{{#with person}}{{first}} {{last}}{{/with}}"; shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); }); test("if", function() { var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", "if with boolean argument shows the contents when true"); shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", "if with string argument shows the contents"); shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", "if with boolean argument does not show the contents when false"); shouldCompileTo(string, {world: "world"}, "cruel world!", "if with undefined does not show the contents"); shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", "if with non-empty array shows the contents"); shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", "if with empty array does not show the contents"); }); test("if with function argument", function() { var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; shouldCompileTo(string, {goodbye: function() {return true}, world: "world"}, "GOODBYE cruel world!", "if with function shows the contents when function returns true"); shouldCompileTo(string, {goodbye: function() {return this.world}, world: "world"}, "GOODBYE cruel world!", "if with function shows the contents when function returns string"); shouldCompileTo(string, {goodbye: function() {return false}, world: "world"}, "cruel world!", "if with function does not show the contents when returns false"); shouldCompileTo(string, {goodbye: function() {return this.foo}, world: "world"}, "cruel world!", "if with function does not show the contents when returns undefined"); }); test("each", function() { var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", "each with array argument iterates over the contents when not empty"); shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", "each with array argument ignores the contents when empty"); }); test("each with @index", function() { var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var template = CompilerContext.compile(string); var result = template(hash); equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); }); test("log", function() { var string = "{{log blah}}" var hash = { blah: "whee" }; var logArg; var originalLog = Handlebars.log; Handlebars.log = function(arg){ logArg = arg; } teardown = function(){ Handlebars.log = originalLog; } shouldCompileTo(string, hash, "", "log should not display"); equals("whee", logArg, "should call log with 'whee'"); }); test("overriding property lookup", function() { }); test("passing in data to a compiled function that expects data - works with helpers", function() { var template = CompilerContext.compile("{{hello}}", {data: true}); var helpers = { hello: function(options) { return options.data.adjective + " " + this.noun; } }; var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}}); equals("happy cat", result, "Data output by helper"); }); test("data can be looked up via @foo", function() { var template = CompilerContext.compile("{{@hello}}"); var result = template({}, { data: { hello: "hello" } }); equals("hello", result, "@foo retrieves template data"); }); var objectCreate = Handlebars.createFrame; test("deep @foo triggers automatic top-level data", function() { var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'); var helpers = objectCreate(Handlebars.helpers); helpers.let = function(options) { var frame = Handlebars.createFrame(options.data); for (var prop in options.hash) { frame[prop] = options.hash[prop]; } return options.fn(this, { data: frame }); }; var result = template({ foo: true }, { helpers: helpers }); equals("Hello world", result, "Automatic data was triggered"); }); test("parameter data can be looked up via @foo", function() { var template = CompilerContext.compile("{{hello @world}}"); var helpers = { hello: function(noun) { return "Hello " + noun; } }; var result = template({}, { helpers: helpers, data: { world: "world" } }); equals("Hello world", result, "@foo as a parameter retrieves template data"); }); test("hash values can be looked up via @foo", function() { var template = CompilerContext.compile("{{hello noun=@world}}"); var helpers = { hello: function(options) { return "Hello " + options.hash.noun; } }; var result = template({}, { helpers: helpers, data: { world: "world" } }); equals("Hello world", result, "@foo as a parameter retrieves template data"); }); test("data is inherited downstream", function() { var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true }); var helpers = { let: function(options) { for (var prop in options.hash) { options.data[prop] = options.hash[prop]; } return options.fn(this); } }; var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); equals("hello world", result, "data variables are inherited downstream"); }); test("passing in data to a compiled function that expects data - works with helpers in partials", function() { var template = CompilerContext.compile("{{>my_partial}}", {data: true}); var partials = { my_partial: CompilerContext.compile("{{hello}}", {data: true}) }; var helpers = { hello: function(options) { return options.data.adjective + " " + this.noun; } }; var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}}); equals("happy cat", result, "Data output by helper inside partial"); }); test("passing in data to a compiled function that expects data - works with helpers and parameters", function() { var template = CompilerContext.compile("{{hello world}}", {data: true}); var helpers = { hello: function(noun, options) { return options.data.adjective + " " + noun + (this.exclaim ? "!" : ""); } }; var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}}); equals("happy world!", result, "Data output by helper"); }); test("passing in data to a compiled function that expects data - works with block helpers", function() { var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); var helpers = { hello: function(options) { return options.fn(this); }, world: function(options) { return options.data.adjective + " world" + (this.exclaim ? "!" : ""); } }; var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}}); equals("happy world!", result, "Data output by helper"); }); test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() { var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { hello: function(options) { return options.fn({exclaim: "?"}); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); } }; var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); equals("happy world?", result, "Data output by helper"); }); test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() { var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { hello: function(options) { return options.data.accessData + " " + options.fn({exclaim: "?"}); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); } }; var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}}); equals("#win happy world?", result, "Data output by helper"); }); test("you can override inherited data when invoking a helper", function() { var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); var helpers = { hello: function(options) { return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); } }; var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}}); equals("sad world?", result, "Overriden data output by helper"); }); test("you can override inherited data when invoking a helper with depth", function() { var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { hello: function(options) { return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); } }; var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); equals("sad world?", result, "Overriden data output by helper"); }); test("helpers take precedence over same-named context properties", function() { var template = CompilerContext.compile("{{goodbye}} {{cruel world}}"); var helpers = { goodbye: function() { return this.goodbye.toUpperCase(); }, cruel: function(world) { return "cruel " + world.toUpperCase(); } }; var context = { goodbye: "goodbye", world: "world" }; var result = template(context, {helpers: helpers}); equals(result, "GOODBYE cruel WORLD", "Helper executed"); }); test("helpers take precedence over same-named context properties$", function() { var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); var helpers = { goodbye: function(options) { return this.goodbye.toUpperCase() + options.fn(this); }, cruel: function(world) { return "cruel " + world.toUpperCase(); } }; var context = { goodbye: "goodbye", world: "world" }; var result = template(context, {helpers: helpers}); equals(result, "GOODBYE cruel WORLD", "Helper executed"); }); test("Scoped names take precedence over helpers", function() { var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}"); var helpers = { goodbye: function() { return this.goodbye.toUpperCase(); }, cruel: function(world) { return "cruel " + world.toUpperCase(); }, }; var context = { goodbye: "goodbye", world: "world" }; var result = template(context, {helpers: helpers}); equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed"); }); test("Scoped names take precedence over block helpers", function() { var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); var helpers = { goodbye: function(options) { return this.goodbye.toUpperCase() + options.fn(this); }, cruel: function(world) { return "cruel " + world.toUpperCase(); }, }; var context = { goodbye: "goodbye", world: "world" }; var result = template(context, {helpers: helpers}); equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); }); test("helpers can take an optional hash", function() { var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}'); var helpers = { goodbye: function(options) { return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES"; } }; var context = {}; var result = template(context, {helpers: helpers}); equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash"); }); test("helpers can take an optional hash with booleans", function() { var helpers = { goodbye: function(options) { if (options.hash.print === true) { return "GOODBYE " + options.hash.cruel + " " + options.hash.world; } else if (options.hash.print === false) { return "NOT PRINTING"; } else { return "THIS SHOULD NOT HAPPEN"; } } }; var context = {}; var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}'); var result = template(context, {helpers: helpers}); equals(result, "GOODBYE CRUEL WORLD", "Helper output hash"); template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}'); result = template(context, {helpers: helpers}); equals(result, "NOT PRINTING", "Boolean helper parameter honored"); }); test("block helpers can take an optional hash", function() { var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'); var helpers = { goodbye: function(options) { return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; } }; var result = template({}, {helpers: helpers}); equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); }); test("block helpers can take an optional hash with booleans", function() { var helpers = { goodbye: function(options) { if (options.hash.print === true) { return "GOODBYE " + options.hash.cruel + " " + options.fn(this); } else if (options.hash.print === false) { return "NOT PRINTING"; } else { return "THIS SHOULD NOT HAPPEN"; } } }; var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}'); var result = template({}, {helpers: helpers}); equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored"); var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'); var result = template({}, {helpers: helpers}); equals(result, "NOT PRINTING", "Boolean hash parameter honored"); }); test("arguments to helpers can be retrieved from options hash in string form", function() { var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); var helpers = { wycats: function(passiveVoice, noun, options) { return "HELP ME MY BOSS " + passiveVoice + ' ' + noun; } }; var result = template({}, {helpers: helpers}); equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output"); }); test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() { var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true}); var helpers = { wycats: function(passiveVoice, noun, options) { return "HELP ME MY BOSS " + passiveVoice + ' ' + noun + ': ' + options.fn(this); } }; var result = template({}, {helpers: helpers}); equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output"); }); test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() { var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true}); var helpers = { tomdale: function(desire, noun, options) { return "STOP ME FROM READING HACKER NEWS I " + options.contexts[0][desire] + " " + noun; }, "with": function(context, options) { return options.fn(options.contexts[0][context]); } }; var result = template({ dale: {}, need: 'need-a' }, {helpers: helpers}); equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output"); }); test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() { var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true}); var helpers = { tomdale: function(desire, noun, options) { return "STOP ME FROM READING HACKER NEWS I " + options.contexts[0][desire] + " " + noun + " " + options.fn(this); }, "with": function(context, options) { return options.fn(options.contexts[0][context]); } }; var result = template({ dale: {}, need: 'need-a' }, {helpers: helpers}); equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output"); }); module("Regressions") test("GH-94: Cannot read property of undefined", function() { var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]}; var string = "{{#books}}{{title}}{{author.name}}{{/books}}"; shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes", "Renders without an undefined property error"); }); test("GH-150: Inverted sections print when they shouldn't", function() { var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}"; shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context"); shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined"); shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false"); shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true"); }); test("Mustache man page", function() { var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}" var data = { "name": "Chris", "value": 10000, "taxed_value": 10000 - (10000 * 0.4), "in_ca": true } shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works"); }); test("GH-158: Using array index twice, breaks the template", function() { var string = "{{arr.[0]}}, {{arr.[1]}}"; var data = { "arr": [1,2] }; shouldCompileTo(string, data, "1, 2", "it works as expected"); }); test("bug reported by @fat where lambdas weren't being properly resolved", function() { var string = "This is a slightly more complicated {{thing}}..\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n