From 9846fb8a286a9c8d9d4d90c92be5789ebaba5910 Mon Sep 17 00:00:00 2001 From: wycats Date: Sun, 28 Nov 2010 17:36:05 -0800 Subject: [PATCH] Got Qunit tests running through RSpec and fixed a bunch of bugs... 33 fails from the original suite to go --- lib/handlebars.js | 9 + lib/handlebars/ast.js | 3 +- lib/handlebars/runtime.js | 6 +- spec/acceptance_spec.rb | 100 +++++++++ spec/qunit_spec.js | 459 ++++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 36 ++- 6 files changed, 599 insertions(+), 14 deletions(-) create mode 100644 spec/acceptance_spec.rb create mode 100644 spec/qunit_spec.js diff --git a/lib/handlebars.js b/lib/handlebars.js index ca080cad..5d53ed59 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -20,4 +20,13 @@ Handlebars.print = function(ast) { return new Handlebars.PrintVisitor().accept(ast); } +Handlebars.compile = function(string) { + var ast = Handlebars.parse(string); + + return function(context, fallback) { + var runtime = new Handlebars.Runtime(context, fallback); + return runtime.accept(ast); + } +} + if(exports) { exports.Handlebars = Handlebars } diff --git a/lib/handlebars/ast.js b/lib/handlebars/ast.js index ca9ffd18..fdd37a30 100644 --- a/lib/handlebars/ast.js +++ b/lib/handlebars/ast.js @@ -32,7 +32,6 @@ Handlebars.AST.ContentNode = function(string) { Handlebars.AST.IdNode = function(parts) { this.type = "ID" - this.parts = parts; var dig = [], depth = 0; @@ -44,7 +43,7 @@ Handlebars.AST.IdNode = function(parts) { else { dig.push(part) } } - this.dig = dig; + this.parts = dig; this.depth = depth; } diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 1e676ea2..ad1ced65 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -96,11 +96,13 @@ Handlebars.Runtime.prototype = { for(var i=0, l=statements.length; i e + return e.backtrace(:javascript) + end + end + + js_context["p"] = proc do |str| + p str + end + + js_context["ok"] = proc do |ok, message| + js_context["$$RSPEC1$$"] = ok + + result = js_context.eval("!!$$RSPEC1$$") + + message ||= "#{ok} was not truthy" + + unless result + backtrace = js_backtrace(js_context) + message << "\n#{backtrace.join("\n")}" + end + + assert result, message + end + + js_context["equals"] = proc do |first, second, message| + js_context["$$RSPEC1$$"] = first + js_context["$$RSPEC2$$"] = second + + result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$") + + message ||= "#{first} did not == #{second}" + + unless result + backtrace = js_backtrace(js_context) + message << "\n#{backtrace.join("\n")}" + end + + assert result, message + end + + js_context["equal"] = js_context["equals"] + + js_context["module"] = proc do |name| + test_context.module(name) + end + + js_context["test"] = proc do |name, function| + test_context.test(name, function) + end + + local = Regexp.escape(File.expand_path(Dir.pwd)) + qunit_spec = File.expand_path("../qunit_spec.js", __FILE__) + js_context.load(qunit_spec.sub(/^#{local}\//, '')) +end + +test_context.modules.each do |mod| + describe mod.name do + mod.tests.each do |name, function| + it name do + function.call + end + end + end +end diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js new file mode 100644 index 00000000..00be18c5 --- /dev/null +++ b/spec/qunit_spec.js @@ -0,0 +1,459 @@ +module("basic context"); + +var helperMissing = function(context, fn) { + if(context === true) { + return fn(this); + } else if(context === false) { + return ""; + } +} + +var shouldCompileTo = function(string, hash, expected, message) { + var template = Handlebars.compile(string); + if(Object.prototype.toString.call(hash) === "[object Array]") { + hash[1].helperMissing = helperMissing; + } else { + hash = [hash, {helperMissing: helperMissing}]; + } + result = template.apply(this, hash) + 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("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(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); +}); + +test("escaping expressions", function() { + shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with 3 handlebars aren't escaped"); + + shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\\\<>', + "by default expressions should be escaped"); + + shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with {{& handlebars aren't 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("functions with context argument", function() { + shouldCompileTo("{{awesome frank}}", + {awesome: function(context) { return context; }, + frank: "Frank"}, + "Frank", "functions are called with context arguments"); +}); + +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("bad idea nested paths", function() { + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldThrow(function() { + Handlebars.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 fallback", 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."); +}); + +test("inverted section using result of function call", function() { + var string = "{{goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {goodbyes: function() { return false; }} + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when result of function in expression is false."); +}); + +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("incorrectly matched blocks", function() { + var string = "{{#goodbyes}}{{/hellos}}"; + + shouldThrow(function() { + Handlebars.compile(string); + }, Handlebars.Exception, "Incorrectly matched blocks return an exception at compile time."); +}); + +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}}}{{/goodbyes}}" + var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var fallback = {link: function() { + return "" + this.text + "" + }}; + shouldCompileTo(string, [hash, fallback], "Goodbye") +}); + +test("helper block with complex lookup expression", function() { + var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}" + var hash = {name: "Alan"}; + var fallback = {goodbyes: function(context, fn) { + var out = ""; + var byes = ["Goodbye", "goodbye", "GOODBYE"]; + for (var i = 0,j = byes.length; i < j; i++) { + out += byes[i] + " " + fn(context) + "! "; + } + return out; + }}; + shouldCompileTo(string, [hash, fallback], "Goodbye Alan! goodbye Alan! GOODBYE Alan! "); +}); + +test("helper with complex lookup and nested template", function() { + var string = "{{#goodbyes}}{{#link}}{{text}}{{/link}}{{/goodbyes}}"; + var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var fallback = {link: function (context, fn) { + return "" + fn(this) + ""; + }}; + shouldCompileTo(string, [hash, fallback], "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 = Handlebars.compile(string); + + result = template({goodbyes: function(context, fn) { return fn({text: "GOODBYE"}); }, world: "world"}); + equal(result, "GOODBYE! cruel world!"); +}); + +test("block helper staying in the same context", function() { + var string = "{{#form}}

{{name}}

{{/form}}" + var template = Handlebars.compile(string); + + result = template({form: function(context, fn) { return "
" + fn(this) + "
" }, name: "Yehuda"}); + equal(result, "

Yehuda

"); +}); + +test("block helper should have wrapped context in this", function() { + var source = ""; + var link = function(context, fn) { + return '' + fn(this) + ''; + }; + var data = { "people": [ + { "name": "Alan", "id": 1 }, + { "name": "Yehuda", "id": 2 } + ]}; + + shouldCompileTo(source, [data, {link: link}], ""); +}); + +test("block helper passing a new context", function() { + var string = "{{#form yehuda}}

{{name}}

{{/form}}" + var template = Handlebars.compile(string); + + result = template({form: function(context, fn) { return "
" + fn(context) + "
" }, yehuda: {name: "Yehuda"}}); + equal(result, "

Yehuda

"); +}); + +test("block helper passing a complex path context", function() { + var string = "{{#form yehuda/cat}}

{{name}}

{{/form}}" + var template = Handlebars.compile(string); + + result = template({form: function(context, fn) { return "
" + fn(context) + "
" }, yehuda: {name: "Yehuda", cat: {name: "Harold"}}}); + equal(result, "

Harold

"); +}); + +test("nested block helpers", function() { + var string = "{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}" + var template = Handlebars.compile(string); + + result = template({form: function(context, fn) { return "
" + fn(context) + "
" }, yehuda: {name: "Yehuda", link: function(context, fn) { return "" + fn(context) + ""; }}}); + equal(result, "

Yehuda

Hello
"); +}); + +test("block inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{../none}}{{/people}}", {none: "No people"}, + "No people"); +}); + +test("block helper inverted sections", function() { + var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}" + var list = function(context, fn) { + console.log(context); + if (context.length > 0) { + var out = ""; + return out; + } + }; + + list.not = function(context, fn) { + return "

" + fn(context, this) + "

"; + }; + var hash = {list: list, people: [{name: "Alan"}, {name: "Yehuda"}]}; + + // 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, "

Nobody's here

", "Not is called when block inverted section is encountered."); +}); + +module("fallback hash"); + +test("providing a fallback hash", function() { + shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: "world"}], "Goodbye cruel world!", + "Fallback hash is available"); + + shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: "world"}], + "Goodbye cruel world!", "Fallback hash is available inside other blocks"); +}); + +test("in cases of conflict, the explicit hash wins", function() { + +}); + +test("the fallback 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"}]}; + shouldCompileTo(string, [hash, {partials: {dude: partial}}], "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"}]}; + shouldCompileTo(string, [hash, {partials: {dude: partial}}], "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"}]}; + shouldCompileTo(string, [hash, {partials: {dude: dude, url: url}}], "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 = Handlebars.compile("{{> whatever}}"); + template(); + }, Handlebars.Exception, "Should throw exception"); +}); + +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"}; + shouldCompileTo(string, [hash, {partials: {dude:dude}}], "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); +}); + +test("Partial containing complex expression", function() { + var template = "Dudes: {{#dudes}}{{> dude}} {{/dudes}}"; + var dude = "{{../salutation}} {{name}}"; + var hash = {salutation: "Mr.", dudes: [{name: "Yehuda"}, {name: "Alan"}]}; + shouldCompileTo(template, [hash, {partials: {dude: dude}}], "Dudes: Mr. Yehuda Mr. Alan "); +}); + +module("String literal parameters"); + +test("simple literals work", function() { + var string = 'Message: {{hello "world"}}'; + var hash = {} + var fallback = {hello: function(param) { return "Hello " + param; }} + shouldCompileTo(string, [hash, fallback], "Message: Hello world", "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"}}'; + Handlebars.compile(string); + }, Handlebars.Exception, "should throw exception"); +}); + +test("escaping a String is possible", function(){ + var string = 'Message: {{hello "\\"world\\""}}'; + var hash = {} + var fallback = {hello: function(param) { return "Hello " + param; }} + shouldCompileTo(string, [hash, fallback], "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 fallback = {hello: function(param) { return "Hello " + param; }} + shouldCompileTo(string, [hash, fallback], "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 fallback = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }} + shouldCompileTo(string, [hash, fallback], "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 fallback = {goodbye: function(cruel, world, fn) { + return fn({greeting: "Goodbye", adj: "cruel", noun: "world"}); + }} + shouldCompileTo(string, [hash, fallback], "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"); +}); + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d8fe50b..36d2aece 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,17 +27,31 @@ class V8::JSError end end -RSpec.configure do |config| - config.before(:all) do - @context = V8::Context.new - @context['exports'] = nil - @context.eval("Handlebars = {}"); +module Handlebars + module Spec + CONTEXT = V8::Context.new + CONTEXT.instance_eval do |context| + context.eval("exports = null") + context.eval("Handlebars = {}") - @context.load('lib/handlebars/ast.js') - @context.load('lib/handlebars/jison_ext.js') - @context.load('lib/handlebars/handlebars_lexer.js') - @context.load('lib/handlebars/printer.js') - @context.load('lib/handlebars/parser.js') - @context.load('lib/handlebars.js') + context.load('lib/handlebars/ast.js') + context.load('lib/handlebars/jison_ext.js') + context.load('lib/handlebars/handlebars_lexer.js') + context.load('lib/handlebars/printer.js') + context.load('lib/handlebars/parser.js') + context.load('lib/handlebars/runtime.js') + context.load('lib/handlebars.js') + end + end +end + + +require "test/unit/assertions" + +RSpec.configure do |config| + config.include Test::Unit::Assertions + + config.before(:all) do + @context = Handlebars::Spec::CONTEXT end end