From 82d1465d18c7bfa539f76d8ba0e8c6a710d9f726 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 12:14:01 -0400 Subject: [PATCH 01/15] Ugly Hack: Remove improperly parsed headers from Mail::Message --- lib/email/receiver.rb | 30 ++++++++++-- spec/components/email/receiver_spec.rb | 11 +++++ spec/fixtures/emails/boundary_email.txt | 61 +++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/emails/boundary_email.txt diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 8efad423575..ee68ce13450 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -18,13 +18,12 @@ module Email def process return Email::Receiver.results[:unprocessable] if @raw.blank? - message = Mail::Message.new(@raw) - return Email::Receiver.results[:unprocessable] if message.body.blank? + @message = Mail::Message.new(@raw) + parse_body - @body = EmailReplyParser.read(message.body.to_s).visible_text return Email::Receiver.results[:unprocessable] if @body.blank? - @reply_key = message.to.first + @reply_key = @message.to.first # Extract the `reply_key` from the format the site has specified tokens = SiteSetting.reply_by_email_address.split("%{reply_key}") @@ -43,6 +42,29 @@ module Email private + def parse_body + @body = @message.body.to_s.strip + return if @body.blank? + + # I really hate to have to do this, but there seems to be a bug in Mail::Message + # with content boundaries in emails. Until it is fixed, this hack removes stuff + # we don't want from emails bodies + content_type = @message.header['Content-Type'].to_s + if content_type.present? + boundary_match = content_type.match(/boundary\=(.*)$/) + boundary = boundary_match[1] if boundary_match && boundary_match[1].present? + if boundary.present? and @body.present? + + lines = @body.lines + lines = lines[1..-1] if lines.present? and lines[0] =~ /^--#{boundary}/ + lines = lines[1..-1] if lines.present? and lines[0] =~ /^Content-Type/ + + @body = lines.join.strip! + end + end + + @body = EmailReplyParser.read(@body).visible_text + end def create_reply diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 05bac6dd68e..a9ff44bd6d9 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -17,6 +17,17 @@ describe Email::Receiver do end end + describe "with a content boundary" do + let(:bounded_email) { File.read("#{Rails.root}/spec/fixtures/emails/boundary_email.txt") } + let(:receiver) { Email::Receiver.new(bounded_email) } + + it "does something" do + receiver.process + expect(receiver.body).to eq("I'll look into it, thanks!") + end + + end + describe "with a valid email" do let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } let(:valid_reply) { File.read("#{Rails.root}/spec/fixtures/emails/valid_reply.txt") } diff --git a/spec/fixtures/emails/boundary_email.txt b/spec/fixtures/emails/boundary_email.txt new file mode 100644 index 00000000000..78dba44f5af --- /dev/null +++ b/spec/fixtures/emails/boundary_email.txt @@ -0,0 +1,61 @@ + +MIME-Version: 1.0 +Received: by 10.64.14.41 with HTTP; Wed, 19 Jun 2013 06:29:41 -0700 (PDT) +In-Reply-To: <51c19490e928a_13442dd8ae892548@tree.mail> +References: <51c19490e928a_13442dd8ae892548@tree.mail> +Date: Wed, 19 Jun 2013 09:29:41 -0400 +Delivered-To: finn@adventuretime.ooo +Message-ID: +Subject: Re: [Adventure Time] jake mentioned you in 'peppermint butler is + missing' +From: Finn the Human +To: jake via Adventure Time +Content-Type: multipart/alternative; boundary=001a11c206a073876a04df81d2a9 + +--001a11c206a073876a04df81d2a9 +Content-Type: text/plain; charset=ISO-8859-1 + +I'll look into it, thanks! + + +On Wednesday, June 19, 2013, jake via Adventure Time wrote: + +> jake mentioned you in 'peppermint butler is missing' on Adventure +> Time: +> ------------------------------ +> +> yeah, just noticed this cc @jake +> ------------------------------ +> +> Please visit this link to respond: +> http://adventuretime.ooo/t/peppermint-butler-is-missing/7628/2 +> +> To unsubscribe from these emails, visit your user preferences +> . +> + +--001a11c206a073876a04df81d2a9 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +I'll look into it, thanks!

