diff --git a/Gemfile b/Gemfile
index 5d32bfd68fd..7786a630a78 100644
--- a/Gemfile
+++ b/Gemfile
@@ -103,10 +103,8 @@ group :test, :development do
gem 'certified', require: false
gem 'fabrication', require: false
gem 'qunit-rails'
- gem 'guard-jasmine', require: false
gem 'guard-rspec', require: false
gem 'guard-spork', require: false
- gem 'jasminerice'
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
diff --git a/Gemfile.lock b/Gemfile.lock
index eae4c8a2729..455b1746446 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -151,13 +151,6 @@ GEM
clockwork (0.5.0)
tzinfo (~> 0.3.35)
coderay (1.0.9)
- coffee-rails (3.2.2)
- coffee-script (>= 2.2.0)
- railties (~> 3.2.0)
- coffee-script (2.2.0)
- coffee-script-source
- execjs
- coffee-script-source (1.6.2)
connection_pool (1.0.0)
daemons (1.1.9)
debug_inspector (0.0.2)
@@ -206,11 +199,6 @@ GEM
lumberjack (>= 1.0.2)
pry (>= 0.9.10)
thor (>= 0.14.6)
- guard-jasmine (1.15.1)
- childprocess
- guard (>= 1.1.0)
- multi_json
- thor
guard-jshint-on-rails (0.0.2)
guard (>= 1.0.0)
jshint_on_rails (>= 1.0.2)
@@ -221,8 +209,6 @@ GEM
childprocess (>= 0.2.3)
guard (>= 1.1)
spork (>= 0.8.4)
- haml (4.0.2)
- tilt
handlebars-source (1.0.0.rc4)
has_ip_address (0.0.1)
hashie (2.0.4)
@@ -239,9 +225,6 @@ GEM
image_size (1.1.2)
image_sorcery (1.1.0)
in_threads (1.1.1)
- jasminerice (0.0.10)
- coffee-rails
- haml
journey (1.0.4)
jshint_on_rails (1.0.2)
json (1.7.7)
@@ -502,7 +485,6 @@ DEPENDENCIES
fast_xs
fastimage
fog
- guard-jasmine
guard-jshint-on-rails
guard-rspec
guard-spork
@@ -512,7 +494,6 @@ DEPENDENCIES
hiredis
image_optim
image_sorcery
- jasminerice
jshint_on_rails
librarian (>= 0.0.25)
listen
diff --git a/Guardfile b/Guardfile
index a3e3ef8b9c2..beef7abeff1 100644
--- a/Guardfile
+++ b/Guardfile
@@ -3,23 +3,6 @@ require 'terminal-notifier-guard' if RUBY_PLATFORM.include?('darwin')
phantom_path = File.expand_path('~/phantomjs/bin/phantomjs')
phantom_path = nil unless File.exists?(phantom_path)
-jasmine_options = {:phantomjs_bin => phantom_path, :server_env => :test}
-
-if ENV['JASMINE_URL']
- jasmine_options[:jasmine_url] = ENV['JASMINE_URL']
- jasmine_options[:server] = :none
-else
- jasmine_options[:server] = :thin
- jasmine_options[:port] = 8888
- jasmine_options[:server_timeout] = 300
-end
-
-guard 'jasmine', jasmine_options do
- watch(%r{spec/javascripts/spec\.js$}) { "spec/javascripts" }
- watch(%r{spec/javascripts/.+_spec\.js$})
- watch(%r{app/assets/javascripts/(.+?)\.js$}) { "spec/javascripts" }
-end
-
# verify that we pass jshint
# see https://github.com/MrOrz/guard-jshint-on-rails
guard 'jshint-on-rails', config_path: 'config/jshint.yml' do
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 81f1e7f4b76..26dd7fc2c08 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -10,9 +10,6 @@ Discourse::Application.configure do
# Configure static asset server for tests with Cache-Control for performance
config.serve_static_assets = true
- # Needed for jasmine specs to work
- config.assets.debug = true
-
# Log error messages when you accidentally call methods on nil
config.whiny_nils = true
diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb
index 44752832d85..3858000afb0 100644
--- a/config/initializers/06-mini_profiler.rb
+++ b/config/initializers/06-mini_profiler.rb
@@ -19,7 +19,7 @@ if defined?(Rack::MiniProfiler)
(env['PATH_INFO'] !~ /^\/message-bus/) &&
(env['PATH_INFO'] !~ /topics\/timings/) &&
(env['PATH_INFO'] !~ /assets/) &&
- (env['PATH_INFO'] !~ /jasmine/) &&
+ (env['PATH_INFO'] !~ /qunit/) &&
(env['PATH_INFO'] !~ /users\/.*\/avatar/) &&
(env['PATH_INFO'] !~ /srv\/status/) &&
(env['PATH_INFO'] !~ /commits-widget/)
@@ -27,7 +27,7 @@ if defined?(Rack::MiniProfiler)
# without a user provider our results will use the ip address for namespacing
# with a load balancer in front this becomes really bad as some results can
- # be stored associated with ip1 as the user and retrieved using ip2 causing 404s
+ # be stored associated with ip1 as the user and retrieved using ip2 causing 404s
Rack::MiniProfiler.config.user_provider = lambda do |env|
request = Rack::Request.new(env)
id = request.cookies["_t"] || request.ip || "unknown"
diff --git a/docs/SOFTWARE.md b/docs/SOFTWARE.md
index bfe79c6f584..ba1a139ac14 100644
--- a/docs/SOFTWARE.md
+++ b/docs/SOFTWARE.md
@@ -37,7 +37,6 @@ The following Ruby Gems are used in Discourse:
* [rspec](https://rubygems.org/gems/rspec)
* [shoulda](https://rubygems.org/gems/shoulda)
* [turn](https://rubygems.org/gems/turn)
-* [jasminerice](https://rubygems.org/gems/jasminerice)
* [fabrication](https://rubygems.org/gems/fabrication)
* [mocha](https://rubygems.org/gems/mocha)
* [simplecov](https://rubygems.org/gems/simplecov)
diff --git a/spec/javascripts/components/click_track_spec.js b/spec/javascripts/components/click_track_spec.js
deleted file mode 100644
index 359ef4b8993..00000000000
--- a/spec/javascripts/components/click_track_spec.js
+++ /dev/null
@@ -1,221 +0,0 @@
-/*global expect:true describe:true it:true beforeEach:true afterEach:true spyOn:true */
-
-describe("Discourse.ClickTrack", function() {
-
- var track = Discourse.ClickTrack.trackClick,
- clickEvent,
- html = [
- '
'].join("\n");
-
- var generateClickEventOn = function(selector) {
- return $.Event("click", { currentTarget: $(selector)[0] });
- }
-
- beforeEach(function() {
- $('body').html(html);
- });
-
- afterEach(function() {
- $('#topic').remove();
- });
-
- describe("lightboxes", function() {
-
- beforeEach(function() {
- clickEvent = generateClickEventOn('.lightbox');
- });
-
- it("does not track clicks on lightboxes", function() {
- expect(track(clickEvent)).toBe(true);
- });
-
- it("does not call preventDefault", function() {
- spyOn(clickEvent, "preventDefault");
- track(clickEvent);
- expect(clickEvent.preventDefault).not.toHaveBeenCalled();
- });
-
- });
-
- it("calls preventDefault", function() {
- clickEvent = generateClickEventOn('a');
- spyOn(clickEvent, "preventDefault");
- track(clickEvent);
- expect(clickEvent.preventDefault).toHaveBeenCalled();
- });
-
- it("does not track clicks on back buttons", function() {
- clickEvent = generateClickEventOn('.back');
- expect(track(clickEvent)).toBe(true);
- });
-
- it("does not track clicks on quote buttons", function() {
- clickEvent = generateClickEventOn('.quote-other-topic');
- expect(track(clickEvent)).toBe(true);
- });
-
- it("removes the href and put it as a data attribute", function() {
- clickEvent = generateClickEventOn('a');
- track(clickEvent);
- var $link = $('a').first();
- expect($link.hasClass('no-href')).toBe(true);
- expect($link.data('href')).toEqual("http://www.google.com");
- expect($link.attr('href')).toBeUndefined();
- expect($link.data('auto-route')).toBe(true);
- });
-
- describe("badges", function() {
-
- it("does not update badge clicks on my own link", function() {
- spyOn(Discourse.User, 'current').andReturn(314);
- spyOn(Discourse, "get").andReturn(314);
-
- track(generateClickEventOn('#with-badge'));
- var $badge = $('span.badge', $('#with-badge').first());
- expect(parseInt($badge.html(), 10)).toEqual(1);
- });
-
- it("does not update badge clicks on links in my own post", function() {
- spyOn(Discourse.User, 'current').andReturn(3141);
- track(generateClickEventOn('#with-badge-but-not-mine'));
- var $badge = $('span.badge', $('#with-badge-but-not-mine').first());
- expect(parseInt($badge.html(), 10)).toEqual(1);
- });
-
- describe("oneboxes", function() {
-
- it("does not update badge clicks in oneboxes", function() {
- track(generateClickEventOn('#inside-onebox'));
- var $badge = $('span.badge', $('#inside-onebox').first());
- expect(parseInt($badge.html(), 10)).toEqual(1);
- });
-
- it("updates badge clicks in oneboxes when forced", function() {
- track(generateClickEventOn('#inside-onebox-forced'));
- var $badge = $('span.badge', $('#inside-onebox-forced').first());
- expect(parseInt($badge.html(), 10)).toEqual(2);
- });
-
- });
-
- it("updates badge clicks", function() {
- track(generateClickEventOn('#with-badge'));
- var $badge = $('span.badge', $('#with-badge').first());
- expect(parseInt($badge.html(), 10)).toEqual(2);
- });
-
- });
-
- describe("right click", function() {
-
- beforeEach(function(){
- clickEvent = generateClickEventOn('a');
- clickEvent.which = 3;
- });
-
- it("detects right clicks", function() {
- expect(track(clickEvent)).toBe(true);
- });
-
- it("changes the href", function() {
- track(clickEvent);
- var $link = $('a').first();
- expect($link.attr('href')).toEqual("http://www.google.com");
- });
-
- it("tracks external right clicks", function() {
- Discourse.SiteSettings.track_external_right_clicks = true;
- track(clickEvent);
- var $link = $('a').first();
- expect($link.attr('href')).toEqual("/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42");
- // reset
- Discourse.SiteSettings.track_external_right_clicks = false;
- });
-
- });
-
- describe("new tab", function() {
-
- beforeEach(function(){
- clickEvent = generateClickEventOn('a');
- spyOn(Discourse, 'ajax');
- spyOn(window, 'open');
- });
-
- var expectItOpensInANewTab = function() {
- expect(track(clickEvent)).toBe(false);
- expect(Discourse.ajax).toHaveBeenCalled();
- expect(window.open).toHaveBeenCalledWith('http://www.google.com', '_blank');
- };
-
- it("opens in a new tab when pressing shift", function() {
- clickEvent.shiftKey = true;
- expectItOpensInANewTab();
- });
-
- it("opens in a new tab when pressing meta", function() {
- clickEvent.metaKey = true;
- expectItOpensInANewTab();
- });
-
- it("opens in a new tab when pressing ctrl", function() {
- clickEvent.ctrlKey = true;
- expectItOpensInANewTab();
- });
-
- it("opens in a new tab when middle clicking", function() {
- clickEvent.which = 2;
- expectItOpensInANewTab();
- });
-
- });
-
- it("tracks via AJAX if we're on the same site", function() {
- // setup
- clickEvent = generateClickEventOn('#same-site');
- spyOn(Discourse, 'ajax');
- spyOn(Discourse.URL, 'routeTo');
- spyOn(Discourse.URL, 'origin').andReturn('http://discuss.domain.com');
- // test
- expect(track(clickEvent)).toBe(false);
- expect(Discourse.ajax).toHaveBeenCalled();
- expect(Discourse.URL.routeTo).toHaveBeenCalledWith('http://discuss.domain.com');
- });
-
- describe("tracks via custom URL", function() {
-
- beforeEach(function() {
- clickEvent = generateClickEventOn('a');
- });
-
- it("in another window", function() {
- // spies
- spyOn(Discourse.User, 'current').andReturn(true);
- spyOn(window, 'open').andCallFake(function() { return { focus: function() {} } });
- spyOn(window, 'focus');
- // test
- expect(track(clickEvent)).toBe(false);
- expect(window.open).toHaveBeenCalledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank');
- });
-
- it("in the same window", function() {
- spyOn(Discourse.URL, 'redirectTo');
- expect(track(clickEvent)).toBe(false);
- expect(Discourse.URL.redirectTo).toHaveBeenCalledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42');
- });
-
- });
-
-});
diff --git a/spec/javascripts/components/utilities_spec.js b/spec/javascripts/components/utilities_spec.js
deleted file mode 100644
index a9b10dfaff0..00000000000
--- a/spec/javascripts/components/utilities_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*global waitsFor:true expect:true describe:true beforeEach:true it:true spyOn:true */
-
-describe("Discourse.Utilities", function() {
-
- describe("emailValid", function() {
-
- it("allows upper case in first part of emails", function() {
- expect(Discourse.Utilities.emailValid('Bob@example.com')).toBe(true);
- });
-
- it("allows upper case in domain of emails", function() {
- expect(Discourse.Utilities.emailValid('bob@EXAMPLE.com')).toBe(true);
- });
-
- });
-
- describe("validateFilesForUpload", function() {
-
- it("returns false when file is undefined", function() {
- expect(Discourse.Utilities.validateFilesForUpload(null)).toBe(false);
- expect(Discourse.Utilities.validateFilesForUpload(undefined)).toBe(false);
- });
-
- it("returns false when file there is no file", function() {
- expect(Discourse.Utilities.validateFilesForUpload([])).toBe(false);
- });
-
- it("supports only one file", function() {
- spyOn(bootbox, 'alert');
- spyOn(Em.String, 'i18n');
- expect(Discourse.Utilities.validateFilesForUpload([1, 2])).toBe(false);
- expect(bootbox.alert).toHaveBeenCalled();
- expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.upload_too_many_images');
- });
-
- it("supports only an image", function() {
- var html = { type: "text/html" };
- spyOn(bootbox, 'alert');
- spyOn(Em.String, 'i18n');
- expect(Discourse.Utilities.validateFilesForUpload([html])).toBe(false);
- expect(bootbox.alert).toHaveBeenCalled();
- expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.only_images_are_supported');
- });
-
- it("prevents the upload of a too large image", function() {
- var image = { type: "image/png", size: 10 * 1024 };
- Discourse.SiteSettings.max_upload_size_kb = 5;
- spyOn(bootbox, 'alert');
- spyOn(Em.String, 'i18n');
- expect(Discourse.Utilities.validateFilesForUpload([image])).toBe(false);
- expect(bootbox.alert).toHaveBeenCalled();
- expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.upload_too_large', { max_size_kb: 5 });
- });
-
- it("works", function() {
- var image = { type: "image/png", size: 10 * 1024 };
- Discourse.SiteSettings.max_upload_size_kb = 15;
- expect(Discourse.Utilities.validateFilesForUpload([image])).toBe(true);
- });
-
- });
-
-});
diff --git a/spec/javascripts/hacks.js b/spec/javascripts/hacks.js
deleted file mode 100644
index 134a3647282..00000000000
--- a/spec/javascripts/hacks.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// hacks for ember, this sets up our app for testing
-
-(function(){
-
- var currentWindowOnload = window.onload;
- window.onload = function() {
- if (currentWindowOnload) {
- currentWindowOnload();
- }
-
- $('').appendTo($('body')).hide();
-
- Discourse.SiteSettings = {}
-
- Discourse.Router.map(function() {
- this.route("jasmine",{path: "/jasmine"});
- Discourse.routeBuilder.apply(this)
- });
- }
-
-})()
diff --git a/spec/javascripts/models/report_spec.js b/spec/javascripts/models/report_spec.js
deleted file mode 100644
index 90b89714b8c..00000000000
--- a/spec/javascripts/models/report_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/*global waitsFor:true expect:true describe:true beforeEach:true it:true */
-
-describe("Discourse.Report", function() {
-
- function dateString(days) {
- return moment().subtract("days", days).format('YYYY-MM-DD');
- }
-
- function reportWithData(data) {
- var arr = [];
- _.each(data,function(val,index) {
- arr.push({x: dateString(index), y: val});
- });
- return Discourse.Report.create({ type: 'topics', data: arr });
- }
-
- describe("todayCount", function() {
- it("returns the correct value", function() {
- expect( reportWithData([5,4,3,2,1]).get('todayCount') ).toBe(5);
- });
- });
-
- describe("yesterdayCount", function() {
- it("returns the correct value", function() {
- expect( reportWithData([5,4,3,2,1]).get('yesterdayCount') ).toBe(4);
- });
- });
-
- describe("sumDays", function() {
- it("adds the values for the given range of days, inclusive", function() {
- expect( reportWithData([1,2,3,5,8,13]).sumDays(2,4) ).toBe(16);
- });
- });
-
- describe("lastSevenDaysCount", function() {
- it("returns the correct value", function() {
- expect( reportWithData([100,9,8,7,6,5,4,3,200,300,400]).get('lastSevenDaysCount') ).toBe(42);
- });
- });
-
- describe("percentChangeString", function() {
- it("returns correct value when value increased", function() {
- expect( reportWithData([]).percentChangeString(8,5) ).toBe("+60%");
- });
-
- it("returns correct value when value decreased", function() {
- expect( reportWithData([]).percentChangeString(2,8) ).toBe("-75%");
- });
-
- it("returns 0 when value is unchanged", function() {
- expect( reportWithData([]).percentChangeString(8,8) ).toBe("0%");
- });
-
- it("returns Infinity when previous value was 0", function() {
- expect( reportWithData([]).percentChangeString(8,0) ).toBe(null);
- });
-
- it("returns -100 when yesterday's value was 0", function() {
- expect( reportWithData([]).percentChangeString(0,8) ).toBe('-100%');
- });
-
- it("returns NaN when both yesterday and the previous day were both 0", function() {
- expect( reportWithData([]).percentChangeString(0,0) ).toBe(null);
- });
- });
-
- describe("yesterdayCountTitle", function() {
- it("displays percent change and previous value", function(){
- var title = reportWithData([6,8,5,2,1]).get('yesterdayCountTitle')
- expect( title.indexOf('+60%') ).not.toBe(-1);
- expect( title ).toMatch("Was 5");
- });
-
- it("handles when two days ago was 0", function() {
- var title = reportWithData([6,8,0,2,1]).get('yesterdayCountTitle')
- expect( title ).toMatch("Was 0");
- expect( title ).not.toMatch("%");
- });
- });
-
- describe("sevenDayCountTitle", function() {
- it("displays percent change and previous value", function(){
- var title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get('sevenDayCountTitle');
- expect( title ).toMatch("-50%");
- expect( title ).toMatch("Was 14");
- });
- });
-
- describe("thirtyDayCountTitle", function() {
- it("displays percent change and previous value", function(){
- var report = reportWithData([5,5,5,5]);
- report.set('prev30Days', 10);
- var title = report.get('thirtyDayCountTitle');
- expect( title.indexOf('+50%') ).not.toBe(-1);
- expect( title ).toMatch("Was 10");
- });
- });
-});
diff --git a/spec/javascripts/models/user_action_spec.js b/spec/javascripts/models/user_action_spec.js
deleted file mode 100644
index fb6e3c63005..00000000000
--- a/spec/javascripts/models/user_action_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/*global waitsFor:true expect:true describe:true beforeEach:true it:true */
-
-describe("Discourse.UserAction", function() {
-
- describe("collapseStream", function() {
-
- it("collapses all likes", function() {
- var actions = [
- Discourse.UserAction.create({
- action_type: Discourse.UserAction.LIKE,
- topic_id: 1,
- user_id: 1,
- post_number: 1
- }), Discourse.UserAction.create({
- action_type: Discourse.UserAction.EDIT,
- topic_id: 2,
- user_id: 1,
- post_number: 1
- }), Discourse.UserAction.create({
- action_type: Discourse.UserAction.LIKE,
- topic_id: 1,
- user_id: 2,
- post_number: 1
- })
- ];
-
- actions = Discourse.UserAction.collapseStream(actions);
-
- expect(actions.length).toBe(2);
- expect(actions[0].get("children").length).toBe(1);
- expect(actions[0].get("children")[0].items.length).toBe(2);
- });
-
- });
-
-});
diff --git a/spec/javascripts/preload_store_spec.js b/spec/javascripts/preload_store_spec.js
deleted file mode 100644
index 02c87edc362..00000000000
--- a/spec/javascripts/preload_store_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/*global waitsFor:true expect:true describe:true beforeEach:true it:true runs:true */
-
-describe("PreloadStore", function() {
-
- beforeEach(function() {
- PreloadStore.store('bane', 'evil');
- });
-
- describe('get', function() {
-
- it("returns undefined if the key doesn't exist", function() {
- expect(PreloadStore.get('joker')).toBe(undefined);
- });
-
- it("returns the value if the key exists", function() {
- expect(PreloadStore.get('bane')).toBe('evil');
- });
-
- });
-
- describe('remove', function() {
-
- it("removes the value if the key exists", function() {
- PreloadStore.remove('bane');
- expect(PreloadStore.get('bane')).toBe(undefined);
- });
-
- });
-
- describe('getAndRemove', function() {
-
- it("returns a promise that resolves to null", function() {
- var done, storeResult;
- done = storeResult = null;
- PreloadStore.getAndRemove('joker').then(function(result) {
- done = true;
- storeResult = result;
- });
- waitsFor((function() { return done; }), "Promise never resolved", 1000);
- runs(function() {
- expect(storeResult).toBe(null);
- });
- });
-
- it("returns a promise that resolves to the result of the finder", function() {
- var done, finder, storeResult;
- done = storeResult = null;
- finder = function() { return 'evil'; };
- PreloadStore.getAndRemove('joker', finder).then(function(result) {
- done = true;
- storeResult = result;
- });
- waitsFor((function() { return done; }), "Promise never resolved", 1000);
- runs(function() {
- expect(storeResult).toBe('evil');
- });
- });
-
- it("returns a promise that resolves to the result of the finder's promise", function() {
- var done, finder, storeResult;
- done = storeResult = null;
- finder = function() {
- return Ember.Deferred.promise(function(promise) { promise.resolve('evil'); });
- };
- PreloadStore.getAndRemove('joker', finder).then(function(result) {
- done = true;
- storeResult = result;
- });
- waitsFor((function() { return done; }), "Promise never resolved", 1000);
- runs(function() {
- expect(storeResult).toBe('evil');
- });
- });
-
- it("returns a promise that resolves to the result of the finder's rejected promise", function() {
- var done, finder, storeResult;
- done = storeResult = null;
- finder = function() {
- return Ember.Deferred.promise(function(promise) { promise.reject('evil'); });
- };
- PreloadStore.getAndRemove('joker', finder).then(null, function(rejectedResult) {
- done = true;
- storeResult = rejectedResult;
- });
- waitsFor((function() { return done; }), "Promise never rejected", 1000);
- runs(function() {
- expect(storeResult).toBe('evil');
- });
- });
-
- it("returns a promise that resolves to 'evil'", function() {
- var done, storeResult;
- done = storeResult = null;
- PreloadStore.getAndRemove('bane').then(function(result) {
- done = true;
- storeResult = result;
- });
- waitsFor((function() { return done; }), "Promise never resolved", 1000);
- runs(function() {
- expect(storeResult).toBe('evil');
- });
- });
-
- });
-
-});
\ No newline at end of file
diff --git a/spec/javascripts/sanitize_spec.js b/spec/javascripts/sanitize_spec.js
deleted file mode 100644
index b5126ec5b30..00000000000
--- a/spec/javascripts/sanitize_spec.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*global waitsFor:true expect:true describe:true beforeEach:true it:true sanitizeHtml:true */
-
-describe("sanitize", function(){
-
- it("strips all script tags", function(){
- var sanitized = sanitizeHtml("");
- expect(sanitized).toBe("");
- });
-
- it("strips disallowed attributes", function(){
- var sanitized = sanitizeHtml("");
- expect(sanitized).toBe("");
- });
-
-});
diff --git a/spec/javascripts/spec.css b/spec/javascripts/spec.css
deleted file mode 100644
index 2358dbf4242..00000000000
--- a/spec/javascripts/spec.css
+++ /dev/null
@@ -1,3 +0,0 @@
-/*
-
- */
\ No newline at end of file
diff --git a/spec/javascripts/spec.js b/spec/javascripts/spec.js
deleted file mode 100644
index e353b71a8ba..00000000000
--- a/spec/javascripts/spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-
-//= require env
-
-//= require ../../app/assets/javascripts/preload_store.js
-
-// probe framework first
-//= require ../../app/assets/javascripts/discourse/components/probes.js
-
-// Externals we need to load first
-//= require ../../app/assets/javascripts/external/jquery-1.9.1.js
-
-//= require ../../app/assets/javascripts/external/jquery.ui.widget.js
-//= require ../../app/assets/javascripts/external/handlebars-1.0.rc.4.js
-//= require ../../app/assets/javascripts/external_production/ember.js
-//= require ../../app/assets/javascripts/external_production/group-helper.js
-
-//= require ../../app/assets/javascripts/locales/i18n
-//= require ../../app/assets/javascripts/locales/date_locales.js
-//= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers
-//= require ../../app/assets/javascripts/locales/en
-//
-// Pagedown customizations
-//= require ../../app/assets/javascripts/pagedown_custom.js
-
-// The rest of the externals
-//= require_tree ../../app/assets/javascripts/external
-
-//= require ../../app/assets/javascripts/discourse
-
-// Stuff we need to load first
-//= require_tree ../../app/assets/javascripts/discourse/mixins
-//= require ../../app/assets/javascripts/discourse/components/debounce
-//= require ../../app/assets/javascripts/discourse/controllers/controller
-//= require ../../app/assets/javascripts/discourse/views/modal/modal_body_view
-//= require ../../app/assets/javascripts/discourse/models/model
-//= require ../../app/assets/javascripts/discourse/routes/discourse_route
-
-//= require_tree ../../app/assets/javascripts/discourse/controllers
-//= require_tree ../../app/assets/javascripts/discourse/components
-//= require_tree ../../app/assets/javascripts/discourse/models
-//= require_tree ../../app/assets/javascripts/discourse/views
-//= require_tree ../../app/assets/javascripts/discourse/helpers
-//= require_tree ../../app/assets/javascripts/discourse/templates
-//= require_tree ../../app/assets/javascripts/discourse/routes
-//= require_tree ../../app/assets/javascripts/admin/models
-
-//= require_tree ../../app/assets/javascripts/defer
-
-//= require_tree .
-
-//= require hacks
diff --git a/test/javascripts/components/click_track_test.js b/test/javascripts/components/click_track_test.js
new file mode 100644
index 00000000000..e5af1a84c74
--- /dev/null
+++ b/test/javascripts/components/click_track_test.js
@@ -0,0 +1,173 @@
+/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true sinon:true blank:true */
+
+module("Discourse.ClickTrack", {
+ setup: function() {
+
+ // Prevent any of these tests from navigating away
+ this.win = {focus: function() { } };
+ this.redirectTo = sinon.stub(Discourse.URL, "redirectTo");
+ sinon.stub(Discourse, "ajax");
+ this.windowOpen = sinon.stub(window, "open").returns(this.win);
+ sinon.stub(this.win, "focus");
+
+ $('#qunit-scratch').html([
+ ''].join("\n"));
+ },
+
+ teardown: function() {
+ $('#topic').remove();
+ $('#qunit-scratch').html('');
+
+ Discourse.URL.redirectTo.restore();
+ Discourse.ajax.restore();
+ window.open.restore();
+ this.win.focus.restore();
+ }
+});
+
+var track = Discourse.ClickTrack.trackClick;
+
+var generateClickEventOn = function(selector) {
+ return $.Event("click", { currentTarget: $(selector)[0] });
+}
+
+test("does not track clicks on lightboxes", function() {
+ var clickEvent = generateClickEventOn('.lightbox');
+ this.stub(clickEvent, "preventDefault");
+ ok(track(clickEvent));
+ ok(!clickEvent.preventDefault.calledOnce);
+});
+
+test("it calls preventDefault when clicking on an a", function() {
+ var clickEvent = generateClickEventOn('a');
+ this.stub(clickEvent, "preventDefault");
+ track(clickEvent);
+ ok(clickEvent.preventDefault.calledOnce);
+ ok(Discourse.URL.redirectTo.calledOnce);
+});
+
+test("does not track clicks on back buttons", function() {
+ ok(track(generateClickEventOn('.back')))
+});
+
+test("does not track clicks on quote buttons", function() {
+ ok(track(generateClickEventOn('.quote-other-topic')))
+});
+
+test("removes the href and put it as a data attribute", function() {
+ track(generateClickEventOn('a'));
+
+ var $link = $('a').first();
+ ok($link.hasClass('no-href'));
+ equal($link.data('href'), 'http://www.google.com');
+ blank($link.attr('href'));
+ ok($link.data('auto-route'));
+ ok(Discourse.URL.redirectTo.calledOnce);
+});
+
+
+var badgeClickCount = function(id, expected) {
+ track(generateClickEventOn('#' + id));
+ var $badge = $('span.badge', $('#' + id).first());
+ equal(parseInt($badge.html(), 10), expected);
+};
+
+test("does not update badge clicks on my own link", function() {
+ this.stub(Discourse.User, 'current').returns(314);
+ badgeClickCount('with-badge', 1);
+});
+
+test("does not update badge clicks in my own post", function() {
+ this.stub(Discourse.User, 'current').returns(3141);
+ badgeClickCount('with-badge-but-not-mine', 1);
+});
+
+test("updates badge counts correctly", function() {
+ badgeClickCount('inside-onebox', 1);
+ badgeClickCount('inside-onebox-forced', 2);
+ badgeClickCount('with-badge', 2);
+});
+
+var trackRightClick = function() {
+ var clickEvent = generateClickEventOn('a')
+ clickEvent.which = 3;
+ return track(clickEvent);
+};
+
+test("right clicks change the href", function() {
+ ok(trackRightClick());
+ equal($('a').first().prop('href'), "http://www.google.com/");
+});
+
+test("right clicks are tracked", function() {
+ Discourse.SiteSettings.track_external_right_clicks = true;
+ trackRightClick();
+ equal($('a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42");
+ Discourse.SiteSettings.track_external_right_clicks = false;
+});
+
+
+var expectToOpenInANewTab = function(clickEvent) {
+ ok(!track(clickEvent));
+ ok(Discourse.ajax.calledOnce);
+ ok(window.open.calledOnce);
+};
+
+test("it opens in a new tab when pressing shift", function() {
+ var clickEvent = generateClickEventOn('a');
+ clickEvent.shiftKey = true;
+ expectToOpenInANewTab(clickEvent);
+});
+
+test("it opens in a new tab when pressing meta", function() {
+ var clickEvent = generateClickEventOn('a');
+ clickEvent.metaKey = true;
+ expectToOpenInANewTab(clickEvent);
+});
+
+test("it opens in a new tab when pressing meta", function() {
+ var clickEvent = generateClickEventOn('a');
+ clickEvent.ctrlKey = true;
+ expectToOpenInANewTab(clickEvent);
+});
+
+test("it opens in a new tab when pressing meta", function() {
+ var clickEvent = generateClickEventOn('a');
+ clickEvent.which = 2;
+ expectToOpenInANewTab(clickEvent);
+});
+
+test("tracks via AJAX if we're on the same site", function() {
+ this.stub(Discourse.URL, "routeTo");
+ this.stub(Discourse.URL, "origin").returns("http://discuss.domain.com");
+
+ ok(!track(generateClickEventOn('#same-site')));
+ ok(Discourse.ajax.calledOnce);
+ ok(Discourse.URL.routeTo.calledOnce);
+});
+
+
+test("tracks custom urls when opening in another window", function() {
+ var clickEvent = generateClickEventOn('a');
+ this.stub(Discourse.User, "current").returns(true);
+ ok(!track(clickEvent));
+ ok(this.windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank'));
+});
+
+test("tracks custom urls when opening in another window", function() {
+ var clickEvent = generateClickEventOn('a');
+ ok(!track(clickEvent));
+ ok(this.redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42'));
+});
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index ed398eb7603..19599f6bde2 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -1,4 +1,4 @@
-/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true md5:true */
+/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true md5:true sanitizeHtml:true */
module("Discourse.Markdown");
@@ -93,3 +93,9 @@ test("Oneboxing", function() {
});
+test("SanitizeHTML", function() {
+
+ equal(sanitizeHtml(""), "");
+ equal(sanitizeHtml(""), "");
+
+});
\ No newline at end of file
diff --git a/test/javascripts/components/preload_store_test.js b/test/javascripts/components/preload_store_test.js
new file mode 100644
index 00000000000..36c056a7f7c
--- /dev/null
+++ b/test/javascripts/components/preload_store_test.js
@@ -0,0 +1,73 @@
+/*global module:true test:true ok:true visit:true expect:true exists:true equal:true count:true present:true asyncTest:true blank:true */
+
+module("Discourse.PreloadStore", {
+ setup: function() {
+ PreloadStore.store('bane', 'evil');
+ }
+});
+
+test("get", function() {
+ blank(PreloadStore.get('joker'), "returns blank for a missing key");
+ equal(PreloadStore.get('bane'), 'evil', "returns the value for that key");
+});
+
+test("remove", function() {
+ PreloadStore.remove('bane');
+ blank(PreloadStore.get('bane'), "removes the value if the key exists");
+});
+
+asyncTest("getAndRemove returns a promise that resolves to null", function() {
+ expect(1);
+
+ PreloadStore.getAndRemove('joker').then(function(result) {
+ blank(result);
+ start();
+ });
+});
+
+asyncTest("getAndRemove returns a promise that resolves to the result of the finder", function() {
+ expect(1);
+
+ var finder = function() { return 'batdance'; };
+ PreloadStore.getAndRemove('joker', finder).then(function(result) {
+ equal(result, 'batdance');
+ start();
+ });
+
+});
+
+asyncTest("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
+ expect(1);
+
+ var finder = function() {
+ return Ember.Deferred.promise(function(promise) { promise.resolve('hahahah'); });
+ };
+
+ PreloadStore.getAndRemove('joker', finder).then(function(result) {
+ equal(result, 'hahahah');
+ start();
+ });
+});
+
+asyncTest("returns a promise that rejects with the result of the finder's rejected promise", function() {
+ expect(1);
+
+ var finder = function() {
+ return Ember.Deferred.promise(function(promise) { promise.reject('error'); });
+ };
+
+ PreloadStore.getAndRemove('joker', finder).then(null, function(result) {
+ equal(result, 'error');
+ start();
+ });
+
+});
+
+asyncTest("returns a promise that resolves to 'evil'", function() {
+ expect(1);
+
+ PreloadStore.getAndRemove('bane').then(function(result) {
+ equal(result, 'evil');
+ start();
+ });
+});
diff --git a/test/javascripts/components/utilities_test.js b/test/javascripts/components/utilities_test.js
new file mode 100644
index 00000000000..227af03096f
--- /dev/null
+++ b/test/javascripts/components/utilities_test.js
@@ -0,0 +1,52 @@
+/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true */
+
+module("Discourse.Utilities");
+
+var utils = Discourse.Utilities;
+
+test("emailValid", function() {
+ ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails");
+ ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
+});
+
+
+var validUpload = utils.validateFilesForUpload;
+
+test("validateFilesForUpload", function() {
+ ok(!validUpload(null), "no files are invalid");
+ ok(!validUpload(undefined), "undefined files are invalid");
+ ok(!validUpload([]), "empty array of files is invalid");
+});
+
+test("uploading one file", function() {
+ this.stub(bootbox, "alert");
+
+ ok(!validUpload([1, 2]));
+ ok(bootbox.alert.calledOnce);
+});
+
+test("ensures an image upload", function() {
+ var html = { type: "text/html" };
+ this.stub(bootbox, "alert");
+
+ ok(!validUpload([html]));
+ ok(bootbox.alert.calledOnce);
+});
+
+test("prevents files that are too big from being uploaded", function() {
+ var image = { type: "image/png", size: 10 * 1024 };
+ Discourse.SiteSettings.max_upload_size_kb = 5;
+ this.stub(bootbox, "alert");
+
+ ok(!validUpload([image]));
+ ok(bootbox.alert.calledOnce);
+});
+
+test("allows valid uploads to go through", function() {
+ var image = { type: "image/png", size: 10 * 1024 };
+ Discourse.SiteSettings.max_upload_size_kb = 15;
+ this.stub(bootbox, "alert");
+
+ ok(validUpload([image]));
+ ok(!bootbox.alert.calledOnce);
+});
diff --git a/test/javascripts/integration/header_test.js b/test/javascripts/integration/header_test.js
index 5ff8723868f..23a95a59024 100644
--- a/test/javascripts/integration/header_test.js
+++ b/test/javascripts/integration/header_test.js
@@ -13,6 +13,8 @@ module("Header", {
test("/", function() {
visit("/").then(function() {
+ expect(2);
+
ok(exists("header"), "The header was rendered");
ok(exists("#site-logo"), "The logo was shown");
});
diff --git a/test/javascripts/integration/list_topics_test.js b/test/javascripts/integration/list_topics_test.js
index 440896618e9..5b94fdad4e8 100644
--- a/test/javascripts/integration/list_topics_test.js
+++ b/test/javascripts/integration/list_topics_test.js
@@ -13,6 +13,8 @@ module("List Topics", {
test("/", function() {
visit("/").then(function() {
+ expect(2);
+
ok(exists("#topic-list"), "The list of topics was rendered");
ok(count('#topic-list .topic-list-item') > 0, "has topics");
});
diff --git a/test/javascripts/models/report_test.js b/test/javascripts/models/report_test.js
new file mode 100644
index 00000000000..2544d2e1c1c
--- /dev/null
+++ b/test/javascripts/models/report_test.js
@@ -0,0 +1,60 @@
+/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true blank:true */
+
+module("Discourse.Report");
+
+function reportWithData(data) {
+ return Discourse.Report.create({
+ type: 'topics',
+ data: _.map(data, function(val, index) {
+ return {x: moment().subtract("days", index).format('YYYY-MM-DD'), y: val};
+ })
+ });
+}
+
+test("counts", function() {
+ var report = reportWithData([5, 4, 3, 2, 1, 100, 99, 98, 1000]);
+
+ equal(report.get('todayCount'), 5);
+ equal(report.get('yesterdayCount'), 4);
+ equal(report.sumDays(2, 4), 6, "adds the values for the given range of days, inclusive");
+ equal(report.get('lastSevenDaysCount'), 307, "sums 7 days excluding today");
+});
+
+test("percentChangeString", function() {
+ var report = reportWithData([]);
+
+ equal(report.percentChangeString(8, 5), "+60%", "value increased");
+ equal(report.percentChangeString(2, 8), "-75%", "value decreased");
+ equal(report.percentChangeString(8, 8), "0%", "value unchanged");
+ blank(report.percentChangeString(8, 0), "returns blank when previous value was 0");
+ equal(report.percentChangeString(0, 8), "-100%", "yesterday was 0");
+ blank(report.percentChangeString(0, 0), "returns blank when both were 0");
+});
+
+test("yesterdayCountTitle with valid values", function() {
+ var title = reportWithData([6,8,5,2,1]).get('yesterdayCountTitle');
+ ok(title.indexOf('+60%') !== -1);
+ ok(title.match(/Was 5/));
+});
+
+test("yesterdayCountTitle when two days ago was 0", function() {
+ var title = reportWithData([6,8,0,2,1]).get('yesterdayCountTitle');
+ equal(title.indexOf('%'), -1);
+ ok(title.match(/Was 0/));
+});
+
+
+test("sevenDayCountTitle", function() {
+ var title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get('sevenDayCountTitle');
+ ok(title.match(/-50%/));
+ ok(title.match(/Was 14/));
+});
+
+test("thirtyDayCountTitle", function() {
+ var report = reportWithData([5,5,5,5]);
+ report.set('prev30Days', 10);
+ var title = report.get('thirtyDayCountTitle');
+
+ ok(title.indexOf('+50%') !== -1);
+ ok(title.match(/Was 10/));
+});
diff --git a/test/javascripts/models/user_action_test.js b/test/javascripts/models/user_action_test.js
new file mode 100644
index 00000000000..567c0271620
--- /dev/null
+++ b/test/javascripts/models/user_action_test.js
@@ -0,0 +1,29 @@
+/*global module:true test:true ok:true visit:true expect:true exists:true count:true present:true equal:true */
+
+module("Discourse.UserAction");
+
+test("collapsing likes", function () {
+ var actions = Discourse.UserAction.collapseStream([
+ Discourse.UserAction.create({
+ action_type: Discourse.UserAction.LIKE,
+ topic_id: 1,
+ user_id: 1,
+ post_number: 1
+ }), Discourse.UserAction.create({
+ action_type: Discourse.UserAction.EDIT,
+ topic_id: 2,
+ user_id: 1,
+ post_number: 1
+ }), Discourse.UserAction.create({
+ action_type: Discourse.UserAction.LIKE,
+ topic_id: 1,
+ user_id: 2,
+ post_number: 1
+ })
+ ]);
+
+ equal(actions.length, 2);
+
+ equal(actions[0].get('children.length'), 1);
+ equal(actions[0].get('children')[0].items.length, 2);
+});
diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js
index 41d3e5e41b9..7689a0ad317 100644
--- a/test/javascripts/test_helper.js
+++ b/test/javascripts/test_helper.js
@@ -52,6 +52,7 @@ sinon.config = {
// Trick JSHint into allow document.write
var d = document;
+d.write('');
d.write('');
d.write('');