Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2013-06-20 17:42:29 +10:00
commit 08df4c41cc
42 changed files with 629 additions and 710 deletions

View File

@ -9,6 +9,6 @@ before_script:
- rake db:migrate - rake db:migrate
- export RUBY_GC_MALLOC_LIMIT=50000000 - export RUBY_GC_MALLOC_LIMIT=50000000
bundler_args: --without development 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: services:
- redis-server - redis-server

View File

@ -103,10 +103,8 @@ group :test, :development do
gem 'certified', require: false gem 'certified', require: false
gem 'fabrication', require: false gem 'fabrication', require: false
gem 'qunit-rails' gem 'qunit-rails'
gem 'guard-jasmine', require: false
gem 'guard-rspec', require: false gem 'guard-rspec', require: false
gem 'guard-spork', require: false gem 'guard-spork', require: false
gem 'jasminerice'
gem 'mocha', require: false gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : 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 gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
@ -118,7 +116,6 @@ group :test, :development do
gem 'rspec-given' gem 'rspec-given'
gem 'pry-rails' gem 'pry-rails'
gem 'pry-nav' gem 'pry-nav'
gem 'webrick'
end end
group :development do group :development do

View File

@ -151,13 +151,6 @@ GEM
clockwork (0.5.0) clockwork (0.5.0)
tzinfo (~> 0.3.35) tzinfo (~> 0.3.35)
coderay (1.0.9) 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) connection_pool (1.0.0)
daemons (1.1.9) daemons (1.1.9)
debug_inspector (0.0.2) debug_inspector (0.0.2)
@ -206,11 +199,6 @@ GEM
lumberjack (>= 1.0.2) lumberjack (>= 1.0.2)
pry (>= 0.9.10) pry (>= 0.9.10)
thor (>= 0.14.6) 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-jshint-on-rails (0.0.2)
guard (>= 1.0.0) guard (>= 1.0.0)
jshint_on_rails (>= 1.0.2) jshint_on_rails (>= 1.0.2)
@ -221,8 +209,6 @@ GEM
childprocess (>= 0.2.3) childprocess (>= 0.2.3)
guard (>= 1.1) guard (>= 1.1)
spork (>= 0.8.4) spork (>= 0.8.4)
haml (4.0.2)
tilt
handlebars-source (1.0.0.rc4) handlebars-source (1.0.0.rc4)
has_ip_address (0.0.1) has_ip_address (0.0.1)
hashie (2.0.4) hashie (2.0.4)
@ -239,9 +225,6 @@ GEM
image_size (1.1.2) image_size (1.1.2)
image_sorcery (1.1.0) image_sorcery (1.1.0)
in_threads (1.1.1) in_threads (1.1.1)
jasminerice (0.0.10)
coffee-rails
haml
journey (1.0.4) journey (1.0.4)
jshint_on_rails (1.0.2) jshint_on_rails (1.0.2)
json (1.7.7) json (1.7.7)
@ -472,7 +455,6 @@ GEM
uglifier (2.0.1) uglifier (2.0.1)
execjs (>= 0.3.0) execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2) multi_json (~> 1.0, >= 1.0.2)
webrick (1.3.1)
PLATFORMS PLATFORMS
ruby ruby
@ -503,7 +485,6 @@ DEPENDENCIES
fast_xs fast_xs
fastimage fastimage
fog fog
guard-jasmine
guard-jshint-on-rails guard-jshint-on-rails
guard-rspec guard-rspec
guard-spork guard-spork
@ -513,7 +494,6 @@ DEPENDENCIES
hiredis hiredis
image_optim image_optim
image_sorcery image_sorcery
jasminerice
jshint_on_rails jshint_on_rails
librarian (>= 0.0.25) librarian (>= 0.0.25)
listen listen
@ -573,4 +553,3 @@ DEPENDENCIES
turbo-sprockets-rails3 turbo-sprockets-rails3
uglifier uglifier
vestal_versions! vestal_versions!
webrick

View File