On Wednesday, June 19, 2= +013, jake via Adventure Time wrote:

sa= +m mentioned you in 'Duplicate message are shown in profile' on Adve= +nture Time

+ + +

yeah, just noticed this cc @eviltrout

+ +

Please visit this link to respond: http= +://adventuretime.ooo/t/peppermint-butler-is-missing/7628/2 + + +

To unsubscribe from these emails, visit your user preferences.

+
+ +--001a11c206a073876a04df81d2a9-- \ No newline at end of file From 33955d62f00151a19cd786117f27588130df9f94 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 19 Jun 2013 10:30:24 -0600 Subject: [PATCH 02/15] Change "unicode" terminology to "UTF-8" --- docs/DEVELOPMENT-OSX-NATIVE.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPMENT-OSX-NATIVE.md b/docs/DEVELOPMENT-OSX-NATIVE.md index 85a2a1c2dd1..c2ef50e34c6 100644 --- a/docs/DEVELOPMENT-OSX-NATIVE.md +++ b/docs/DEVELOPMENT-OSX-NATIVE.md @@ -6,9 +6,16 @@ OS X has become a popular platform for developing Ruby on Rails applications; as Obviously, if you **already** develop Ruby on OS X, a lot of this will be redundant, because you'll have already done it, or something like it. If that's the case, you may well be able to just install Ruby 2.0 using RVM and get started! Discourse has enough dependencies, however (note: not a criticism!) that there's a good chance you'll find **something** else in this document that's useful for getting your Discourse development started! -## Unicode +## UTF-8 -OS X 10.8 uses Unicode by default. You can, of course, double-check this by examining LANG, which appears to be the only relevant environment variable. +OS X 10.8 uses UTF-8 by default. You can, of course, double-check this by examining LANG, which appears to be the only relevant environment variable. + +You should see this: + +```sh +$ echo $LANG +en_US.UTF +``` ## OSX Development Tools From 5ef6714d48b78e4ae304e957d990f31b60f94ee3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 13:13:12 -0400 Subject: [PATCH 03/15] New site setting: `minimum_topics_similar`, allows you to specify a minimum amount of topics that need to be in the database before it will suggest similar topics as a user creates a post. --- app/controllers/topics_controller.rb | 6 +++- app/models/site_setting.rb | 2 ++ config/locales/server.en.yml | 4 ++- spec/controllers/topics_controller_spec.rb | 38 +++++++++++++++++----- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 185ba7e106e..2c54ae1507c 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -84,7 +84,11 @@ class TopicsController < ApplicationController raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length - topics = Topic.similar_to(title, raw, current_user) + # Only suggest similar topics if the site has a minimmum amount of topics present. + if Topic.count > SiteSetting.minimum_topics_similar + topics = Topic.similar_to(title, raw, current_user) + end + render_serialized(topics, BasicTopicSerializer) end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index e12a9bd3764..10e19ceae86 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -222,6 +222,8 @@ class SiteSetting < ActiveRecord::Base client_setting(:topic_views_heat_medium, 2000) client_setting(:topic_views_heat_high, 5000) + setting(:minimum_topics_similar, 50) + def self.generate_api_key! self.api_key = SecureRandom.hex(32) end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 13b8f901e52..5649f1a48ef 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -383,7 +383,7 @@ en: memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.' facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' cas_config_warning: 'The server is configured to allow signup and log in with CAS (enable_cas_logins), but the hostname and domain name values are not set.' - twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' + twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/environments/production.rb file and ensure that the config.action_mailer settings are correct. See the failed jobs in Sidekiq.' default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the Site Settings." @@ -623,6 +623,8 @@ en: pop3s_polling_username: "The username for the POP3S account to poll for email" pop3s_polling_password: "The password for the POP3S account to poll for email" + minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented." + notification_types: mentioned: "%{display_username} mentioned you in %{link}" diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index ba910a33fc2..d8d08de8100 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -161,18 +161,40 @@ describe TopicsController do -> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters) end - it "delegates to Topic.similar_to" do - Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)]) - xhr :get, :similar_to, title: title, raw: raw - end + describe "minimum_topics_similar" do - context "logged in" do - let(:user) { log_in } + before do + SiteSetting.stubs(:minimum_topics_similar).returns(30) + end - it "passes a user throught if logged in" do - Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)]) + after do xhr :get, :similar_to, title: title, raw: raw end + + describe "With enough topics" do + before do + Topic.stubs(:count).returns(50) + end + + it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do + Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)]) + end + + describe "with a logged in user" do + let(:user) { log_in } + + it "passes a user through if logged in" do + Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)]) + end + end + + end + + it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do + Topic.stubs(:count).returns(10) + Topic.expects(:similar_to).never + end + end end From 3e0f47705a0fe543ed2af4b923cd5dafa5b44035 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 19 Jun 2013 09:46:09 -0400 Subject: [PATCH 04/15] Remove some extra padding on the right of post info --- app/assets/stylesheets/application/topic-post.css.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss index 82a492bdf27..18482576a50 100644 --- a/app/assets/stylesheets/application/topic-post.css.scss +++ b/app/assets/stylesheets/application/topic-post.css.scss @@ -496,15 +496,11 @@ } .topic-meta-data-inside { float: right; - margin-right: 10px; z-index: 490; .post-info { font-size: 12px; display: inline-block; margin-right: 12px; - &:first-child { - margin-right: 0; - } &.edits { a, a:visited { color: #aaa; From 8b9b87f42e6edb2fde0f7bb82c7c3959ebf3e504 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 19 Jun 2013 14:30:05 -0400 Subject: [PATCH 05/15] Remove extra padding at top of posts --- app/assets/stylesheets/application/topic-post.css.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss index 18482576a50..dac676909fb 100644 --- a/app/assets/stylesheets/application/topic-post.css.scss +++ b/app/assets/stylesheets/application/topic-post.css.scss @@ -497,6 +497,7 @@ .topic-meta-data-inside { float: right; z-index: 490; + margin-left: 20px; .post-info { font-size: 12px; display: inline-block; @@ -518,7 +519,7 @@ position: relative; .contents { .cooked { - padding: 25px 10px 0; + padding: 10px 10px 0; } position: relative; border: 1px solid #b9b9b9; From d5643551cc6863fa2596f3f4daeefd73babd0329 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 14:35:56 -0400 Subject: [PATCH 06/15] Remove dependency on Webrick for QUnit tests --- Gemfile | 1 - Gemfile.lock | 2 -- lib/tasks/qunit.rake | 2 -- 3 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index c8cb3fd3b79..5d32bfd68fd 100644 --- a/Gemfile +++ b/Gemfile @@ -118,7 +118,6 @@ 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 52811943135..eae4c8a2729 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -472,7 +472,6 @@ GEM uglifier (2.0.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) - webrick (1.3.1) PLATFORMS ruby @@ -573,4 +572,3 @@ DEPENDENCIES turbo-sprockets-rails3 uglifier vestal_versions! - webrick diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index b75296a667a..14eb3ee1a1b 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -3,7 +3,6 @@ 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" @@ -12,7 +11,6 @@ task "qunit:test" => :environment do 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 From e263bb3c0a704ca1653e1ad0bffee60c5a63f1d4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 19 Jun 2013 16:43:16 -0400 Subject: [PATCH 07/15] Anons should be able to see post history --- app/controllers/posts_controller.rb | 2 +- spec/controllers/posts_controller_spec.rb | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 9a9d5a086b9..d431d92dfd9 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -4,7 +4,7 @@ require_dependency 'post_destroyer' class PostsController < ApplicationController # Need to be logged in for all actions here - before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link] + before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :versions] skip_before_filter :store_incoming_links, only: [:short_link] skip_before_filter :check_xhr, only: [:markdown,:short_link] diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 0936b7a9879..292b573df95 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -56,13 +56,7 @@ describe PostsController do describe 'versions' do - it 'raises an exception when not logged in' do - lambda { xhr :get, :versions, post_id: 123 }.should raise_error(Discourse::NotLoggedIn) - end - - describe 'when logged in' do - let(:post) { Fabricate(:post, user: log_in) } - + shared_examples 'posts_controller versions examples' do it "raises an error if the user doesn't have permission to see the post" do Guardian.any_instance.expects(:can_see?).with(post).returns(false) xhr :get, :versions, post_id: post.id @@ -73,7 +67,16 @@ describe PostsController do xhr :get, :versions, post_id: post.id ::JSON.parse(response.body).should be_present end + end + context 'when not logged in' do + let(:post) { Fabricate(:post) } + include_examples 'posts_controller versions examples' + end + + context 'when logged in' do + let(:post) { Fabricate(:post, user: log_in) } + include_examples 'posts_controller versions examples' end end From 8c4aac7f946b1a392238e098a8022a01ef861846 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 15:06:23 -0400 Subject: [PATCH 08/15] Migrate all jasmine specs to Qunit. Removed Jasmine. --- Gemfile | 2 - Gemfile.lock | 19 -- Guardfile | 17 -- config/environments/test.rb | 3 - config/initializers/06-mini_profiler.rb | 4 +- docs/SOFTWARE.md | 1 - .../components/click_track_spec.js | 221 ------------------ spec/javascripts/components/utilities_spec.js | 63 ----- spec/javascripts/hacks.js | 21 -- spec/javascripts/models/report_spec.js | 98 -------- spec/javascripts/models/user_action_spec.js | 36 --- spec/javascripts/preload_store_spec.js | 106 --------- spec/javascripts/sanitize_spec.js | 15 -- spec/javascripts/spec.css | 3 - spec/javascripts/spec.js | 51 ---- .../components/click_track_test.js | 173 ++++++++++++++ test/javascripts/components/markdown_test.js | 8 +- .../components/preload_store_test.js | 73 ++++++ test/javascripts/components/utilities_test.js | 52 +++++ test/javascripts/integration/header_test.js | 2 + .../integration/list_topics_test.js | 2 + test/javascripts/models/report_test.js | 60 +++++ test/javascripts/models/user_action_test.js | 29 +++ test/javascripts/test_helper.js | 1 + 24 files changed, 401 insertions(+), 659 deletions(-) delete mode 100644 spec/javascripts/components/click_track_spec.js delete mode 100644 spec/javascripts/components/utilities_spec.js delete mode 100644 spec/javascripts/hacks.js delete mode 100644 spec/javascripts/models/report_spec.js delete mode 100644 spec/javascripts/models/user_action_spec.js delete mode 100644 spec/javascripts/preload_store_spec.js delete mode 100644 spec/javascripts/sanitize_spec.js delete mode 100644 spec/javascripts/spec.css delete mode 100644 spec/javascripts/spec.js create mode 100644 test/javascripts/components/click_track_test.js create mode 100644 test/javascripts/components/preload_store_test.js create mode 100644 test/javascripts/components/utilities_test.js create mode 100644 test/javascripts/models/report_test.js create mode 100644 test/javascripts/models/user_action_test.js 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("

