diff --git a/Gemfile b/Gemfile index 5d32bfd68..c8cb3fd3b 100644 --- a/Gemfile +++ b/Gemfile @@ -118,6 +118,7 @@ group :test, :development do gem 'rspec-given' gem 'pry-rails' gem 'pry-nav' + gem 'webrick' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index eae4c8a27..528119431 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -472,6 +472,7 @@ GEM uglifier (2.0.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) + webrick (1.3.1) PLATFORMS ruby @@ -572,3 +573,4 @@ DEPENDENCIES turbo-sprockets-rails3 uglifier vestal_versions! + webrick diff --git a/app/assets/javascripts/discourse/mixins/selected_posts_count.js b/app/assets/javascripts/discourse/mixins/selected_posts_count.js index 38425c940..e9e6b5496 100644 --- a/app/assets/javascripts/discourse/mixins/selected_posts_count.js +++ b/app/assets/javascripts/discourse/mixins/selected_posts_count.js @@ -9,10 +9,10 @@ Discourse.SelectedPostsCount = Em.Mixin.create({ selectedPostsCount: function() { - if (!this.get('selectedPosts')) return 0; - if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count'); + if (!this.get('selectedPosts')) return 0; + return this.get('selectedPosts.length'); }.property('selectedPosts.length', 'allPostsSelected') diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js index 2812412f9..622d19cf3 100644 --- a/app/assets/javascripts/discourse/models/post.js +++ b/app/assets/javascripts/discourse/models/post.js @@ -293,7 +293,7 @@ Discourse.Post.reopenClass({ create: function(obj, topic) { var result = this._super(obj); this.createActionSummary(result); - if (obj.reply_to_user) { + if (obj && obj.reply_to_user) { result.set('reply_to_user', Discourse.User.create(obj.reply_to_user)); } result.set('topic', topic); diff --git a/lib/discourse_iife.rb b/lib/discourse_iife.rb index 30e8f7bde..867c11b94 100644 --- a/lib/discourse_iife.rb +++ b/lib/discourse_iife.rb @@ -6,9 +6,12 @@ class DiscourseIIFE < Sprockets::Processor path = context.pathname.to_s # Only discourse or admin paths - return data unless (path =~ /\/javascripts\/discourse/ || path =~ /\/javascripts\/admin/) + return data unless (path =~ /\/javascripts\/discourse/ || path =~ /\/javascripts\/admin/ || path =~ /\/test\/javascripts/) - # Ugh, ignore translations + # Ignore the js helper + return data if (path =~ /test\_helper\.js/) + + # Ignore translations return data if (path =~ /\/translations/) # We don't add IIFEs to handlebars diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake new file mode 100644 index 000000000..b75296a66 --- /dev/null +++ b/lib/tasks/qunit.rake @@ -0,0 +1,48 @@ +desc "Runs the qunit test suite" + +task "qunit:test" => :environment do + + require "rack" + require "webrick" + + unless %x{which phantomjs > /dev/null 2>&1} + abort "PhantomJS is not installed. Download from http://phantomjs.org" + end + + port = ENV['TEST_SERVER_PORT'] || 60099 + server = Thread.new do + Rack::Server.start(:config => "config.ru", + :Logger => WEBrick::Log.new("/dev/null"), + :AccessLog => [], + :Port => port) + end + + begin + success = true + test_path = "#{Rails.root}/vendor/assets/javascripts" + cmd = "phantomjs #{test_path}/run-qunit.js \"http://localhost:#{port}/qunit\"" + + rake_system(cmd) + + # A bit of a hack until we can figure this out on Travis + tries = 0 + while tries < 3 && $?.exitstatus === 124 + tries += 1 + puts "\nTimed Out. Trying again...\n" + sh(cmd) + end + + success &&= $?.success? + + ensure + server.kill + end + + if success + puts "\nTests Passed" + else + puts "\nTests Failed" + exit(1) + end + +end \ No newline at end of file diff --git a/spec/javascripts/components/bbcode_spec.js b/spec/javascripts/components/bbcode_spec.js deleted file mode 100644 index db2c5a57e..000000000 --- a/spec/javascripts/components/bbcode_spec.js +++ /dev/null @@ -1,87 +0,0 @@ -/*global waitsFor:true expect:true describe:true beforeEach:true it:true md5:true */ - -describe("Discourse.BBCode", function() { - - var format = Discourse.BBCode.format; - - describe("quoting", function() { - - // Format text without an avatar lookup - function formatQuote(text) { - return format(text, {lookupAvatar: false}); - } - - it("can quote", function() { - expect(formatQuote("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]")). - toBe("</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n " + - "<div class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</blockquote>\n</aside>\n<p>"); - }); - - it("can nest quotes", function() { - expect(formatQuote("[quote=\"eviltrout, post:1, topic:1\"]abc[quote=\"eviltrout, post:2, topic:2\"]nested[/quote][/quote]")). - toBe("</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div " + - "class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</p><aside " + - "class='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-" + - "controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>nested</blockquote>\n</aside>\n<p></blockquote>\n</aside>\n<p>"); - }); - - it("can handle more than one quote", function() { - expect(formatQuote("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after")). - toBe("before</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-cont" + - "rols'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>first</blockquote>\n</aside>\n<p>middle</p><aside cla" + - "ss='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-controls'></div>\n \n " + - "eviltrout\n said:\n </div>\n <blockquote>second</blockquote>\n</aside>\n<p>after"); - }); - - describe("extractQuotes", function() { - - var extractQuotes = Discourse.BBCode.extractQuotes; - - it("returns an object a template renderer", function() { - var q = "[quote=\"eviltrout, post:1, topic:2\"]hello[/quote]"; - var result = extractQuotes(q + " world"); - - expect(result.text).toBe(md5(q) + "\n world"); - expect(result.template).not.toBe(null); - }); - - }); - - describe("buildQuoteBBCode", function() { - - var build = Discourse.BBCode.buildQuoteBBCode; - - var post = Discourse.Post.create({ - cooked: "<p><b>lorem</b> ipsum</p>", - username: "eviltrout", - post_number: 1, - topic_id: 2 - }); - - it("returns an empty string when contents is undefined", function() { - expect(build(post, undefined)).toBe(""); - expect(build(post, null)).toBe(""); - expect(build(post, "")).toBe(""); - }); - - it("returns the quoted contents", function() { - expect(build(post, "lorem")).toBe("[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n"); - }); - - it("trims white spaces before & after the quoted contents", function() { - expect(build(post, " lorem ")).toBe("[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n"); - }); - - it("marks quotes as full when the quote is the full message", function() { - expect(build(post, "lorem ipsum")).toBe("[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n"); - }); - - it("keeps BBCode formatting", function() { - expect(build(post, "**lorem** ipsum")).toBe("[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n"); - }); - - }); - - }); - -}); diff --git a/test/javascripts/components/bbcode_test.js b/test/javascripts/components/bbcode_test.js index e0fef9c33..ee8253329 100644 --- a/test/javascripts/components/bbcode_test.js +++ b/test/javascripts/components/bbcode_test.js @@ -1,9 +1,9 @@ -/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true */ +/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true present:true md5:true */ module("Discourse.BBCode"); var format = function(input, expected, text) { - equal(Discourse.BBCode.format(input), expected, text); + equal(Discourse.BBCode.format(input, {lookupAvatar: false}), expected, text); } test('basic bbcode', function() { @@ -36,4 +36,74 @@ test('tags with arguments', function() { format("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title"); format("[u][i]abc[/i][/u]", "<span class='bbcode-u'><span class='bbcode-i'>abc</span></span>", "can nest tags"); format("[b]first[/b] [b]second[/b]", "<span class='bbcode-b'>first</span> <span class='bbcode-b'>second</span>", "can bold two things on the same line"); -}); \ No newline at end of file +}); + + +test("quotes", function() { + + var post = Discourse.Post.create({ + cooked: "<p><b>lorem</b> ipsum</p>", + username: "eviltrout", + post_number: 1, + topic_id: 2 + }); + + var formatQuote = function(val, expected, text) { + equal(Discourse.BBCode.buildQuoteBBCode(post, val), expected, text); + } + + formatQuote(undefined, "", "empty string for undefined content"); + formatQuote(null, "", "empty string for null content"); + formatQuote("", "", "empty string for empty string content"); + + formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes"); + + formatQuote(" lorem \t ", + "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", + "trims white spaces before & after the quoted contents"); + + formatQuote("lorem ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n", + "marks quotes as full when the quote is the full message"); + + formatQuote("**lorem** ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n", + "keeps BBCode formatting"); + +}); + +test("quote formatting", function() { + + // TODO: This HTML matching is quite ugly. + format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]", + "</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n " + + "<div class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</blockquote>\n</aside>\n<p>", + "renders quotes properly"); + + format("[quote=\"eviltrout, post:1, topic:1\"]abc[quote=\"eviltrout, post:2, topic:2\"]nested[/quote][/quote]", + "</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div " + + "class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</p><aside " + + "class='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-" + + "controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>nested</blockquote>\n</aside>\n<p></blockquote>\n</aside>\n<p>", + "can nest quotes"); + + format("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after", + "before</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-cont" + + "rols'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>first</blockquote>\n</aside>\n<p>middle</p><aside cla" + + "ss='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-controls'></div>\n \n " + + "eviltrout\n said:\n </div>\n <blockquote>second</blockquote>\n</aside>\n<p>after", + "can handle more than one quote"); + +}); + + +test("extract quotes", function() { + + var q = "[quote=\"eviltrout, post:1, topic:2\"]hello[/quote]"; + var result = Discourse.BBCode.extractQuotes(q + " world"); + + equal(result.text, md5(q) + "\n world"); + present(result.template); + +}); + diff --git a/test/javascripts/mixins/presence_test.js b/test/javascripts/mixins/presence_test.js index f6e0816b0..abaf493c0 100644 --- a/test/javascripts/mixins/presence_test.js +++ b/test/javascripts/mixins/presence_test.js @@ -7,7 +7,7 @@ var testObj = Em.Object.createWithMixins(Discourse.Presence, { nonEmptyString: "Evil Trout", emptyArray: [], nonEmptyArray: [1, 2, 3], - age: 34, + age: 34 }); test("present", function() { @@ -18,7 +18,6 @@ test("present", function() { ok(testObj.present('age'), "integers are present"); }); - test("blank", function() { ok(testObj.blank('emptyString'), "Empty strings are blank"); ok(!testObj.blank('nonEmptyString'), "Non empty strings are not blank"); diff --git a/test/javascripts/mixins/selected_posts_count_test.js b/test/javascripts/mixins/selected_posts_count_test.js new file mode 100644 index 000000000..b6450b4ad --- /dev/null +++ b/test/javascripts/mixins/selected_posts_count_test.js @@ -0,0 +1,34 @@ +/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true */ +module("Discourse.SelectedPostsCount"); + +var buildTestObj = function(params, topicParams) { + return Ember.Object.createWithMixins(Discourse.SelectedPostsCount, params || {}); +}; + +test("without selectedPosts", function () { + var testObj = buildTestObj(); + + equal(testObj.get('selectedPostsCount'), 0, "No posts are selected without a selectedPosts property"); + + testObj.set('selectedPosts', []); + equal(testObj.get('selectedPostsCount'), 0, "No posts are selected when selectedPosts is an empty array"); +}); + +test("with some selectedPosts", function() { + var testObj = buildTestObj({ selectedPosts: [Discourse.Post.create()] }); + equal(testObj.get('selectedPostsCount'), 1, "It returns the amount of posts"); +}); + +test("when all posts are selected and there is a posts_count", function() { + var testObj = buildTestObj({ allPostsSelected: true, posts_count: 1024 }); + equal(testObj.get('selectedPostsCount'), 1024, "It returns the posts_count"); +}); + +test("when all posts are selected and there is topic with a posts_count", function() { + var testObj = buildTestObj({ + allPostsSelected: true, + topic: Discourse.Topic.create({ posts_count: 3456 }) + }); + + equal(testObj.get('selectedPostsCount'), 3456, "It returns the topic's posts_count"); +}); diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js new file mode 100644 index 000000000..469c77059 --- /dev/null +++ b/vendor/assets/javascripts/run-qunit.js @@ -0,0 +1,122 @@ +// PhantomJS QUnit Test Runner + +/*globals QUnit phantom*/ + +var args = phantom.args; +if (args.length < 1 || args.length > 2) { + console.log("Usage: " + phantom.scriptName + " <URL> <timeout>"); + phantom.exit(1); +} + +var fs = require('fs'); +function print(str) { + fs.write('/dev/stdout', str, 'w'); +} + +var page = require('webpage').create(); + +page.onConsoleMessage = function(msg) { + if (msg.slice(0,8) === 'WARNING:') { return; } + if (msg.slice(0,6) === 'DEBUG:') { return; } + + // Hack to access the print method + // If there's a better way to do this, please change + if (msg.slice(0,6) === 'PRINT:') { + print(msg.slice(7)); + return; + } + + console.log(msg); +}; + +page.open(args[0], function(status) { + if (status !== 'success') { + console.error("Unable to access network"); + phantom.exit(1); + } else { + page.evaluate(logQUnit); + + var timeout = parseInt(args[1] || 60000, 10); + var start = Date.now(); + var interval = setInterval(function() { + if (Date.now() > start + timeout) { + console.error("Tests timed out"); + phantom.exit(124); + } else { + var qunitDone = page.evaluate(function() { + return window.qunitDone; + }); + + if (qunitDone) { + clearInterval(interval); + if (qunitDone.failed > 0) { + phantom.exit(1); + } else { + phantom.exit(); + } + } + } + }, 500); + } +}); + +function logQUnit() { + var moduleErrors = []; + var testErrors = []; + var assertionErrors = []; + + console.log("\nRunning: " + JSON.stringify(QUnit.urlParams) + "\n"); + + QUnit.moduleDone(function(context) { + if (context.failed) { + var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n"); + moduleErrors.push(msg); + testErrors = []; + } + }); + + QUnit.testDone(function(context) { + if (context.failed) { + var msg = " Test Failed: " + context.name + assertionErrors.join(" "); + testErrors.push(msg); + assertionErrors = []; + console.log('PRINT: F'); + } else { + console.log('PRINT: .'); + } + }); + + QUnit.log(function(context) { + if (context.result) { return; } + + var msg = "\n Assertion Failed:"; + if (context.message) { + msg += " " + context.message; + } + + if (context.expected) { + msg += "\n Expected: " + context.expected + ", Actual: " + context.actual; + } + + assertionErrors.push(msg); + }); + + QUnit.done(function(context) { + console.log('\n'); + + if (moduleErrors.length > 0) { + for (var idx=0; idx<moduleErrors.length; idx++) { + console.error(moduleErrors[idx]+"\n"); + } + } + + var stats = [ + "Time: " + context.runtime + "ms", + "Total: " + context.total, + "Passed: " + context.passed, + "Failed: " + context.failed + ]; + console.log(stats.join(", ")); + window.qunitDone = context; + }); +}