diff --git a/Gemfile b/Gemfile index ac4ef6e150..5730a447ee 100644 --- a/Gemfile +++ b/Gemfile @@ -50,4 +50,9 @@ group :development, :test do gem 'rspec-rails', '>= 2.0.0.beta.20' gem 'simplecov', :require => false gem 'sqlite3' + gem 'cucumber-rails' + gem 'cucumber-rails-training-wheels' # some pre-fabbed step definitions + gem 'database_cleaner' # to clear Cucumber's test database between runs + gem 'capybara' # lets Cucumber pretend to be a web browser + gem 'launchy' # a useful debugging aid for user stories end diff --git a/Gemfile.lock b/Gemfile.lock index 286f8b9fa7..be57162897 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,23 +36,50 @@ GEM arel (2.0.10) bluecloth (2.2.0) builder (2.1.2) + capybara (1.1.2) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 0.1.4) + childprocess (0.3.1) + ffi (~> 1.0.6) coderay (0.9.8) columnize (0.3.6) + cucumber (1.1.9) + builder (>= 2.1.2) + diff-lcs (>= 1.1.2) + gherkin (~> 2.9.0) + json (>= 1.4.6) + term-ansicolor (>= 1.0.6) + cucumber-rails (1.3.0) + capybara (>= 1.1.2) + cucumber (>= 1.1.8) + nokogiri (>= 1.5.0) + cucumber-rails-training-wheels (1.0.0) + cucumber-rails (>= 1.1.1) + database_cleaner (0.7.1) diff-lcs (1.1.3) erubis (2.6.6) abstract (>= 1.0.0) factory_girl (2.2.0) activesupport + ffi (1.0.11) flickraw (0.9.5) flickraw-cached (20110920) flickraw (>= 0.9) + gherkin (2.9.0) + json (>= 1.4.6) htmlentities (4.3.1) i18n (0.5.0) json (1.6.5) + json_pure (1.6.5) kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) railties (>= 3.0.0) + launchy (2.0.3) linecache19 (0.5.12) ruby_core_source (>= 0.1.4) mail (2.2.19) @@ -115,12 +142,19 @@ GEM ruby_core_source (0.1.5) archive-tar-minitar (>= 0.5.2) rubypants (0.2.0) + rubyzip (0.9.6.1) + selenium-webdriver (2.13.0) + childprocess (>= 0.2.1) + ffi (~> 1.0.9) + json_pure + rubyzip simplecov (0.6.1) multi_json (~> 1.0) simplecov-html (~> 0.5.3) simplecov-html (0.5.3) sqlite3 (1.3.5) subexec (0.0.4) + term-ansicolor (1.0.7) thor (0.14.6) treetop (1.4.10) polyglot @@ -131,6 +165,8 @@ GEM nokogiri (>= 1.2.0) rack (>= 1.0) rack-test (>= 0.5.3) + xpath (0.1.4) + nokogiri (~> 1.3) PLATFORMS ruby @@ -141,12 +177,17 @@ DEPENDENCIES acts_as_tree_rails3 addressable (~> 2.1.0) bluecloth (>= 2.0.5) + capybara coderay (~> 0.9) + cucumber-rails + cucumber-rails-training-wheels + database_cleaner factory_girl (= 2.2.0) flickraw-cached htmlentities json kaminari + launchy mini_magick (= 1.3.3) pg rails (= 3.0.10) diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..19b288df9d --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/config/database.yml b/config/database.yml index ecd1ce3d9f..69adc959da 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,7 +3,7 @@ development: database: db/db_development timeout: 5000 -test: +test: &test adapter: sqlite3 database: db/db_test timeout: 5000 @@ -12,3 +12,6 @@ production: adapter: postgresql database: db_production timeout: 5000 + +cucumber: + <<: *test \ No newline at end of file diff --git a/db/db_development b/db/db_development new file mode 100644 index 0000000000..d57a0c3aa4 Binary files /dev/null and b/db/db_development differ diff --git a/db/db_test b/db/db_test new file mode 100644 index 0000000000..23d02c1143 Binary files /dev/null and b/db/db_test differ diff --git a/features/auth.feature b/features/auth.feature new file mode 100644 index 0000000000..fec07b5c3c --- /dev/null +++ b/features/auth.feature @@ -0,0 +1,26 @@ +Feature: A non_admin can not merge articles + + As a admin + I should be able to merge articles with same topic from different authors + so I can preserve both authors' content + +Background: articles in database + + + +Scenario: user should be admin + + + + +Scenario: admin is able to merge articles + + + + + +Scenario: user is not admin(sad path) + + + +Scenario: user should not be able to merge articles(sad path) \ No newline at end of file diff --git a/features/authors.feature b/features/authors.feature new file mode 100644 index 0000000000..d6d22eced8 --- /dev/null +++ b/features/authors.feature @@ -0,0 +1,9 @@ +Feature: When articles are merged, the merged article should have multiple authors(the authors of the original article) + + As a admin + I want to merge articles + So that merged article should have multiple authors + +Background: articles in database + +Scenario: the merged article should have multiple authors \ No newline at end of file diff --git a/features/comment.feature b/features/comment.feature new file mode 100644 index 0000000000..094afa18b5 --- /dev/null +++ b/features/comment.feature @@ -0,0 +1,9 @@ +Feature: comments on each of the two original articles need to all carry over and point to the new, merged article + + As a user + I should be able to read both articles' comments appear in the merged article + so I could read comments +Background: articles in database and comments in articles + + +Scenario: I should see both articles'comments appear in the merged article \ No newline at end of file diff --git a/features/edit.feature b/features/edit.feature new file mode 100644 index 0000000000..3526d0b205 --- /dev/null +++ b/features/edit.feature @@ -0,0 +1,9 @@ +Feature: When articles are merged, any of the authors of the original articles should be able to make edits to the merged article + + As a author + I want to edit the merged article + So that merged article is changed + +Background: articles in database + +Scenario: \ No newline at end of file diff --git a/features/permalinks.feature b/features/permalinks.feature new file mode 100644 index 0000000000..40066ba1e7 --- /dev/null +++ b/features/permalinks.feature @@ -0,0 +1,13 @@ +Feature: When mergeing two articles (and thus deleting at least one of them), the permalinks to the original articles should point to the merged article + + As a user + I should be able to go to the merged article when click on the permalinks + so I could read the merged article + +Background: articles in database + + + +Scenario: one of the articles should be deleted +Scenario: permalink of the remaining article should points to itself +Scenario: permalink of the deleted article should points to the merged article \ No newline at end of file diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb new file mode 100644 index 0000000000..4d9aab6450 --- /dev/null +++ b/features/step_definitions/web_steps.rb @@ -0,0 +1,254 @@ +# TL;DR: YOU SHOULD DELETE THIS FILE +# +# This file was generated by Cucumber-Rails and is only here to get you a head start +# These step definitions are thin wrappers around the Capybara/Webrat API that lets you +# visit pages, interact with widgets and make assertions about page content. +# +# If you use these step definitions as basis for your features you will quickly end up +# with features that are: +# +# * Hard to maintain +# * Verbose to read +# +# A much better approach is to write your own higher level step definitions, following +# the advice in the following blog posts: +# +# * http://benmabey.com/2008/05/19/imperative-vs-declarative-scenarios-in-user-stories.html +# * http://dannorth.net/2011/01/31/whose-domain-is-it-anyway/ +# * http://elabs.se/blog/15-you-re-cuking-it-wrong +# + + +require 'uri' +require 'cgi' +require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) +require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors")) + +module WithinHelpers + def with_scope(locator) + locator ? within(*selector_for(locator)) { yield } : yield + end +end +World(WithinHelpers) + +# Single-line step scoper +When /^(.*) within (.*[^:])$/ do |step, parent| + with_scope(parent) { When step } +end + +# Multi-line step scoper +When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string| + with_scope(parent) { When "#{step}:", table_or_string } +end + +Given /^(?:|I )am on (.+)$/ do |page_name| + visit path_to(page_name) +end + +When /^(?:|I )go to (.+)$/ do |page_name| + visit path_to(page_name) +end + +When /^(?:|I )press "([^"]*)"$/ do |button| + click_button(button) +end + +When /^(?:|I )follow "([^"]*)"$/ do |link| + click_link(link) +end + +When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| + fill_in(field, :with => value) +end + +When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field| + fill_in(field, :with => value) +end + +# Use this to fill in an entire form with data from a table. Example: +# +# When I fill in the following: +# | Account Number | 5002 | +# | Expiry date | 2009-11-01 | +# | Note | Nice guy | +# | Wants Email? | | +# +# TODO: Add support for checkbox, select or option +# based on naming conventions. +# +When /^(?:|I )fill in the following:$/ do |fields| + fields.rows_hash.each do |name, value| + When %{I fill in "#{name}" with "#{value}"} + end +end + +When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field| + select(value, :from => field) +end + +When /^(?:|I )check "([^"]*)"$/ do |field| + check(field) +end + +When /^(?:|I )uncheck "([^"]*)"$/ do |field| + uncheck(field) +end + +When /^(?:|I )choose "([^"]*)"$/ do |field| + choose(field) +end + +When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field| + attach_file(field, File.expand_path(path)) +end + +Then /^(?:|I )should see "([^"]*)"$/ do |text| + if page.respond_to? :should + page.should have_content(text) + else + assert page.has_content?(text) + end +end + +Then /^(?:|I )should see \/([^\/]*)\/$/ do |regexp| + regexp = Regexp.new(regexp) + + if page.respond_to? :should + page.should have_xpath('//*', :text => regexp) + else + assert page.has_xpath?('//*', :text => regexp) + end +end + +Then /^(?:|I )should not see "([^"]*)"$/ do |text| + if page.respond_to? :should + page.should have_no_content(text) + else + assert page.has_no_content?(text) + end +end + +Then /^(?:|I )should not see \/([^\/]*)\/$/ do |regexp| + regexp = Regexp.new(regexp) + + if page.respond_to? :should + page.should have_no_xpath('//*', :text => regexp) + else + assert page.has_no_xpath?('//*', :text => regexp) + end +end + +Then /^the "([^"]*)" field(?: within (.*))? should contain "([^"]*)"$/ do |field, parent, value| + with_scope(parent) do + field = find_field(field) + field_value = (field.tag_name == 'textarea') ? field.text : field.value + if field_value.respond_to? :should + field_value.should =~ /#{value}/ + else + assert_match(/#{value}/, field_value) + end + end +end + +Then /^the "([^"]*)" field(?: within (.*))? should not contain "([^"]*)"$/ do |field, parent, value| + with_scope(parent) do + field = find_field(field) + field_value = (field.tag_name == 'textarea') ? field.text : field.value + if field_value.respond_to? :should_not + field_value.should_not =~ /#{value}/ + else + assert_no_match(/#{value}/, field_value) + end + end +end + +Then /^the "([^"]*)" field should have the error "([^"]*)"$/ do |field, error_message| + element = find_field(field) + classes = element.find(:xpath, '..')[:class].split(' ') + + form_for_input = element.find(:xpath, 'ancestor::form[1]') + using_formtastic = form_for_input[:class].include?('formtastic') + error_class = using_formtastic ? 'error' : 'field_with_errors' + + if classes.respond_to? :should + classes.should include(error_class) + else + assert classes.include?(error_class) + end + + if page.respond_to?(:should) + if using_formtastic + error_paragraph = element.find(:xpath, '../*[@class="inline-errors"][1]') + error_paragraph.should have_content(error_message) + else + page.should have_content("#{field.titlecase} #{error_message}") + end + else + if using_formtastic + error_paragraph = element.find(:xpath, '../*[@class="inline-errors"][1]') + assert error_paragraph.has_content?(error_message) + else + assert page.has_content?("#{field.titlecase} #{error_message}") + end + end +end + +Then /^the "([^"]*)" field should have no error$/ do |field| + element = find_field(field) + classes = element.find(:xpath, '..')[:class].split(' ') + if classes.respond_to? :should + classes.should_not include('field_with_errors') + classes.should_not include('error') + else + assert !classes.include?('field_with_errors') + assert !classes.include?('error') + end +end + +Then /^the "([^"]*)" checkbox(?: within (.*))? should be checked$/ do |label, parent| + with_scope(parent) do + field_checked = find_field(label)['checked'] + if field_checked.respond_to? :should + field_checked.should be_true + else + assert field_checked + end + end +end + +Then /^the "([^"]*)" checkbox(?: within (.*))? should not be checked$/ do |label, parent| + with_scope(parent) do + field_checked = find_field(label)['checked'] + if field_checked.respond_to? :should + field_checked.should be_false + else + assert !field_checked + end + end +end + +Then /^(?:|I )should be on (.+)$/ do |page_name| + current_path = URI.parse(current_url).path + if current_path.respond_to? :should + current_path.should == path_to(page_name) + else + assert_equal path_to(page_name), current_path + end +end + +Then /^(?:|I )should have the following query string:$/ do |expected_pairs| + query = URI.parse(current_url).query + actual_params = query ? CGI.parse(query) : {} + expected_params = {} + expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')} + + if actual_params.respond_to? :should + actual_params.should == expected_params + else + assert_equal expected_params, actual_params + end +end + +Then /^show me the page$/ do + save_and_open_page +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000000..29f204c1ee --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,59 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +require 'cucumber/rails' + +# Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In +# order to ease the transition to Capybara we set the default here. If you'd +# prefer to use XPath just remove this line and adjust any selectors in your +# steps to use the XPath syntax. +Capybara.default_selector = :css + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { :except => [:widgets] } may not do what you expect here +# # as tCucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation + diff --git a/features/support/paths.rb b/features/support/paths.rb new file mode 100644 index 0000000000..290543c37a --- /dev/null +++ b/features/support/paths.rb @@ -0,0 +1,38 @@ +# TL;DR: YOU SHOULD DELETE THIS FILE +# +# This file is used by web_steps.rb, which you should also delete +# +# You have been warned +module NavigationHelpers + # Maps a name to a path. Used by the + # + # When /^I go to (.+)$/ do |page_name| + # + # step definition in web_steps.rb + # + def path_to(page_name) + case page_name + + when /^the home\s?page$/ + '/' + + # Add more mappings here. + # Here is an example that pulls values out of the Regexp: + # + # when /^(.*)'s profile page$/i + # user_profile_path(User.find_by_login($1)) + + else + begin + page_name =~ /^the (.*) page$/ + path_components = $1.split(/\s+/) + self.send(path_components.push('path').join('_').to_sym) + rescue NoMethodError, ArgumentError + raise "Can't find mapping from \"#{page_name}\" to a path.\n" + + "Now, go and add a mapping in #{__FILE__}" + end + end + end +end + +World(NavigationHelpers) diff --git a/features/support/selectors.rb b/features/support/selectors.rb new file mode 100644 index 0000000000..33bebc1d6b --- /dev/null +++ b/features/support/selectors.rb @@ -0,0 +1,44 @@ +# TL;DR: YOU SHOULD DELETE THIS FILE +# +# This file is used by web_steps.rb, which you should also delete +# +# You have been warned +module HtmlSelectorsHelpers + # Maps a name to a selector. Used primarily by the + # + # When /^(.+) within (.+)$/ do |step, scope| + # + # step definitions in web_steps.rb + # + def selector_for(locator) + case locator + + when "the page" + "html > body" + + # Add more mappings here. + # Here is an example that pulls values out of the Regexp: + # + # when /^the (notice|error|info) flash$/ + # ".flash.#{$1}" + + # You can also return an array to use a different selector + # type, like: + # + # when /the header/ + # [:xpath, "//header"] + + # This allows you to provide a quoted selector as the scope + # for "within" steps as was previously the default for the + # web steps: + when /^"(.+)"$/ + $1 + + else + raise "Can't find mapping from \"#{locator}\" to a selector.\n" + + "Now, go and add a mapping in #{__FILE__}" + end + end +end + +World(HtmlSelectorsHelpers) diff --git a/features/text.feature b/features/text.feature new file mode 100644 index 0000000000..72311e90e1 --- /dev/null +++ b/features/text.feature @@ -0,0 +1,9 @@ +Feature: When articles are merged, the merged article should contain the text of both previous articles + + As a admin + I want to merge articles + So that the merged article should contain the text of both previous articles + +Background: artcles in database + +Scenario: \ No newline at end of file diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..83f79471e5 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,65 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({:ok => 'db:test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({:wip => 'db:test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({:rerun => 'db:test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task :all => [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + end + desc 'Alias for cucumber:ok' + task :cucumber => 'cucumber:ok' + + task :default => :cucumber + + task :features => :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have ActiveRecord, append a no-op task that we can depend upon. + task 'db:test:prepare' do + end + + task :stats => 'cucumber:statsetup' +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/script/cucumber b/script/cucumber new file mode 100755 index 0000000000..7fa5c92086 --- /dev/null +++ b/script/cucumber @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end