diff --git a/.gitignore b/.gitignore index 4d203b225..c82fb7265 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ #* +\#*\# *~ TAGS db/schema.rb diff --git a/Gemfile b/Gemfile index e17dd3f95..d56bb6075 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,20 @@ gem 'rails', '3.1.0' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' +# for Heroku deployment - as described in Ap. A of ELLS book +group :development, :test do + gem 'sqlite3' + gem 'ruby-debug19', :require => 'ruby-debug' + gem 'cucumber-rails' + gem 'cucumber-rails-training-wheels' + gem 'database_cleaner' + gem 'capybara' + gem 'launchy' +end +group :production do +# gem 'pg' +end + # Gems used only for assets and not required # in production environments by default. group :assets do @@ -22,13 +36,5 @@ gem 'jquery-rails' # Deploy with Capistrano # gem 'capistrano' -group :development, :test do - gem 'sqlite3' - gem 'ruby-debug19' -end - -group :production do - gem 'pg' -end - +# To use debugger gem 'haml' diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index 9aea345d0..eb5576fe2 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -30,9 +30,27 @@ def new end def create - debugger @movie = Movie.create!(params[:movie]) flash[:notice] = "#{@movie.title} was successfully created." + redirect_to movies_path + end + + def edit + @movie = Movie.find params[:id] + end + + def update + @movie = Movie.find params[:id] + @movie.update_attributes!(params[:movie]) + flash[:notice] = "#{@movie.title} was successfully updated." + redirect_to movie_path(@movie) + end + + def destroy + @movie = Movie.find(params[:id]) + @movie.destroy + flash[:notice] = "Movie '#{@movie.title}' deleted." + redirect_to movies_path end end diff --git a/app/models/.gitkeep b/app/models/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/config/cucumber.yml b/config/cucumber.yml index 621a14cea..19b288df9 100644 --- a/config/cucumber.yml +++ b/config/cucumber.yml @@ -1,7 +1,7 @@ <% 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'] || 'progress'} --strict --tags ~@wip" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" %> default: <%= std_opts %> features wip: --tags @wip:3 --wip features diff --git a/config/database.yml b/config/database.yml index 51a4dd459..699867ec9 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,7 +12,7 @@ development: # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. -test: +test: &test adapter: sqlite3 database: db/test.sqlite3 pool: 5 @@ -23,3 +23,6 @@ production: database: db/production.sqlite3 pool: 5 timeout: 5000 + +cucumber: + <<: *test diff --git a/config/routes.rb b/config/routes.rb index d5a5828f6..18a27b40d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,7 @@ # Sample resource route (maps HTTP verbs to controller actions automatically): # resources :products resources :movies - + # Sample resource route with options: # resources :products do # member do diff --git a/db/migrate/20120130161449_add_more_movies.rb b/db/migrate/20120130161449_add_more_movies.rb new file mode 100644 index 000000000..c1fdac9af --- /dev/null +++ b/db/migrate/20120130161449_add_more_movies.rb @@ -0,0 +1,25 @@ +class AddMoreMovies < ActiveRecord::Migration + MORE_MOVIES = [ + {:title => 'Aladdin', :rating => 'G', :release_date => '25-Nov-1992'}, + {:title => 'The Terminator', :rating => 'R', :release_date => '26-Oct-1984'}, + {:title => 'When Harry Met Sally', :rating => 'R', :release_date => '21-Jul-1989'}, + {:title => 'The Help', :rating => 'PG-13', :release_date => '10-Aug-2011'}, + {:title => 'Chocolat', :rating => 'PG-13', :release_date => '5-Jan-2001'}, + {:title => 'Amelie', :rating => 'R', :release_date => '25-Apr-2001'}, + {:title => '2001: A Space Odyssey', :rating => 'G', :release_date => '6-Apr-1968'}, + {:title => 'The Incredibles', :rating => 'PG', :release_date => '5-Nov-2004'}, + {:title => 'Raiders of the Lost Ark', :rating => 'PG', :release_date => '12-Jun-1981'}, + {:title => 'Chicken Run', :rating => 'G', :release_date => '21-Jun-2000'}, + ] + def up + MORE_MOVIES.each do |movie| + Movie.create!(movie) + end + end + + def down + MORE_MOVIES.each do |movie| + Movie.find_by_title_and_rating(movie[:title], movie[:rating]).destroy + end + end +end diff --git a/features/filter_movie_list.feature b/features/filter_movie_list.feature new file mode 100644 index 000000000..0e157f872 --- /dev/null +++ b/features/filter_movie_list.feature @@ -0,0 +1,35 @@ +Feature: display list of movies filtered by MPAA rating + + As a concerned parent + So that I can quickly browse movies appropriate for my family + I want to see movies matching only certain MPAA ratings + +Background: movies have been added to database + + Given the following movies exist: + | title | rating | release_date | + | Aladdin | G | 25-Nov-1992 | + | The Terminator | R | 26-Oct-1984 | + | When Harry Met Sally | R | 21-Jul-1989 | + | The Help | PG-13 | 10-Aug-2011 | + | Chocolat | PG-13 | 5-Jan-2001 | + | Amelie | R | 25-Apr-2001 | + | 2001: A Space Odyssey | G | 6-Apr-1968 | + | The Incredibles | PG | 5-Nov-2004 | + | Raiders of the Lost Ark | PG | 12-Jun-1981 | + | Chicken Run | G | 21-Jun-2000 | + + And I am on the RottenPotatoes home page + +Scenario: restrict to movies with 'PG' or 'R' ratings + # enter step(s) to check the 'PG' and 'R' checkboxes + # enter step(s) to uncheck all other checkboxes + # enter step to "submit" the search form on the homepage + # enter step(s) to ensure that PG and R movies are visible + # enter step(s) to ensure that other movies are not visible + +Scenario: no checkboxes selected + # see assignment + +Scenario: all checkboxes selected + # see assignment diff --git a/features/sort_movie_list.feature b/features/sort_movie_list.feature new file mode 100644 index 000000000..4018dc0a4 --- /dev/null +++ b/features/sort_movie_list.feature @@ -0,0 +1,29 @@ +Feature: display list of movies sorted by different criteria + + As an avid moviegoer + So that I can quickly browse movies based on my preferences + I want to see movies sorted by title or release date + +Background: movies have been added to database + + Given the following movies exist: + | title | rating | release_date | + | Aladdin | G | 25-Nov-1992 | + | The Terminator | R | 26-Oct-1984 | + | When Harry Met Sally | R | 21-Jul-1989 | + | The Help | PG-13 | 10-Aug-2011 | + | Chocolat | PG-13 | 5-Jan-2001 | + | Amelie | R | 25-Apr-2001 | + | 2001: A Space Odyssey | G | 6-Apr-1968 | + | The Incredibles | PG | 5-Nov-2004 | + | Raiders of the Lost Ark | PG | 12-Jun-1981 | + | Chicken Run | G | 21-Jun-2000 | + + And I am on the RottenPotatoes home page + +Feature: sort movies alphabetically + # your steps here + +Feature: sort movies in increasing order of release date + # your steps here + diff --git a/features/step_definitions/movie_steps.rb b/features/step_definitions/movie_steps.rb new file mode 100644 index 000000000..111b4f175 --- /dev/null +++ b/features/step_definitions/movie_steps.rb @@ -0,0 +1,26 @@ +# Add a declarative step here for populating the DB with movies. + +Given /the following movies exist/ do |movies_table| + movies_table.hashes.each do |movie| + # each returned element will be a hash whose key is the table header. + # you should arrange to add that movie to the database here. + end +end + +# Make sure that one string (regexp) occurs before or after another one +# on the same page + +Then /I should see "(.*)" before "(.*)"/ do |e1, e2| + # ensure that that e1 occurs before e2. + # page.content is the entire content of the page as a string. +end + +# Make it easier to express checking or unchecking several boxes at once +# "When I uncheck the following ratings: PG, G, R" +# "When I check the following ratings: G" + +When /I (un)?check the following ratings: (.*)/ do |uncheck, rating_list| + # HINT: use String#split to split up the rating_list, then + # iterate over the ratings and reuse the "When I check..." or + # "When I uncheck..." steps in lines 89-95 of web_steps.rb +end diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb new file mode 100644 index 000000000..4d9aab645 --- /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 000000000..b2cf67330 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,56 @@ +# 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 +# DatabaseCleaner.strategy = :truncation, {:except => %w[widgets]} +# 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 000000000..290543c37 --- /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 000000000..33bebc1d6 --- /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/lib/assets/.gitkeep b/lib/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake index 7db1a5570..83f79471e 100644 --- a/lib/tasks/cucumber.rake +++ b/lib/tasks/cucumber.rake @@ -34,6 +34,12 @@ begin 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' @@ -43,6 +49,12 @@ begin 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 diff --git a/vendor/assets/stylesheets/.gitkeep b/vendor/assets/stylesheets/.gitkeep new file mode 100644 index 000000000..e69de29bb