@ -3,23 +3,6 @@ require 'terminal-notifier-guard' if RUBY_PLATFORM.include?('darwin')
phantom_path = File.expand_path('~/phantomjs/bin/phantomjs') phantom_path = File.expand_path('~/phantomjs/bin/phantomjs')
phantom_path = nil unless File.exists?(phantom_path) 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 # verify that we pass jshint
# see https://github.com/MrOrz/guard-jshint-on-rails # see https://github.com/MrOrz/guard-jshint-on-rails
guard 'jshint-on-rails', config_path: 'config/jshint.yml' do guard 'jshint-on-rails', config_path: 'config/jshint.yml' do

View File

@ -496,15 +496,12 @@
} }
.topic-meta-data-inside { .topic-meta-data-inside {
float: right; float: right;
margin-right: 10px;
z-index: 490; z-index: 490;
margin-left: 20px;
.post-info { .post-info {
font-size: 12px; font-size: 12px;
display: inline-block; display: inline-block;
margin-right: 12px; margin-right: 12px;
&:first-child {
margin-right: 0;
}
&.edits { &.edits {
a, a:visited { a, a:visited {
color: #aaa; color: #aaa;
@ -522,7 +519,7 @@
position: relative; position: relative;
.contents { .contents {
.cooked { .cooked {
padding: 25px 10px 0; padding: 10px 10px 0;
} }
position: relative; position: relative;
border: 1px solid #b9b9b9; border: 1px solid #b9b9b9;

View File

@ -15,16 +15,7 @@
font-size: 22px; font-size: 22px;
line-height: 28px; line-height: 28px;
} }
@include medium-width {
h1 {
max-width: 735px;
}
}
@include small-width {
h1 {
max-width: 690px;
}
}
.star { .star {
height: 40px; height: 40px;
font-size: 20px !important; font-size: 20px !important;

View File

@ -158,7 +158,7 @@ $muted-important-link-color: #5d5d5d;
// Colors based on basics // Colors based on basics
$topicMenuColor: darken($white, 80%); $topicMenuColor: darken($white, 80%);
$bookmarkColor: #b5b500; $bookmarkColor: #0088CC;
$tag_color: #e1ecf9; $tag_color: #e1ecf9;

View File

@ -4,7 +4,7 @@ require_dependency 'post_destroyer'
class PostsController < ApplicationController class PostsController < ApplicationController
# Need to be logged in for all actions here # 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 :store_incoming_links, only: [:short_link]
skip_before_filter :check_xhr, only: [:markdown,:short_link] skip_before_filter :check_xhr, only: [:markdown,:short_link]

View File

@ -84,7 +84,11 @@ class TopicsController < ApplicationController
raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length 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 raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length
# 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) topics = Topic.similar_to(title, raw, current_user)
end
render_serialized(topics, BasicTopicSerializer) render_serialized(topics, BasicTopicSerializer)
end end

View File

@ -222,6 +222,8 @@ class SiteSetting < ActiveRecord::Base
client_setting(:topic_views_heat_medium, 2000) client_setting(:topic_views_heat_medium, 2000)
client_setting(:topic_views_heat_high, 5000) client_setting(:topic_views_heat_high, 5000)
setting(:minimum_topics_similar, 50)
def self.generate_api_key! def self.generate_api_key!
self.api_key = SecureRandom.hex(32) self.api_key = SecureRandom.hex(32)
end end

View File

@ -10,9 +10,6 @@ Discourse::Application.configure do
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.serve_static_assets = true 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 # Log error messages when you accidentally call methods on nil
config.whiny_nils = true config.whiny_nils = true

View File

@ -19,7 +19,7 @@ if defined?(Rack::MiniProfiler)
(env['PATH_INFO'] !~ /^\/message-bus/) && (env['PATH_INFO'] !~ /^\/message-bus/) &&
(env['PATH_INFO'] !~ /topics\/timings/) && (env['PATH_INFO'] !~ /topics\/timings/) &&
(env['PATH_INFO'] !~ /assets/) && (env['PATH_INFO'] !~ /assets/) &&
(env['PATH_INFO'] !~ /jasmine/) && (env['PATH_INFO'] !~ /qunit/) &&
(env['PATH_INFO'] !~ /users\/.*\/avatar/) && (env['PATH_INFO'] !~ /users\/.*\/avatar/) &&
(env['PATH_INFO'] !~ /srv\/status/) && (env['PATH_INFO'] !~ /srv\/status/) &&
(env['PATH_INFO'] !~ /commits-widget/) (env['PATH_INFO'] !~ /commits-widget/)

View File

@ -45,8 +45,8 @@ en:
other: "%{count}y" other: "%{count}y"
medium: medium:
x_minutes: x_minutes:
one: "1 minute" one: "1 min"
other: "%{count} minutes" other: "%{count} mins"
x_hours: x_hours:
one: "1 hour" one: "1 hour"
other: "%{count} hours" other: "%{count} hours"
@ -55,8 +55,8 @@ en:
other: "%{count} days" other: "%{count} days"
medium_with_ago: medium_with_ago:
x_minutes: x_minutes:
one: "1 minute ago" one: "1 min ago"
other: "%{count} minutes ago" other: "%{count} mins ago"
x_hours: x_hours:
one: "1 hour ago" one: "1 hour ago"
other: "%{count} hours ago" other: "%{count} hours ago"

View File

@ -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.' 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 <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-facebook-logins" target="_blank">See this guide to learn more</a>.' 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 <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-facebook-logins" target="_blank">See this guide to learn more</a>.'
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.' 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 <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.' 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 <a href="/admin/site_site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.'
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 <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.' 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 <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.'
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. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.' 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. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.'
default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the <a href='/admin/site_settings'>Site Settings</a>." default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the <a href='/admin/site_settings'>Site Settings</a>."
@ -623,6 +623,8 @@ en:
pop3s_polling_username: "The username for the POP3S account to poll for email" 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" 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: notification_types:
mentioned: "%{display_username} mentioned you in %{link}" mentioned: "%{display_username} mentioned you in %{link}"

View File

@ -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! 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 ## OSX Development Tools

View File

@ -37,7 +37,6 @@ The following Ruby Gems are used in Discourse:
* [rspec](https://rubygems.org/gems/rspec) * [rspec](https://rubygems.org/gems/rspec)
* [shoulda](https://rubygems.org/gems/shoulda) * [shoulda](https://rubygems.org/gems/shoulda)
* [turn](https://rubygems.org/gems/turn) * [turn](https://rubygems.org/gems/turn)
* [jasminerice](https://rubygems.org/gems/jasminerice)
* [fabrication](https://rubygems.org/gems/fabrication) * [fabrication](https://rubygems.org/gems/fabrication)
* [mocha](https://rubygems.org/gems/mocha) * [mocha](https://rubygems.org/gems/mocha)
* [simplecov](https://rubygems.org/gems/simplecov) * [simplecov](https://rubygems.org/gems/simplecov)

View File

@ -18,13 +18,12 @@ module Email
def process def process
return Email::Receiver.results[:unprocessable] if @raw.blank? return Email::Receiver.results[:unprocessable] if @raw.blank?
message = Mail::Message.new(@raw) @message = Mail::Message.new(@raw)
return Email::Receiver.results[:unprocessable] if message.body.blank? parse_body
@body = EmailReplyParser.read(message.body.to_s).visible_text
return Email::Receiver.results[:unprocessable] if @body.blank? 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 # Extract the `reply_key` from the format the site has specified
tokens = SiteSetting.reply_by_email_address.split("%{reply_key}") tokens = SiteSetting.reply_by_email_address.split("%{reply_key}")
@ -43,6 +42,29 @@ module Email
private 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 def create_reply

View File

@ -3,7 +3,6 @@ desc "Runs the qunit test suite"
task "qunit:test" => :environment do task "qunit:test" => :environment do
require "rack" require "rack"
require "webrick"
unless %x{which phantomjs > /dev/null 2>&1} unless %x{which phantomjs > /dev/null 2>&1}
abort "PhantomJS is not installed. Download from http://phantomjs.org" 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 port = ENV['TEST_SERVER_PORT'] || 60099
server = Thread.new do server = Thread.new do
Rack::Server.start(:config => "config.ru", Rack::Server.start(:config => "config.ru",
:Logger => WEBrick::Log.new("/dev/null"),
:AccessLog => [], :AccessLog => [],
:Port => port) :Port => port)
end end

View File

@ -5,7 +5,7 @@ module Discourse
MAJOR = 0 MAJOR = 0
MINOR = 9 MINOR = 9
TINY = 3 TINY = 3
PRE = nil PRE = 5
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end end

60
script/setup_dev Executable file
View File

@ -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

View File

@ -17,6 +17,17 @@ describe Email::Receiver do
end end
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 describe "with a valid email" do
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
let(:valid_reply) { File.read("#{Rails.root}/spec/fixtures/emails/valid_reply.txt") } let(:valid_reply) { File.read("#{Rails.root}/spec/fixtures/emails/valid_reply.txt") }

View File

@ -56,13 +56,7 @@ describe PostsController do
describe 'versions' do describe 'versions' do
it 'raises an exception when not logged in' do shared_examples 'posts_controller versions examples' 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) }
it "raises an error if the user doesn't have permission to see the post" 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) Guardian.any_instance.expects(:can_see?).with(post).returns(false)
xhr :get, :versions, post_id: post.id xhr :get, :versions, post_id: post.id
@ -73,7 +67,16 @@ describe PostsController do
xhr :get, :versions, post_id: post.id xhr :get, :versions, post_id: post.id
::JSON.parse(response.body).should be_present ::JSON.parse(response.body).should be_present
end 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
end end

View File

@ -161,22 +161,44 @@ describe TopicsController do
-> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters) -> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters)
end end
it "delegates to Topic.similar_to" do describe "minimum_topics_similar" do
Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)])
before do
SiteSetting.stubs(:minimum_topics_similar).returns(30)
end
after do
xhr :get, :similar_to, title: title, raw: raw xhr :get, :similar_to, title: title, raw: raw
end end
context "logged in" do 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 } let(:user) { log_in }
it "passes a user throught if logged in" do it "passes a user through if logged in" do
Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)]) Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)])
xhr :get, :similar_to, title: title, raw: raw
end end
end 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
context 'clear_pin' do context 'clear_pin' do
it 'needs you to be logged in' do it 'needs you to be logged in' do

61
spec/fixtures/emails/boundary_email.txt vendored Normal file
View File

@ -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: <CADkmRcL=2aPV7jOcs2i00QGZNwmjYj3qMaJTsKnB7DTP5sgyFQ@mail.gmail.com>
Subject: Re: [Adventure Time] jake mentioned you in 'peppermint butler is
missing'
From: Finn the Human <finn@adventuretime.ooo>
To: jake via Adventure Time <math+96dcd9072ba9072d06226009d4223a6e@tree.mail>
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
> <http://adventuretime.ooo/user_preferences>.
>
--001a11c206a073876a04df81d2a9
Content-Type: text/html; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
I&#39;ll look into it, thanks!<span></span><br><br>On Wednesday, June 19, 2=
013, jake via Adventure Time wrote:<br><blockquote class=3D"gmail_quote" st=
yle=3D"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><p>sa=
m mentioned you in &#39;Duplicate message are shown in profile&#39; on Adve=
nture Time</p>
<hr><p>yeah, just noticed this cc <a href=3D"http://users/eviltrout" target=
=3D"_blank">@eviltrout</a> </p>
<hr><p>Please visit this link to respond: <a href=3D"http://adventuretime.oo=
o/t/duplicate-message-are-shown-in-profile/7628/2" target=3D"_blank">http=
://adventuretime.ooo/t/peppermint-butler-is-missing/7628/2</a></=
p>
<p>To unsubscribe from these emails, visit your <a href=3D"http://adventuret=
ime.ooo/user_preferences" target=3D"_blank">user preferences</a>.</p>
</blockquote>
--001a11c206a073876a04df81d2a9--

View File

@ -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 = [
'<div id="topic" id="1337">',
' <article data-post-id="42" data-user-id="3141">',
' <a href="http://www.google.com">google.com</a>',
' <a class="lightbox back quote-other-topic" href="http://www.google.com">google.com</a>',
' <a id="with-badge" data-user-id="314" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <a id="with-badge-but-not-mine" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <div class="onebox-result">',
' <a id="inside-onebox" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' </div>',
' <a id="same-site" href="http://discuss.domain.com">forum</a>',
' </article>',
'</div>'].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');
});
});
});