hello

"); - expect(sanitized).toBe("

hello

"); - }); - -}); 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("

hello

"), "

hello

"); + +}); \ 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(''); From 7772ef752937a3afd492c14ad5c459a2065435eb Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 18:19:36 -0400 Subject: [PATCH 09/15] Change travis to use qunit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fc5181262d2..59f44be2963 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,6 @@ before_script: - rake db:migrate - export RUBY_GC_MALLOC_LIMIT=50000000 bundler_args: --without development -script: 'rake jshint && rake spec && bundle exec guard-jasmine --server-timeout=60' +script: 'rake jshint && rake spec && bundle exec rake qunit:test" services: - redis-server From 285e27fe84d6c9bce1aa00a54d684e0fea887780 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Jun 2013 18:22:14 -0400 Subject: [PATCH 10/15] OOPS: Invalid YAML --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 59f44be2963..754e69dbb6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,6 @@ before_script: - rake db:migrate - export RUBY_GC_MALLOC_LIMIT=50000000 bundler_args: --without development -script: 'rake jshint && rake spec && bundle exec rake qunit:test" +script: 'rake jshint && rake spec && bundle exec rake qunit:test' services: - redis-server From c2568d9a636efe0f3f0844ffe8ddd6e564976bca Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 19 Jun 2013 18:36:32 -0400 Subject: [PATCH 11/15] Version bump to v0.9.3.5 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index a37952e88b4..a7f16e86cf2 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 0 MINOR = 9 TINY = 3 - PRE = nil + PRE = 5 STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end From da582fc202c7ea4c20c60ee561779cab9d41bf9b Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 19 Jun 2013 16:01:39 -0700 Subject: [PATCH 12/15] switch bookmark from yellow to blue --- app/assets/stylesheets/foundation/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/foundation/variables.scss b/app/assets/stylesheets/foundation/variables.scss index 2f5619b7569..5daf1864088 100644 --- a/app/assets/stylesheets/foundation/variables.scss +++ b/app/assets/stylesheets/foundation/variables.scss @@ -158,7 +158,7 @@ $muted-important-link-color: #5d5d5d; // Colors based on basics $topicMenuColor: darken($white, 80%); -$bookmarkColor: #b5b500; +$bookmarkColor: #0088CC; $tag_color: #e1ecf9; From f1fd29003d53b3cebb50995265cdf5f71ff24f89 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 19 Jun 2013 16:09:57 -0700 Subject: [PATCH 13/15] switch medium time format to "mins" vs "minutes" --- config/locales/client.en.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5e2bc97438e..d89701f3af2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -45,8 +45,8 @@ en: other: "%{count}y" medium: x_minutes: - one: "1 minute" - other: "%{count} minutes" + one: "1 min" + other: "%{count} mins" x_hours: one: "1 hour" other: "%{count} hours" @@ -55,8 +55,8 @@ en: other: "%{count} days" medium_with_ago: x_minutes: - one: "1 minute ago" - other: "%{count} minutes ago" + one: "1 min ago" + other: "%{count} mins ago" x_hours: one: "1 hour ago" other: "%{count} hours ago" From 6424a3d3e163af7dd1bb734a2e799ba5405b8ccc Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 19 Jun 2013 16:31:18 -0700 Subject: [PATCH 14/15] remove max-width on h1, seems unnecessary? --- app/assets/stylesheets/application/topic.css.scss | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/assets/stylesheets/application/topic.css.scss b/app/assets/stylesheets/application/topic.css.scss index e5f3fd96034..82eef3b50c5 100644 --- a/app/assets/stylesheets/application/topic.css.scss +++ b/app/assets/stylesheets/application/topic.css.scss @@ -15,16 +15,7 @@ font-size: 22px; line-height: 28px; } - @include medium-width { - h1 { - max-width: 735px; - } - } - @include small-width { - h1 { - max-width: 690px; - } - } + .star { height: 40px; font-size: 20px !important; From d176b1d7233d6a12682cbe6e9923815d2becaaf0 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Jun 2013 15:55:18 +1000 Subject: [PATCH 15/15] A simple setup dev enviroment script --- script/setup_dev | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 script/setup_dev diff --git a/script/setup_dev b/script/setup_dev new file mode 100755 index 00000000000..133d76e5bad --- /dev/null +++ b/script/setup_dev @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby + +root = File.expand_path('../../', __FILE__) + + +puts "Setting up local development environment!" +puts + +Dir.chdir root + +puts "Running: bundle" +system "bundle" + + +redis_yml = root + '/config/redis.yml' +database_yml = root + '/config/database.yml' + +if !File.exists?(redis_yml) + puts "Creating config/redis.yml" + system "cp #{root}/config/redis.yml.sample #{redis_yml}" +end + +if !File.exists?(database_yml) + puts "Creating config/database.yml" + system "cp #{root}/config/database.yml.development-sample #{database_yml}" + + puts "Creating development database" + system "bundle exec rake db:create" + + puts "Migrating development database" + system "bundle exec rake db:migrate" + + puts "Creating test database" + system "RAILS_ENV=test bundle exec rake db:create" + + puts "Migrating test database" + system "RAILS_ENV=test bundle exec rake db:migrate" +end + +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") + +if User.count == 0 + puts "Setting up an admin user" + admin = User.new + admin.email = "admin@localhost" + admin.username = "admin" + admin.password = "password" + admin.save + admin.grant_admin! + admin.change_trust_level!(:regular) + admin.email_tokens.update_all(confirmed: true) + + puts "An administrator was created:" + puts "Username: admin" + puts "Password: password" + puts + puts "To get started run: bundle exec thin start" +end + +