View File

@ -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);
});
});
});

View File

@ -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();
}
$('<div id="main"><div class="rootElement"></div></div>').appendTo($('body')).hide();
Discourse.SiteSettings = {}
Discourse.Router.map(function() {
this.route("jasmine",{path: "/jasmine"});
Discourse.routeBuilder.apply(this)
});
}
})()

View File

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

View File

@ -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);
});
});
});

View File

@ -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');
});
});
});
});

View File

@ -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("<div><script>alert('hi');</script></div>");
expect(sanitized).toBe("<div></div>");
});
it("strips disallowed attributes", function(){
var sanitized = sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>");
expect(sanitized).toBe("<div><p class=\"funky\">hello</p></div>");
});
});

View File

@ -1,3 +0,0 @@
/*
*/

View File

@ -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

View File

@ -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([
'<div id="topic" id="1337">',
' <article data-post-id="42" data-user-id="3141">',
' <a href="http://www.google.com">google.com</a>',
' <a class="lightbox back quote-other-topic" href="http://www.google.com">google.com</a>',
' <a id="with-badge" data-user-id="314" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <a id="with-badge-but-not-mine" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <div class="onebox-result">',
' <a id="inside-onebox" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' </div>',
' <a id="same-site" href="http://discuss.domain.com">forum</a>',
' </article>',
'</div>'].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'));
});

View File

@ -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"); module("Discourse.Markdown");
@ -93,3 +93,9 @@ test("Oneboxing", function() {
}); });
test("SanitizeHTML", function() {
equal(sanitizeHtml("<div><script>alert('hi');</script></div>"), "<div></div>");
equal(sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p class=\"funky\">hello</p></div>");
});

View File

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

View File

@ -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);
});

View File

@ -13,6 +13,8 @@ module("Header", {
test("/", function() { test("/", function() {
visit("/").then(function() { visit("/").then(function() {
expect(2);
ok(exists("header"), "The header was rendered"); ok(exists("header"), "The header was rendered");
ok(exists("#site-logo"), "The logo was shown"); ok(exists("#site-logo"), "The logo was shown");
}); });

View File

@ -13,6 +13,8 @@ module("List Topics", {
test("/", function() { test("/", function() {
visit("/").then(function() { visit("/").then(function() {
expect(2);
ok(exists("#topic-list"), "The list of topics was rendered"); ok(exists("#topic-list"), "The list of topics was rendered");
ok(count('#topic-list .topic-list-item') > 0, "has topics"); ok(count('#topic-list .topic-list-item') > 0, "has topics");
}); });

View File

@ -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/));
});

View File

@ -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);
});

View File

@ -52,6 +52,7 @@ sinon.config = {
// Trick JSHint into allow document.write // Trick JSHint into allow document.write
var d = document; var d = document;
d.write('<div id="qunit-scratch" style="display:none"></div>');
d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>'); d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>');
d.write('<style>#ember-testing-container { position: absolute; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; } #ember-testing { zoom: 50%; }</style>'); d.write('<style>#ember-testing-container { position: absolute; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; } #ember-testing { zoom: 50%; }</style>');