From 38e37e5e586d7222ec4662c5e9c64916f7e482dc Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Fri, 21 Jun 2019 12:47:50 +0300 Subject: [PATCH 01/19] Add 'selenium-webdriver' and 'webdrivers' gems. Update spec/rails_helper.rb. --- Gemfile | 2 ++ Gemfile.lock | 14 +++++++++++++- spec/rails_helper.rb | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index ec14f1a..cb882a6 100644 --- a/Gemfile +++ b/Gemfile @@ -61,7 +61,9 @@ end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' + gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome + gem 'webdrivers', '~> 4.0' gem 'shoulda-matchers' gem 'rails-controller-testing' gem 'launchy' diff --git a/Gemfile.lock b/Gemfile.lock index cddc5b8..7195415 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -59,6 +59,8 @@ GEM rack-test (>= 0.6.3) regexp_parser (~> 1.5) xpath (~> 3.2) + childprocess (1.0.1) + rake (< 13.0) coffee-rails (4.2.2) coffee-script (>= 2.2.0) railties (>= 4.0.0) @@ -107,7 +109,7 @@ GEM mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.10) + msgpack (1.3.0) nio4r (2.3.1) nokogiri (1.10.3) mini_portile2 (~> 2.4.0) @@ -172,6 +174,7 @@ GEM rspec-support (~> 3.8.0) rspec-support (3.8.2) ruby_dep (1.5.0) + rubyzip (1.2.3) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -183,6 +186,9 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + selenium-webdriver (3.142.3) + childprocess (>= 0.5, < 2.0) + rubyzip (~> 1.2, >= 1.2.2) shoulda-matchers (4.1.0) activesupport (>= 4.2.0) slim (4.0.1) @@ -221,6 +227,10 @@ GEM activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) + webdrivers (4.0.1) + nokogiri (~> 1.6) + rubyzip (~> 1.0) + selenium-webdriver (>= 3.0, < 4.0) websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.4) @@ -246,6 +256,7 @@ DEPENDENCIES rails-controller-testing rspec-rails (~> 3.8) sass-rails (~> 5.0) + selenium-webdriver shoulda-matchers slim-rails spring @@ -254,6 +265,7 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) web-console (>= 3.3.0) + webdrivers (~> 4.0) RUBY VERSION ruby 2.6.3p62 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 3555d8d..a56e492 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -36,6 +36,8 @@ config.include ControllerHelpers, type: :controller config.include FeatureHelpers, type: :feature + Capybara.javascript_driver = :selenium_chrome_headless + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" From 38903afdaee3b624728253fe455d7debe53afed7 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Fri, 21 Jun 2019 13:14:43 +0300 Subject: [PATCH 02/19] Add 'jquery-rails' gem. Update JS manifest. --- Gemfile | 2 ++ Gemfile.lock | 5 +++++ app/assets/javascripts/application.js | 1 + 3 files changed, 8 insertions(+) diff --git a/Gemfile b/Gemfile index cb882a6..259c4f2 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,8 @@ gem 'uglifier', '>= 1.3.0' # gem 'mini_racer', platforms: :ruby # Devise gem 'devise' +# JS +gem 'jquery-rails' # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.2' diff --git a/Gemfile.lock b/Gemfile.lock index 7195415..b398e95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,6 +91,10 @@ GEM concurrent-ruby (~> 1.0) jbuilder (2.9.1) activesupport (>= 4.2.0) + jquery-rails (4.3.5) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) launchy (2.4.3) addressable (~> 2.3) listen (3.1.5) @@ -248,6 +252,7 @@ DEPENDENCIES devise factory_bot_rails jbuilder (~> 2.5) + jquery-rails launchy listen (>= 3.0.5, < 3.2) pg (>= 0.18, < 2.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 82e6f0f..a55d271 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,5 @@ //= require rails-ujs //= require activestorage //= require turbolinks +//= require jquery3 //= require_tree . From 5177213d94bd396691942e75af18fe151ea080df Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Sat, 22 Jun 2019 15:18:57 +0300 Subject: [PATCH 03/19] Refactor code. --- app/controllers/answers_controller.rb | 8 +------- app/models/answer.rb | 4 ++++ app/views/answers/_answer.html.slim | 6 ++++-- app/views/answers/create.js.erb | 5 +++++ app/views/layouts/application.html.erb | 4 +--- app/views/questions/show.html.slim | 11 ++++++----- app/views/shared/_user_nav.html.slim | 8 +++++++- spec/controllers/answers_controller_spec.rb | 18 ++++++------------ spec/features/answer/create_spec.rb | 8 +++----- spec/features/sign_in_spec.rb | 4 ++-- spec/features/sign_up_spec.rb | 6 +++--- spec/models/user_spec.rb | 2 +- spec/support/feature_helpers.rb | 2 +- 13 files changed, 44 insertions(+), 42 deletions(-) create mode 100644 app/views/answers/create.js.erb diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 73afc65..fed20ab 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -9,13 +9,7 @@ def create @question = Question.find(params[:question_id]) @answer = @question.answers.new(answer_params) @answer.user = current_user - - if @answer.save - redirect_to @question, notice: "Your answer was saved." - else - flash[:alert] = "Your answer was not saved." - render "questions/show" - end + @answer.save end def destroy diff --git a/app/models/answer.rb b/app/models/answer.rb index 3fb76d6..b5cb84f 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -4,4 +4,8 @@ class Answer < ApplicationRecord validates :body, presence: true, length: { minimum: 50 } + + def saved? + !!self.id + end end diff --git a/app/views/answers/_answer.html.slim b/app/views/answers/_answer.html.slim index 7455e6f..154c8aa 100644 --- a/app/views/answers/_answer.html.slim +++ b/app/views/answers/_answer.html.slim @@ -1,2 +1,4 @@ -p= answer.body -p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) if current_user&.owned?(answer) +- if answer.persisted? + p= answer.body +- if answer.persisted? && current_user&.owned?(answer) + p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) diff --git a/app/views/answers/create.js.erb b/app/views/answers/create.js.erb new file mode 100644 index 0000000..8ced58f --- /dev/null +++ b/app/views/answers/create.js.erb @@ -0,0 +1,5 @@ +$('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') +<% if @answer.persisted? %> + $('.answers').append('<%= j render @answer %>') + $('#answer_body').val('') +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e536b96..d6f50f6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -10,9 +10,7 @@ -

- <%= render 'shared/user_nav' %> -

+ <%= render 'shared/user_nav' %>

<%= notice %>

<%= alert %>

<%= yield %> diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index 4bc1b39..1ccd938 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -1,12 +1,13 @@ -= render 'shared/errors', resource: @answer - =render(partial: 'shared/manage_question_links', locals: { question: @question }) if current_user&.owned?(@question) h4= @question.title p= @question.body -p Answers -p= render @question.reload.answers +h4 Answers +div.answers + = render @question.answers p Your answer -= form_with model: @answer, url: question_answers_path(@question), local: true do |f| +.answer-errors + = render 'shared/errors', resource: @answer += form_with model: @answer, url: question_answers_path(@question) do |f| = f.label :body = f.text_area :body = f.submit 'Leave' diff --git a/app/views/shared/_user_nav.html.slim b/app/views/shared/_user_nav.html.slim index 0ff5995..074341a 100644 --- a/app/views/shared/_user_nav.html.slim +++ b/app/views/shared/_user_nav.html.slim @@ -1 +1,7 @@ -p= link_to 'Log out', destroy_user_session_path, method: :delete +- if current_user + p= link_to 'Log out', destroy_user_session_path, method: :delete +- else + p + = link_to 'Log in', new_user_session_path + | |  + = link_to 'Sign up', new_user_registration_path diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index 46934e8..6d604aa 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -23,18 +23,12 @@ context 'with valid attributes' do it 'saves a new answer in the database' do - expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer) } }.to change(question.answers, :count).by(1) - end - - it 'redirects to question show view' do - post :create, params: { question_id: question.id, answer: attributes_for(:answer) } - - expect(response).to redirect_to question + expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer), format: :js } }.to change(question.answers, :count).by(1) end it 'creates answer by the name of logged user' do new_answer_params = attributes_for(:answer) - post :create, params: { question_id: question.id, answer: new_answer_params } + post :create, params: { question_id: question.id, answer: new_answer_params, format: :js } created_answer = question.answers.find_by! new_answer_params expect(created_answer.user).to eq user @@ -43,12 +37,12 @@ context 'with invalid attributes' do it 'does not save a new answer with short body in the database' do - expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid) } }.to_not change(Answer, :count) + expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } }.to_not change(Answer, :count) end - it 'renders question show view' do - post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid) } - expect(response).to render_template 'questions/show' + it 'renders Answer create template' do + post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } + expect(response).to render_template 'create' end end end diff --git a/spec/features/answer/create_spec.rb b/spec/features/answer/create_spec.rb index edb3d74..f95ba28 100644 --- a/spec/features/answer/create_spec.rb +++ b/spec/features/answer/create_spec.rb @@ -8,23 +8,21 @@ given(:user) { create(:user) } given(:question) { create(:question, user: user) } - before do + background do login(user) visit question_path(question) end - scenario 'answers to a question' do + scenario 'answers to a question', js: true do fill_in 'Body', with: "#{"body" * 25}" click_on 'Leave' - expect(page).to have_content 'Your answer was saved.' expect(page).to have_content "#{"body" * 25}" end - scenario 'answers to a question with errors' do + scenario 'answers to a question with errors', js: true do click_on 'Leave' - expect(page).to have_content 'Your answer was not saved.' expect(page).to have_content "Body can't be blank" expect(page).to have_content 'Body is too short (minimum is 50 characters)' end diff --git a/spec/features/sign_in_spec.rb b/spec/features/sign_in_spec.rb index 4c9a6e7..f2d4a42 100644 --- a/spec/features/sign_in_spec.rb +++ b/spec/features/sign_in_spec.rb @@ -12,7 +12,7 @@ scenario 'registered user tries to sign in' do fill_in 'Email', with: user.email fill_in 'Password', with: user.password - click_on 'Log in' + click_button 'Log in' expect(page).to have_content 'Signed in successfully.' end @@ -20,7 +20,7 @@ scenario 'unregistered user tries to sign in' do fill_in 'Email', with: 'wrong@test.com' fill_in 'Password', with: '12345678' - click_on 'Log in' + click_button 'Log in' expect(page).to have_content 'Invalid Email or password.' end diff --git a/spec/features/sign_up_spec.rb b/spec/features/sign_up_spec.rb index fac997d..4470f2a 100644 --- a/spec/features/sign_up_spec.rb +++ b/spec/features/sign_up_spec.rb @@ -11,7 +11,7 @@ fill_in 'Email', with: 'user@test.com' fill_in 'Password', with: '12345678' fill_in 'Password confirmation', with: '12345678' - click_on 'Sign up' + click_button 'Sign up' expect(page).to have_content 'Welcome! You have signed up successfully.' end @@ -20,7 +20,7 @@ fill_in 'Email', with: 'user@test.com' fill_in 'Password', with: '12345678' fill_in 'Password confirmation', with: '87654321' - click_on 'Sign up' + click_button 'Sign up' expect(page).to have_content "Password confirmation doesn't match" end @@ -29,7 +29,7 @@ fill_in 'Email', with: 'user_at_test.com' fill_in 'Password', with: '12345678' fill_in 'Password confirmation', with: '12345678' - click_on 'Sign up' + click_button 'Sign up' expect(page).to have_content "Email is invalid" end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ac58464..e992a2b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -17,7 +17,7 @@ expect(user1).to be_owned(question1) end - it 'check if user is owner of resource created by himself' do + it 'check if user is not owner of resource created by another user' do expect(user1).to_not be_owned(question2) end end diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index d5d93ec..7319b74 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -3,6 +3,6 @@ def login(user) visit new_user_session_path fill_in 'Email', with: user.email fill_in 'Password', with: user.password - click_on 'Log in' + click_button 'Log in' end end From 36850d9afbb06c2998523cfcbdf655f2ab1be9a2 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Sun, 23 Jun 2019 18:15:06 +0300 Subject: [PATCH 04/19] Implement scenario 'authenticated user edits his answer'. --- app/assets/javascripts/answers.coffee | 3 -- app/assets/javascripts/answers.js | 8 ++++ app/assets/stylesheets/common.scss | 3 ++ app/controllers/answers_controller.rb | 6 +++ app/views/answers/_answer.html.slim | 6 +++ app/views/answers/create.js.erb | 2 +- app/views/answers/update.js.erb | 4 ++ app/views/questions/show.html.slim | 4 +- .../shared/_manage_answer_links.html.slim | 5 ++- spec/controllers/answers_controller_spec.rb | 43 ++++++++++++++++++- spec/features/answer/destroy_spec.rb | 9 ++-- spec/features/answer/edit_spec.rb | 41 ++++++++++++++++++ spec/features/question/show_spec.rb | 2 +- 13 files changed, 123 insertions(+), 13 deletions(-) delete mode 100644 app/assets/javascripts/answers.coffee create mode 100644 app/assets/javascripts/answers.js create mode 100644 app/assets/stylesheets/common.scss create mode 100644 app/views/answers/update.js.erb create mode 100644 spec/features/answer/edit_spec.rb diff --git a/app/assets/javascripts/answers.coffee b/app/assets/javascripts/answers.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/answers.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/answers.js b/app/assets/javascripts/answers.js new file mode 100644 index 0000000..936f8ac --- /dev/null +++ b/app/assets/javascripts/answers.js @@ -0,0 +1,8 @@ +$(document).on('turbolinks:load', function() { + $('.answers').on('click', '.edit-answer-links', function(e) { + e.preventDefault() + $(this).hide() + var answerId = $(this).data('answerId') + $('form#edit-answer-' + answerId).show() + }) +}) diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss new file mode 100644 index 0000000..cb57e72 --- /dev/null +++ b/app/assets/stylesheets/common.scss @@ -0,0 +1,3 @@ +.hidden { + display: none +} diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index fed20ab..2d0f707 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -12,6 +12,12 @@ def create @answer.save end + def update + @answer = Answer.find(params[:id]) + @answer.update(answer_params) + @question = @answer.question + end + def destroy answer = Answer.find(params[:id]) return head :forbidden unless current_user.owned?(answer) diff --git a/app/views/answers/_answer.html.slim b/app/views/answers/_answer.html.slim index 154c8aa..e77dea8 100644 --- a/app/views/answers/_answer.html.slim +++ b/app/views/answers/_answer.html.slim @@ -1,4 +1,10 @@ - if answer.persisted? p= answer.body + - if answer.persisted? && current_user&.owned?(answer) p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) + += form_with model: answer, class: 'hidden', html: { id: "edit-answer-#{answer.id}" } do |f| + = f.label :body, 'Your answer' + = f.text_area :body + = f.submit 'Update' diff --git a/app/views/answers/create.js.erb b/app/views/answers/create.js.erb index 8ced58f..f075720 100644 --- a/app/views/answers/create.js.erb +++ b/app/views/answers/create.js.erb @@ -1,5 +1,5 @@ $('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') <% if @answer.persisted? %> $('.answers').append('<%= j render @answer %>') - $('#answer_body').val('') + $('.new-answer #answer_body').val('') <% end %> diff --git a/app/views/answers/update.js.erb b/app/views/answers/update.js.erb new file mode 100644 index 0000000..d451576 --- /dev/null +++ b/app/views/answers/update.js.erb @@ -0,0 +1,4 @@ +$('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') +<% if !@answer.errors.present? %> + $('.answers').html('<%= j render @question.answers.order(:created_at) %>') +<% end %> diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index 1ccd938..20b4485 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -3,11 +3,11 @@ h4= @question.title p= @question.body h4 Answers div.answers - = render @question.answers + = render @question.answers.order(:created_at) p Your answer .answer-errors = render 'shared/errors', resource: @answer -= form_with model: @answer, url: question_answers_path(@question) do |f| += form_with model: [@question, @answer], class: 'new-answer' do |f| = f.label :body = f.text_area :body = f.submit 'Leave' diff --git a/app/views/shared/_manage_answer_links.html.slim b/app/views/shared/_manage_answer_links.html.slim index d5bef88..24a1a8a 100644 --- a/app/views/shared/_manage_answer_links.html.slim +++ b/app/views/shared/_manage_answer_links.html.slim @@ -1 +1,4 @@ -p= link_to 'Delete the answer', answer_path(answer), method: :delete +p + = link_to 'Edit', '#', class: 'edit-answer-links', data: { answer_id: answer.id } + |  + = link_to 'Delete', answer_path(answer), method: :delete diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index 6d604aa..b1bcdb2 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -40,7 +40,7 @@ expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } }.to_not change(Answer, :count) end - it 'renders Answer create template' do + it 'renders create template' do post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } expect(response).to render_template 'create' end @@ -48,7 +48,6 @@ end describe 'DELETE #destroy' do - let(:question) { create(:question, user: user) } let!(:answer) { create(:answer, question: question, user: user) } let(:user2) { create(:user) } let!(:answer2) { create(:answer, question: question, user: user2) } @@ -72,4 +71,44 @@ expect { delete :destroy, params: { id: answer2 } }.to_not change(Answer, :count) end end + + describe 'PATCH #update' do + let!(:answer) { create(:answer, question: question, user: user) } + let!(:new_body) { "New answer's body #{"a" * 32}" } + + before { login(user) } + + context 'with valid attributes' do + it 'updates an answer' do + patch :update, params: { id: answer, answer: { body: new_body } }, format: :js + answer.reload + + expect(answer.body).to eq new_body + end + + it 'renders update view' do + patch :update, params: { id: answer, answer: { body: new_body } }, format: :js + + expect(response).to render_template :update + end + end + + context 'with invalid attributes' do + it 'does not update an answer' do + expect { + patch :update, + params: { id: answer, answer: attributes_for(:answer, :invalid) }, + format: :js + }.to_not change(answer, :body) + end + + it 'renders update view' do + patch :update, + params: { id: answer, answer: attributes_for(:answer, :invalid) }, + format: :js + + expect(response).to render_template :update + end + end + end end diff --git a/spec/features/answer/destroy_spec.rb b/spec/features/answer/destroy_spec.rb index dcf28c0..bae2a6c 100644 --- a/spec/features/answer/destroy_spec.rb +++ b/spec/features/answer/destroy_spec.rb @@ -17,10 +17,11 @@ answer = create(:answer, question: question, user: user1) visit question_path(question) - click_on 'Delete the answer' + within '.answers' do + click_on 'Delete' + end expect(page).to have_content "Your answer was successfully deleted!" - expect { answer.reload }.to raise_error ActiveRecord::RecordNotFound end scenario "user tries to delete another's answer" do @@ -28,6 +29,8 @@ visit question_path(question) - expect(page).to_not have_link 'Delete the answer' + within '.answers' do + expect(page).to_not have_link 'Delete' + end end end diff --git a/spec/features/answer/edit_spec.rb b/spec/features/answer/edit_spec.rb new file mode 100644 index 0000000..3237bb9 --- /dev/null +++ b/spec/features/answer/edit_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +feature 'user can edit his answer', %q{ + in order to correct mistakes + as an author of an answer + i'd like to be able to edit my answer +} do + given(:user) { create(:user) } + given(:question) { create(:question, user: user) } + given!(:answer) { create(:answer, question: question, user: user) } + + scenario 'unauthenticated user can not edit answers' do + visit question_path(question) + + expect(page).to_not have_link "Edit the answer" + end + + describe 'authenticated user' do + background do + login(user) + visit question_path(question) + end + + given(:new_body) { "New answer's body #{"a" * 32}" } + + scenario 'edits his answer', js: true do + within '.answers' do + click_on 'Edit' + fill_in 'Body', with: new_body + click_on 'Update' + + expect(page).to_not have_content answer.body + expect(page).to have_content new_body + expect(page).to_not have_selector 'textarea' + end + end + + scenario 'tries to edit his answer with errors' + scenario "tries to edit another user's answer" + end +end diff --git a/spec/features/question/show_spec.rb b/spec/features/question/show_spec.rb index aaeb67c..04d6df3 100644 --- a/spec/features/question/show_spec.rb +++ b/spec/features/question/show_spec.rb @@ -12,7 +12,7 @@ given(:question) { create(:question, user: user) } given!(:answers) { create_list(:answer, n, question: question) } - scenario 'unauthenticated user sees a question and answers' do + scenario 'unauthenticated user sees a question and answers', js: true do visit question_path(question) expect(page).to have_content question.title From 13bffa388296bb9fd6f536fd203ff3dd1c1bbcae Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Sun, 23 Jun 2019 20:09:40 +0300 Subject: [PATCH 05/19] Implement scenarios 'authenticated user tries to edit his answer with errors' and 'authenticated user tries to edit another user's answer'. --- app/models/answer.rb | 4 +--- app/views/answers/update.js.erb | 2 +- app/views/questions/show.html.slim | 2 +- spec/features/answer/edit_spec.rb | 32 ++++++++++++++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/models/answer.rb b/app/models/answer.rb index b5cb84f..37837dd 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -5,7 +5,5 @@ class Answer < ApplicationRecord validates :body, presence: true, length: { minimum: 50 } - def saved? - !!self.id - end + default_scope { order :created_at } end diff --git a/app/views/answers/update.js.erb b/app/views/answers/update.js.erb index d451576..8adbc24 100644 --- a/app/views/answers/update.js.erb +++ b/app/views/answers/update.js.erb @@ -1,4 +1,4 @@ $('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') <% if !@answer.errors.present? %> - $('.answers').html('<%= j render @question.answers.order(:created_at) %>') + $('.answers').html('<%= j render @question.answers %>') <% end %> diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index 20b4485..1187b86 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -3,7 +3,7 @@ h4= @question.title p= @question.body h4 Answers div.answers - = render @question.answers.order(:created_at) + = render @question.answers p Your answer .answer-errors = render 'shared/errors', resource: @answer diff --git a/spec/features/answer/edit_spec.rb b/spec/features/answer/edit_spec.rb index 3237bb9..fd5a12b 100644 --- a/spec/features/answer/edit_spec.rb +++ b/spec/features/answer/edit_spec.rb @@ -6,13 +6,14 @@ i'd like to be able to edit my answer } do given(:user) { create(:user) } + given(:user2) { create(:user) } given(:question) { create(:question, user: user) } given!(:answer) { create(:answer, question: question, user: user) } scenario 'unauthenticated user can not edit answers' do visit question_path(question) - expect(page).to_not have_link "Edit the answer" + expect(page).to_not have_link "Edit" end describe 'authenticated user' do @@ -21,21 +22,40 @@ visit question_path(question) end - given(:new_body) { "New answer's body #{"a" * 32}" } + given(:new_valid_body) { "New answer's body #{"a" * 32}" } + given(:new_invalid_body) { "New answer's invalid body" } scenario 'edits his answer', js: true do within '.answers' do click_on 'Edit' - fill_in 'Body', with: new_body + fill_in 'answer_body', with: new_valid_body click_on 'Update' expect(page).to_not have_content answer.body - expect(page).to have_content new_body + expect(page).to have_content new_valid_body expect(page).to_not have_selector 'textarea' end end - scenario 'tries to edit his answer with errors' - scenario "tries to edit another user's answer" + scenario 'tries to edit his answer with errors', js: true do + within '.answers' do + click_on 'Edit' + fill_in 'answer_body', with: new_invalid_body + click_on 'Update' + + expect(page).to have_content answer.body + end + expect(page).to have_content 'Body is too short (minimum is 50 characters)' + end + + scenario "tries to edit another user's answer" do + click_on 'Log out' + login(user2) + visit question_path(question) + + within '.answers' do + expect(page).to_not have_link 'Edit' + end + end end end From bd897ae9043813b6e1ee7cd2e846a681dc2989ca Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Sun, 23 Jun 2019 22:24:36 +0300 Subject: [PATCH 06/19] Update Answer partial and Update view. --- app/views/answers/_answer.html.slim | 17 +++++++++-------- app/views/answers/update.js.erb | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/answers/_answer.html.slim b/app/views/answers/_answer.html.slim index e77dea8..07cb7ed 100644 --- a/app/views/answers/_answer.html.slim +++ b/app/views/answers/_answer.html.slim @@ -1,10 +1,11 @@ -- if answer.persisted? - p= answer.body +div id="answer_#{answer.id}" + - if answer.persisted? + p= answer.body -- if answer.persisted? && current_user&.owned?(answer) - p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) + - if answer.persisted? && current_user&.owned?(answer) + p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) -= form_with model: answer, class: 'hidden', html: { id: "edit-answer-#{answer.id}" } do |f| - = f.label :body, 'Your answer' - = f.text_area :body - = f.submit 'Update' + = form_with model: answer, class: 'hidden', html: { id: "edit-answer-#{answer.id}" } do |f| + = f.label :body, 'Your answer' + = f.text_area :body + = f.submit 'Update' diff --git a/app/views/answers/update.js.erb b/app/views/answers/update.js.erb index 8adbc24..b0bdda4 100644 --- a/app/views/answers/update.js.erb +++ b/app/views/answers/update.js.erb @@ -1,4 +1,4 @@ $('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') <% if !@answer.errors.present? %> - $('.answers').html('<%= j render @question.answers %>') + $("#answer_" + <%= @answer.id %>).replaceWith('<%= j render @answer %>') <% end %> From 37c3263aed4ecc6e6770f0e0a5763e16fc730dec Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 10:42:46 +0300 Subject: [PATCH 07/19] Implement scenario 'authenticated user edits his question'. --- app/assets/javascripts/questions.coffee | 3 -- app/assets/javascripts/questions.js | 8 ++++ app/controllers/questions_controller.rb | 6 +-- .../questions/_question_for_show.html.slim | 9 ++++ app/views/questions/show.html.slim | 7 ++- app/views/questions/update.js.erb | 4 ++ .../shared/_manage_question_links.html.slim | 5 +- spec/features/answer/edit_spec.rb | 15 +++--- spec/features/question/destroy_spec.rb | 8 +++- spec/features/question/edit_spec.rb | 47 +++++++++++++++++++ spec/support/wait_for_ajax.rb | 15 ++++++ 11 files changed, 103 insertions(+), 24 deletions(-) delete mode 100644 app/assets/javascripts/questions.coffee create mode 100644 app/assets/javascripts/questions.js create mode 100644 app/views/questions/_question_for_show.html.slim create mode 100644 app/views/questions/update.js.erb create mode 100644 spec/features/question/edit_spec.rb create mode 100644 spec/support/wait_for_ajax.rb diff --git a/app/assets/javascripts/questions.coffee b/app/assets/javascripts/questions.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/questions.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/questions.js b/app/assets/javascripts/questions.js new file mode 100644 index 0000000..44b3cdb --- /dev/null +++ b/app/assets/javascripts/questions.js @@ -0,0 +1,8 @@ +$(document).on('turbolinks:load', function() { + $('.edit-question-link').on('click', function(e) { + e.preventDefault() + $(this).hide() + var questionId = $(this).data('questionId') + $('form#edit-question-' + questionId).show() + }) +}) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index c9b1a94..39b4314 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -29,11 +29,7 @@ def create end def update - if @question.update(question_params) - redirect_to @question - else - render :edit - end + @question.update(question_params) end def destroy diff --git a/app/views/questions/_question_for_show.html.slim b/app/views/questions/_question_for_show.html.slim new file mode 100644 index 0000000..bf32d56 --- /dev/null +++ b/app/views/questions/_question_for_show.html.slim @@ -0,0 +1,9 @@ +h4= @question.title +p= @question.body +=render(partial: 'shared/manage_question_links', locals: { question: @question }) if current_user&.owned?(@question) +.question-errors + =render 'shared/errors', resource: @question += form_with model: @question, class: 'hidden', html: { id: "edit-question-#{@question.id}" } do |f| + = f.label :body, 'Your question' + = f.text_area :body + = f.submit 'Update' diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index 1187b86..a5dd6af 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -1,8 +1,7 @@ -=render(partial: 'shared/manage_question_links', locals: { question: @question }) if current_user&.owned?(@question) -h4= @question.title -p= @question.body -h4 Answers +div id="question_#{@question.id}" + = render 'question_for_show', resource: @question div.answers + h4 Answers = render @question.answers p Your answer .answer-errors diff --git a/app/views/questions/update.js.erb b/app/views/questions/update.js.erb new file mode 100644 index 0000000..02dec75 --- /dev/null +++ b/app/views/questions/update.js.erb @@ -0,0 +1,4 @@ +$('.question-errors').html('<%= render 'shared/errors', resource: @question %>') +<% if !@question.errors.present? %> + $("#question_" + <%= @question.id %>).replaceWith('<%= j render 'question_for_show', resource: @question %>') +<% end %> diff --git a/app/views/shared/_manage_question_links.html.slim b/app/views/shared/_manage_question_links.html.slim index 45d5e31..1ceb9ff 100644 --- a/app/views/shared/_manage_question_links.html.slim +++ b/app/views/shared/_manage_question_links.html.slim @@ -1 +1,4 @@ -p= link_to 'Delete the question', question_path(question), method: :delete +p + = link_to 'Edit', '#', class: 'edit-question-link', data: { question_id: question.id } + |  + = link_to 'Delete', question_path(question), method: :delete diff --git a/spec/features/answer/edit_spec.rb b/spec/features/answer/edit_spec.rb index fd5a12b..2a350ce 100644 --- a/spec/features/answer/edit_spec.rb +++ b/spec/features/answer/edit_spec.rb @@ -9,6 +9,7 @@ given(:user2) { create(:user) } given(:question) { create(:question, user: user) } given!(:answer) { create(:answer, question: question, user: user) } + given!(:answer2) { create(:answer, question: question, user: user2) } scenario 'unauthenticated user can not edit answers' do visit question_path(question) @@ -26,9 +27,9 @@ given(:new_invalid_body) { "New answer's invalid body" } scenario 'edits his answer', js: true do - within '.answers' do + within "#answer_#{answer.id}" do click_on 'Edit' - fill_in 'answer_body', with: new_valid_body + fill_in 'body', with: new_valid_body click_on 'Update' expect(page).to_not have_content answer.body @@ -38,9 +39,9 @@ end scenario 'tries to edit his answer with errors', js: true do - within '.answers' do + within "#answer_#{answer.id}" do click_on 'Edit' - fill_in 'answer_body', with: new_invalid_body + fill_in 'body', with: new_invalid_body click_on 'Update' expect(page).to have_content answer.body @@ -49,11 +50,7 @@ end scenario "tries to edit another user's answer" do - click_on 'Log out' - login(user2) - visit question_path(question) - - within '.answers' do + within "#answer_#{answer2.id}" do expect(page).to_not have_link 'Edit' end end diff --git a/spec/features/question/destroy_spec.rb b/spec/features/question/destroy_spec.rb index 7b43b9c..3968cc6 100644 --- a/spec/features/question/destroy_spec.rb +++ b/spec/features/question/destroy_spec.rb @@ -14,7 +14,9 @@ scenario 'user deletes his question' do visit question_path(question1) - click_on 'Delete the question' + within "#question_#{question1.id}" do + click_on 'Delete' + end expect(page).to have_content "Your question was successfully deleted!" expect(page).to_not have_content question1.title @@ -23,6 +25,8 @@ scenario "user tries to delete another's question" do visit question_path(question2) - expect(page).to_not have_link 'Delete the question' + within "#question_#{question2.id}" do + expect(page).to_not have_link 'Delete' + end end end diff --git a/spec/features/question/edit_spec.rb b/spec/features/question/edit_spec.rb new file mode 100644 index 0000000..cfa53dc --- /dev/null +++ b/spec/features/question/edit_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +feature 'user can edit his question', %q{ + in order to correct mistakes + as an author of an question + i'd like to be able to edit my question +} do + given(:user1) { create(:user) } + given(:user2) { create(:user) } + given(:question1) { create(:question, user: user1) } + given(:question2) { create(:question, user: user2) } + + scenario 'unauthenticated user can not edit questions' do + visit question_path(question1) + + within "#question_#{question1.id}" do + expect(page).to_not have_link "Edit" + end + end + + describe 'authenticated user' do + given(:new_valid_body) { "New question's body #{"c" * 32}" } + given(:new_invalid_body) { "New question's invalid body" } + + background do + login(user1) + end + + scenario 'edits his question', js: true do + visit question_path(question1) + + within "#question_#{question1.id}" do + click_on 'Edit' + fill_in 'question_body', with: new_valid_body + click_on 'Update' + sleep(20) + + expect(page).to_not have_content(question1.body) + expect(page).to have_content new_valid_body + expect(page).to_not have_selector 'textarea' + end + end + + scenario 'try to edit his question with errors' + scenario "try to edit another user's question" + end +end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb new file mode 100644 index 0000000..3e84d42 --- /dev/null +++ b/spec/support/wait_for_ajax.rb @@ -0,0 +1,15 @@ +module WaitForAjax + def wait_for_ajax + Timeout.timeout(Capybara.default_max_wait_time) do + loop until finished_all_ajax_requests? + end + end + + def finished_all_ajax_requests? + page.evaluate_script('jQuery.active').zero? + end +end + +RSpec.configure do |config| + config.include WaitForAjax, type: :feature +end From 7451f3b17b536da3f4a246c9fb4f4a954406f5bf Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 11:35:11 +0300 Subject: [PATCH 08/19] Fix tests. --- app/views/questions/_question_for_show.html.slim | 4 +++- app/views/questions/update.js.erb | 2 +- spec/controllers/questions_controller_spec.rb | 12 ++++++------ spec/features/answer/create_spec.rb | 8 +++++--- spec/features/answer/edit_spec.rb | 4 ++-- spec/features/question/edit_spec.rb | 1 - 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/views/questions/_question_for_show.html.slim b/app/views/questions/_question_for_show.html.slim index bf32d56..d1eb0b2 100644 --- a/app/views/questions/_question_for_show.html.slim +++ b/app/views/questions/_question_for_show.html.slim @@ -4,6 +4,8 @@ p= @question.body .question-errors =render 'shared/errors', resource: @question = form_with model: @question, class: 'hidden', html: { id: "edit-question-#{@question.id}" } do |f| - = f.label :body, 'Your question' + = f.label :title + = f.text_field :title + = f.label :body, 'Body' = f.text_area :body = f.submit 'Update' diff --git a/app/views/questions/update.js.erb b/app/views/questions/update.js.erb index 02dec75..a085def 100644 --- a/app/views/questions/update.js.erb +++ b/app/views/questions/update.js.erb @@ -1,4 +1,4 @@ $('.question-errors').html('<%= render 'shared/errors', resource: @question %>') <% if !@question.errors.present? %> - $("#question_" + <%= @question.id %>).replaceWith('<%= j render 'question_for_show', resource: @question %>') + $("#question_" + <%= @question.id %>).html('<%= j render 'question_for_show', resource: @question %>') <% end %> diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb index a353cef..6f23674 100644 --- a/spec/controllers/questions_controller_spec.rb +++ b/spec/controllers/questions_controller_spec.rb @@ -101,7 +101,7 @@ context 'with valid attributes' do it 'changes question attributes' do - patch :update, params: { id: question, question: { title: "New title for the first question", body: "#{"b" * 50}" } } + patch :update, params: { id: question, question: { title: "New title for the first question", body: "#{"b" * 50}" }, format: :js } question.reload expect(question.title).to eq("New title for the first question") @@ -109,7 +109,7 @@ end it 'redirects to updated question' do - patch :update, params: { id: question, question: attributes_for(:question) } + patch :update, params: { id: question, question: attributes_for(:question), format: :js } expect(response).to redirect_to question end @@ -117,14 +117,14 @@ context 'with invalid attributes' do it 'does not change the question' do expect { - patch :update, params: { id: question, question: attributes_for(:question, :invalid) } + patch :update, params: { id: question, question: attributes_for(:question, :invalid), format: :js } }.to_not change { question.reload.updated_at } end - it 'renders edit view' do - patch :update, params: { id: question, question: attributes_for(:question, :invalid) } + it 'renders update view' do + patch :update, params: { id: question, question: attributes_for(:question, :invalid), format: :js } - expect(response).to render_template :edit + expect(response).to render_template :update end end end diff --git a/spec/features/answer/create_spec.rb b/spec/features/answer/create_spec.rb index f95ba28..e1d76a0 100644 --- a/spec/features/answer/create_spec.rb +++ b/spec/features/answer/create_spec.rb @@ -36,10 +36,12 @@ given(:user) { create(:user) } given(:question) { create(:question, user: user) } - scenario 'unauthenticated user tries to ask a question' do + scenario 'unauthenticated user tries to get an answer' do visit question_path(question) - fill_in 'Body', with: "#{"body" * 25}" - click_on 'Leave' + within ".new-answer" do + fill_in 'Body', with: "#{"body" * 25}" + click_on 'Leave' + end expect(page).to have_content 'You need to sign in or sign up before continuing.' end diff --git a/spec/features/answer/edit_spec.rb b/spec/features/answer/edit_spec.rb index 2a350ce..0e5c5a8 100644 --- a/spec/features/answer/edit_spec.rb +++ b/spec/features/answer/edit_spec.rb @@ -29,7 +29,7 @@ scenario 'edits his answer', js: true do within "#answer_#{answer.id}" do click_on 'Edit' - fill_in 'body', with: new_valid_body + fill_in 'answer_body', with: new_valid_body click_on 'Update' expect(page).to_not have_content answer.body @@ -41,7 +41,7 @@ scenario 'tries to edit his answer with errors', js: true do within "#answer_#{answer.id}" do click_on 'Edit' - fill_in 'body', with: new_invalid_body + fill_in 'answer_body', with: new_invalid_body click_on 'Update' expect(page).to have_content answer.body diff --git a/spec/features/question/edit_spec.rb b/spec/features/question/edit_spec.rb index cfa53dc..cb7c559 100644 --- a/spec/features/question/edit_spec.rb +++ b/spec/features/question/edit_spec.rb @@ -33,7 +33,6 @@ click_on 'Edit' fill_in 'question_body', with: new_valid_body click_on 'Update' - sleep(20) expect(page).to_not have_content(question1.body) expect(page).to have_content new_valid_body From f739bd8984c4d9294f1be94028a6666be8689dda Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 12:09:04 +0300 Subject: [PATCH 09/19] Update feature 'user can edit his question'. --- spec/features/question/edit_spec.rb | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/features/question/edit_spec.rb b/spec/features/question/edit_spec.rb index cb7c559..6537234 100644 --- a/spec/features/question/edit_spec.rb +++ b/spec/features/question/edit_spec.rb @@ -40,7 +40,25 @@ end end - scenario 'try to edit his question with errors' - scenario "try to edit another user's question" + scenario 'tries to edit his question with errors', js: true do + visit question_path(question1) + + within "#question_#{question1.id}" do + click_on 'Edit' + fill_in 'question_body', with: new_invalid_body + click_on 'Update' + + expect(page).to have_content(question1.body) + end + expect(page).to have_content 'Body is too short (minimum is 50 characters)' + end + + scenario "tries to edit another user's question", js: true do + visit question_path(question2) + + within "#question_#{question2.id}" do + expect(page).to_not have_link 'Edit' + end + end end end From e2c249601f81497132f0a66fd517f1323b6b1d56 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 14:35:09 +0300 Subject: [PATCH 10/19] Update feature 'user can delete his answers but not another's'. --- app/controllers/answers_controller.rb | 7 ++-- app/views/answers/destroy.js.erb | 1 + .../shared/_manage_answer_links.html.slim | 2 +- spec/controllers/answers_controller_spec.rb | 12 +++---- spec/features/answer/destroy_spec.rb | 36 ++++++++++++------- 5 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 app/views/answers/destroy.js.erb diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 2d0f707..079496c 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -19,10 +19,9 @@ def update end def destroy - answer = Answer.find(params[:id]) - return head :forbidden unless current_user.owned?(answer) - answer.destroy - redirect_to answer.question, notice: "Your answer was successfully deleted!" + @answer = Answer.find(params[:id]) + return head :forbidden unless current_user.owned?(@answer) + @answer.destroy end private diff --git a/app/views/answers/destroy.js.erb b/app/views/answers/destroy.js.erb new file mode 100644 index 0000000..9290d66 --- /dev/null +++ b/app/views/answers/destroy.js.erb @@ -0,0 +1 @@ +$('#answer_' + <%= @answer.id %>).remove() diff --git a/app/views/shared/_manage_answer_links.html.slim b/app/views/shared/_manage_answer_links.html.slim index 24a1a8a..4cfe8f7 100644 --- a/app/views/shared/_manage_answer_links.html.slim +++ b/app/views/shared/_manage_answer_links.html.slim @@ -1,4 +1,4 @@ p = link_to 'Edit', '#', class: 'edit-answer-links', data: { answer_id: answer.id } |  - = link_to 'Delete', answer_path(answer), method: :delete + = link_to 'Delete', answer_path(answer), method: :delete, remote: true diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index b1bcdb2..c7e8d4a 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -54,21 +54,21 @@ before { login(user) } - it 'deletes the answer' do + it "deletes user's answer" do expect { - delete :destroy, params: { id: answer } + delete :destroy, params: { id: answer, format: :js } }.to change(Answer, :count).by(-1) expect { answer.reload }.to raise_error ActiveRecord::RecordNotFound end - it 'redirects to Question show view' do - delete :destroy, params: { id: answer } + it 'renders destroy template' do + delete :destroy, params: { id: answer, format: :js } - expect(response).to redirect_to question + expect(response).to render_template 'destroy' end it "tries to delete another user's answer" do - expect { delete :destroy, params: { id: answer2 } }.to_not change(Answer, :count) + expect { delete :destroy, params: { id: answer2, format: :js } }.to_not change(Answer, :count) end end diff --git a/spec/features/answer/destroy_spec.rb b/spec/features/answer/destroy_spec.rb index bae2a6c..73fb2c5 100644 --- a/spec/features/answer/destroy_spec.rb +++ b/spec/features/answer/destroy_spec.rb @@ -8,28 +8,38 @@ } do given(:user1) { create(:user) } given(:user2) { create(:user) } - given(:user3) { create(:user) } - given(:question) { create(:question, user: user3) } + given(:question) { create(:question, user: user1) } - before { login(user1) } + describe 'authenticated user', js: true do + background do + login(user1) + end - scenario 'user deletes his answer' do - answer = create(:answer, question: question, user: user1) + scenario 'deletes his answer' do + answer = create(:answer, question: question, user: user1) + visit question_path(question) + within "#answer_#{answer.id}" do + click_on 'Delete' + end - visit question_path(question) - within '.answers' do - click_on 'Delete' + expect(page).to_not have_content answer.body end - expect(page).to have_content "Your answer was successfully deleted!" - end + scenario "tries to delete another's answer" do + answer = create(:answer, question: question, user: user2) + visit question_path(question) - scenario "user tries to delete another's answer" do - create(:answer, question: question, user: user2) + within "#answer_#{answer.id}" do + expect(page).to_not have_link 'Delete' + end + end + end + scenario 'unauthenticated user tries to delete an answer' do + answer = create(:answer, question: question, user: user1) visit question_path(question) - within '.answers' do + within "#answer_#{answer.id}" do expect(page).to_not have_link 'Delete' end end From 87025f9849f8ed0eb50204cd1f6ba54a58b7324a Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 22:17:05 +0300 Subject: [PATCH 11/19] Implement scenario 'marks an answer to his question as the best'. --- app/controllers/answers_controller.rb | 13 ++- app/models/answer.rb | 9 +- app/views/answers/_answer.html.slim | 7 +- app/views/answers/mark.js.erb | 1 + app/views/questions/show.html.slim | 2 +- app/views/shared/_answer_links.html.slim | 7 ++ .../shared/_manage_answer_links.html.slim | 4 - config/routes.rb | 6 +- .../20190624142356_add_best_to_answers.rb | 5 ++ db/schema.rb | 3 +- spec/controllers/answers_controller_spec.rb | 82 +++++++++++++------ spec/features/answer/mark_spec.rb | 37 +++++++++ spec/models/answer_spec.rb | 22 +++++ 13 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 app/views/answers/mark.js.erb create mode 100644 app/views/shared/_answer_links.html.slim delete mode 100644 app/views/shared/_manage_answer_links.html.slim create mode 100644 db/migrate/20190624142356_add_best_to_answers.rb create mode 100644 spec/features/answer/mark_spec.rb diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 079496c..7a0e943 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -1,5 +1,6 @@ class AnswersController < ApplicationController before_action :authenticate_user! + before_action :set_answer, only: %i[update destroy mark] def new @answer = Answer.new @@ -13,20 +14,28 @@ def create end def update - @answer = Answer.find(params[:id]) @answer.update(answer_params) @question = @answer.question end def destroy - @answer = Answer.find(params[:id]) return head :forbidden unless current_user.owned?(@answer) @answer.destroy end + def mark + return head :forbidden unless current_user.owned?(@answer.question) + @answer.mark_as_best + @question = @answer.question + end + private def answer_params params.require(:answer).permit(:body) end + + def set_answer + @answer = Answer.find(params[:id]) + end end diff --git a/app/models/answer.rb b/app/models/answer.rb index 37837dd..0064297 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,8 +2,15 @@ class Answer < ApplicationRecord belongs_to :user belongs_to :question + default_scope { order :created_at } + validates :body, presence: true, length: { minimum: 50 } - default_scope { order :created_at } + def mark_as_best + transaction do + question.answers.update_all(best: false) + self.update!(best: true) + end + end end diff --git a/app/views/answers/_answer.html.slim b/app/views/answers/_answer.html.slim index 07cb7ed..d1062ba 100644 --- a/app/views/answers/_answer.html.slim +++ b/app/views/answers/_answer.html.slim @@ -1,9 +1,10 @@ div id="answer_#{answer.id}" - if answer.persisted? + - if answer.best? + div id="best-answer" + h4 The best answer p= answer.body - - - if answer.persisted? && current_user&.owned?(answer) - p= render(partial: 'shared/manage_answer_links', locals: { answer: answer }) + p= render(partial: 'shared/answer_links', locals: { answer: answer }) = form_with model: answer, class: 'hidden', html: { id: "edit-answer-#{answer.id}" } do |f| = f.label :body, 'Your answer' diff --git a/app/views/answers/mark.js.erb b/app/views/answers/mark.js.erb new file mode 100644 index 0000000..2c524c3 --- /dev/null +++ b/app/views/answers/mark.js.erb @@ -0,0 +1 @@ +$('.answers').html('<%= j render @question.answers %>') diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index a5dd6af..05a8929 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -1,7 +1,7 @@ div id="question_#{@question.id}" = render 'question_for_show', resource: @question +h4 Answers div.answers - h4 Answers = render @question.answers p Your answer .answer-errors diff --git a/app/views/shared/_answer_links.html.slim b/app/views/shared/_answer_links.html.slim new file mode 100644 index 0000000..0242f10 --- /dev/null +++ b/app/views/shared/_answer_links.html.slim @@ -0,0 +1,7 @@ +p + => link_to('Mark as the best', mark_answer_path(answer), method: :post, remote: true) \ + if current_user&.owned?(answer.question) && !answer.best? + => link_to('Edit', '#', class: 'edit-answer-links', data: { answer_id: answer.id }) \ + if current_user&.owned?(answer) + = link_to('Delete', answer_path(answer), method: :delete, remote: true) \ + if current_user&.owned?(answer) diff --git a/app/views/shared/_manage_answer_links.html.slim b/app/views/shared/_manage_answer_links.html.slim deleted file mode 100644 index 4cfe8f7..0000000 --- a/app/views/shared/_manage_answer_links.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -p - = link_to 'Edit', '#', class: 'edit-answer-links', data: { answer_id: answer.id } - |  - = link_to 'Delete', answer_path(answer), method: :delete, remote: true diff --git a/config/routes.rb b/config/routes.rb index 0c9d95c..4175820 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,10 @@ root to: "questions#index" resources :questions do - resources :answers, shallow: true, except: %i[index show] + resources :answers, shallow: true, except: %i[index show] do + member do + post :mark + end + end end end diff --git a/db/migrate/20190624142356_add_best_to_answers.rb b/db/migrate/20190624142356_add_best_to_answers.rb new file mode 100644 index 0000000..a9007d8 --- /dev/null +++ b/db/migrate/20190624142356_add_best_to_answers.rb @@ -0,0 +1,5 @@ +class AddBestToAnswers < ActiveRecord::Migration[5.2] + def change + add_column :answers, :best, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6107500..199c4e9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_18_204807) do +ActiveRecord::Schema.define(version: 2019_06_24_142356) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -21,6 +21,7 @@ t.datetime "updated_at", null: false t.bigint "question_id" t.bigint "user_id" + t.boolean "best", default: false t.index ["question_id"], name: "index_answers_on_question_id" t.index ["user_id"], name: "index_answers_on_user_id" end diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index c7e8d4a..c677e48 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -2,12 +2,12 @@ require 'byebug' RSpec.describe AnswersController, type: :controller do - let(:user) { create(:user) } - let(:question) { create(:question, user: user) } + let(:user1) { create(:user) } + let(:question1) { create(:question, user: user1) } describe 'GET #new' do - before { login(user) } - before { get :new, params: { question_id: question.id } } + before { login(user1) } + before { get :new, params: { question_id: question1.id } } it 'assigns a new answer to @answer' do expect(assigns(:answer)).to be_a_new(Answer) @@ -19,50 +19,50 @@ end describe 'POST #create' do - before { login(user) } + before { login(user1) } context 'with valid attributes' do it 'saves a new answer in the database' do - expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer), format: :js } }.to change(question.answers, :count).by(1) + expect { post :create, params: { question_id: question1.id, answer: attributes_for(:answer), format: :js } }.to change(question1.answers, :count).by(1) end it 'creates answer by the name of logged user' do new_answer_params = attributes_for(:answer) - post :create, params: { question_id: question.id, answer: new_answer_params, format: :js } - created_answer = question.answers.find_by! new_answer_params + post :create, params: { question_id: question1.id, answer: new_answer_params, format: :js } + created_answer = question1.answers.find_by! new_answer_params - expect(created_answer.user).to eq user + expect(created_answer.user).to eq user1 end end context 'with invalid attributes' do it 'does not save a new answer with short body in the database' do - expect { post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } }.to_not change(Answer, :count) + expect { post :create, params: { question_id: question1.id, answer: attributes_for(:answer, :invalid), format: :js } }.to_not change(Answer, :count) end it 'renders create template' do - post :create, params: { question_id: question.id, answer: attributes_for(:answer, :invalid), format: :js } + post :create, params: { question_id: question1.id, answer: attributes_for(:answer, :invalid), format: :js } expect(response).to render_template 'create' end end end describe 'DELETE #destroy' do - let!(:answer) { create(:answer, question: question, user: user) } + let!(:answer1) { create(:answer, question: question1, user: user1) } let(:user2) { create(:user) } - let!(:answer2) { create(:answer, question: question, user: user2) } + let!(:answer2) { create(:answer, question: question1, user: user2) } - before { login(user) } + before { login(user1) } it "deletes user's answer" do expect { - delete :destroy, params: { id: answer, format: :js } + delete :destroy, params: { id: answer1, format: :js } }.to change(Answer, :count).by(-1) - expect { answer.reload }.to raise_error ActiveRecord::RecordNotFound + expect { answer1.reload }.to raise_error ActiveRecord::RecordNotFound end it 'renders destroy template' do - delete :destroy, params: { id: answer, format: :js } + delete :destroy, params: { id: answer1, format: :js } expect(response).to render_template 'destroy' end @@ -73,21 +73,21 @@ end describe 'PATCH #update' do - let!(:answer) { create(:answer, question: question, user: user) } + let!(:answer1) { create(:answer, question: question1, user: user1) } let!(:new_body) { "New answer's body #{"a" * 32}" } - before { login(user) } + before { login(user1) } context 'with valid attributes' do it 'updates an answer' do - patch :update, params: { id: answer, answer: { body: new_body } }, format: :js - answer.reload + patch :update, params: { id: answer1, answer: { body: new_body } }, format: :js + answer1.reload - expect(answer.body).to eq new_body + expect(answer1.body).to eq new_body end it 'renders update view' do - patch :update, params: { id: answer, answer: { body: new_body } }, format: :js + patch :update, params: { id: answer1, answer: { body: new_body } }, format: :js expect(response).to render_template :update end @@ -97,18 +97,48 @@ it 'does not update an answer' do expect { patch :update, - params: { id: answer, answer: attributes_for(:answer, :invalid) }, + params: { id: answer1, answer: attributes_for(:answer, :invalid) }, format: :js - }.to_not change(answer, :body) + }.to_not change(answer1, :body) end it 'renders update view' do patch :update, - params: { id: answer, answer: attributes_for(:answer, :invalid) }, + params: { id: answer1, answer: attributes_for(:answer, :invalid) }, format: :js expect(response).to render_template :update end end end + + describe 'POST #mark' do + let(:user2) { create(:user) } + let!(:question2) { create(:question, user: user2) } + let!(:answer1) { create(:answer, question: question2, user: user1) } + let!(:answer2) { create(:answer, question: question1, user: user2) } + let!(:answer3) { create(:answer, question: question1, user: user2) } + + before { login(user1) } + + it 'marks answers to his question' do + post :mark, params: { id: answer2 }, format: :js + answer2.reload + + expect(answer2.best).to eq true + + post :mark, params: { id: answer3 }, format: :js + answer2.reload + answer3.reload + + expect(answer2.best).to eq false + expect(answer3.best).to eq true + end + + it "tries to mark an answer to another user's question" do + post :mark, params: { id: answer1 }, format: :js + + expect(answer2.best).to eq false + end + end end diff --git a/spec/features/answer/mark_spec.rb b/spec/features/answer/mark_spec.rb new file mode 100644 index 0000000..dc3aaac --- /dev/null +++ b/spec/features/answer/mark_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +feature 'user can mark one answer as the best', %q{ + in order to emphasize the most proper answer + as an author of a question + i'd like to be able to mark one of answers to my question as the best +} do + given(:user1) { create(:user) } + given(:user2) { create(:user) } + given(:question1) { create(:question, user: user1) } + given(:question2) { create(:question, user: user2) } + given!(:answer1) { create(:answer, question: question2, user: user1) } + given!(:answer2) { create(:answer, question: question1, user: user2) } + + scenario 'unauthenticated user tries to choose the best answer' do + visit question_path(question1) + + expect(page).to_not have_link 'Mark as the best' + end + + describe 'authenticated user', js: true do + background do + login(user1) + end + + scenario 'marks an answer to his question as the best' do + visit question_path(question1) + within "#answer_#{answer2.id}" do + click_on 'Mark as the best' + + expect(page).to have_content 'The best answer' + end + end + + scenario "tries to mark an answer to another's user question as the best" + end +end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index 1352b36..706b61e 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -7,4 +7,26 @@ it { should validate_presence_of :body } it { should validate_length_of(:body).is_at_least(50) } + + describe '#mark_best' do + let(:user) { create(:user) } + let(:question) { create(:question, user: user) } + let(:answer1) { create(:answer, question: question, user: user) } + let(:answer2) { create(:answer, question: question, user: user) } + + it 'marks one answer' do + answer1.mark_as_best + + expect(answer1).to be_best + end + + it 'marks one and then another answer' do + answer1.mark_as_best + answer2.mark_as_best + answer1.reload + + expect(answer1).to_not be_best + expect(answer2).to be_best + end + end end From 39eb7219d0fb16cdbd06d046bca8cd7b23902461 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 22:24:31 +0300 Subject: [PATCH 12/19] Update scenario 'marks an answer to his question as the best'. --- spec/features/answer/mark_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/features/answer/mark_spec.rb b/spec/features/answer/mark_spec.rb index dc3aaac..5068373 100644 --- a/spec/features/answer/mark_spec.rb +++ b/spec/features/answer/mark_spec.rb @@ -11,6 +11,7 @@ given(:question2) { create(:question, user: user2) } given!(:answer1) { create(:answer, question: question2, user: user1) } given!(:answer2) { create(:answer, question: question1, user: user2) } + given!(:answer3) { create(:answer, question: question1, user: user2) } scenario 'unauthenticated user tries to choose the best answer' do visit question_path(question1) @@ -29,6 +30,19 @@ click_on 'Mark as the best' expect(page).to have_content 'The best answer' + expect(page).to_not have_link 'Mark as the best' + end + + within "#answer_#{answer3.id}" do + click_on 'Mark as the best' + + expect(page).to have_content 'The best answer' + expect(page).to_not have_link 'Mark as the best' + end + + within "#answer_#{answer2.id}" do + expect(page).to_not have_content 'The best answer' + expect(page).to have_link 'Mark as the best' end end From 44b29edf0fdb211a0f5dbec12cde2120919e6478 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 22:27:59 +0300 Subject: [PATCH 13/19] Implement scenario 'tries to mark an answer to another's user question as the best'. --- spec/features/answer/mark_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/features/answer/mark_spec.rb b/spec/features/answer/mark_spec.rb index 5068373..8ec0e1e 100644 --- a/spec/features/answer/mark_spec.rb +++ b/spec/features/answer/mark_spec.rb @@ -46,6 +46,11 @@ end end - scenario "tries to mark an answer to another's user question as the best" + scenario "tries to mark an answer to another's user question as the best" do + visit question_path(question2) + within "#answer_#{answer1.id}" do + expect(page).to_not have_link 'Mark as the best' + end + end end end From d4a8da03a0402d53e03c23bb91dc1d9548ce0a41 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Mon, 24 Jun 2019 23:24:45 +0300 Subject: [PATCH 14/19] Implement scenario 'marks an answer to his question as the best thereby moving this answer to the top'. --- app/models/answer.rb | 2 +- spec/features/answer/mark_spec.rb | 41 ++++++++++++++++++++++++++----- spec/models/answer_spec.rb | 26 ++++++++++++++------ 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/app/models/answer.rb b/app/models/answer.rb index 0064297..5207488 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,7 +2,7 @@ class Answer < ApplicationRecord belongs_to :user belongs_to :question - default_scope { order :created_at } + default_scope { order(best: :desc, created_at: :asc) } validates :body, presence: true, length: { minimum: 50 } diff --git a/spec/features/answer/mark_spec.rb b/spec/features/answer/mark_spec.rb index 8ec0e1e..e0175d1 100644 --- a/spec/features/answer/mark_spec.rb +++ b/spec/features/answer/mark_spec.rb @@ -3,15 +3,11 @@ feature 'user can mark one answer as the best', %q{ in order to emphasize the most proper answer as an author of a question - i'd like to be able to mark one of answers to my question as the best + i'd like to be able to mark + one of answers to my question as the best } do given(:user1) { create(:user) } - given(:user2) { create(:user) } given(:question1) { create(:question, user: user1) } - given(:question2) { create(:question, user: user2) } - given!(:answer1) { create(:answer, question: question2, user: user1) } - given!(:answer2) { create(:answer, question: question1, user: user2) } - given!(:answer3) { create(:answer, question: question1, user: user2) } scenario 'unauthenticated user tries to choose the best answer' do visit question_path(question1) @@ -20,12 +16,19 @@ end describe 'authenticated user', js: true do + given(:user2) { create(:user) } + given(:question2) { create(:question, user: user2) } + given!(:answer1) { create(:answer, question: question2, user: user1) } + given!(:answer2) { create(:answer, question: question1, user: user2) } + given!(:answer3) { create(:answer, question: question1, user: user2) } + background do login(user1) end scenario 'marks an answer to his question as the best' do visit question_path(question1) + within "#answer_#{answer2.id}" do click_on 'Mark as the best' @@ -48,9 +51,35 @@ scenario "tries to mark an answer to another's user question as the best" do visit question_path(question2) + within "#answer_#{answer1.id}" do expect(page).to_not have_link 'Mark as the best' end end + + scenario %q{ + marks an answer to his question as the best + thereby moving this answer to the top + } do + visit question_path(question1) + + answer2_el = find("#answer_#{answer2.id}") + answer3_el = find("#answer_#{answer3.id}") + + if answer2_el.path < answer3_el.path + upper_answer, lower_answer = answer2, answer3 + else + upper_answer, lower_answer = answer3, answer2 + end + + within "#answer_#{lower_answer.id}" do + click_on 'Mark as the best' + end + + best_answer_el = find("#answer_#{lower_answer.id}") + regular_answer_el = find("#answer_#{upper_answer.id}") + + expect(best_answer_el.path).to be < regular_answer_el.path + end end end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index 706b61e..ef0a7e3 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -3,17 +3,16 @@ RSpec.describe Answer, type: :model do it { should belong_to :question } it { should belong_to :user } - it { should validate_presence_of :body } - it { should validate_length_of(:body).is_at_least(50) } - describe '#mark_best' do - let(:user) { create(:user) } - let(:question) { create(:question, user: user) } - let(:answer1) { create(:answer, question: question, user: user) } - let(:answer2) { create(:answer, question: question, user: user) } + let(:user) { create(:user) } + let(:question) { create(:question, user: user) } + let(:answer1) { create(:answer, question: question, user: user) } + let(:answer2) { create(:answer, question: question, user: user) } + let(:answer3) { create(:answer, question: question, user: user) } + describe '#mark_best' do it 'marks one answer' do answer1.mark_as_best @@ -29,4 +28,17 @@ expect(answer2).to be_best end end + + describe 'default_scope order' do + it 'sorting answers with the best as the first' do + answer3.mark_as_best + expect(answer3.question.answers.first).to eq answer3 + + answer2.mark_as_best + expect(answer2.question.answers.first).to eq answer2 + + answer1.mark_as_best + expect(answer1.question.answers.first).to eq answer1 + end + end end From 7290c45f98627e6fb96d8386539cadb43a5ce888 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Tue, 25 Jun 2019 18:12:19 +0300 Subject: [PATCH 15/19] Fix tests, models, controllers. --- app/controllers/answers_controller.rb | 1 + app/controllers/questions_controller.rb | 1 + app/models/answer.rb | 19 +- app/views/answers/update.js.erb | 2 +- .../questions/_question_for_show.html.slim | 2 +- .../shared/_manage_question_links.html.slim | 4 - app/views/shared/_question_links.html.slim | 3 + spec/controllers/answers_controller_spec.rb | 185 +++++++++++++----- spec/controllers/questions_controller_spec.rb | 100 +++++++--- spec/features/question/destroy_spec.rb | 34 ++-- spec/models/answer_spec.rb | 24 +-- spec/support/wait_for_ajax.rb | 15 -- 12 files changed, 263 insertions(+), 127 deletions(-) delete mode 100644 app/views/shared/_manage_question_links.html.slim create mode 100644 app/views/shared/_question_links.html.slim delete mode 100644 spec/support/wait_for_ajax.rb diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 7a0e943..e4555f9 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -14,6 +14,7 @@ def create end def update + return head :forbidden unless current_user.owned?(@answer) @answer.update(answer_params) @question = @answer.question end diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 39b4314..24036ea 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -29,6 +29,7 @@ def create end def update + return head :forbidden unless current_user.owned?(@question) @question.update(question_params) end diff --git a/app/models/answer.rb b/app/models/answer.rb index 5207488..871d79c 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -3,14 +3,31 @@ class Answer < ApplicationRecord belongs_to :question default_scope { order(best: :desc, created_at: :asc) } + scope :best, -> { where(best: true) } validates :body, presence: true, length: { minimum: 50 } + validate :only_one_best_per_question def mark_as_best transaction do - question.answers.update_all(best: false) + question.answers.best.update_all(best: false) self.update!(best: true) end end + + private + + def only_one_best_per_question + return unless best? + return unless question&.answers.best.any? + + if persisted? + # update + errors.add(:best, :there_is_the_best_answer_in_question_already) if changes['best'] == [false, true] + else + # create + errors.add(:best, :there_is_the_best_answer_in_question_already) + end + end end diff --git a/app/views/answers/update.js.erb b/app/views/answers/update.js.erb index b0bdda4..76bc285 100644 --- a/app/views/answers/update.js.erb +++ b/app/views/answers/update.js.erb @@ -1,4 +1,4 @@ $('.answer-errors').html('<%= render 'shared/errors', resource: @answer %>') -<% if !@answer.errors.present? %> +<% if @answer.errors.empty? %> $("#answer_" + <%= @answer.id %>).replaceWith('<%= j render @answer %>') <% end %> diff --git a/app/views/questions/_question_for_show.html.slim b/app/views/questions/_question_for_show.html.slim index d1eb0b2..7af2f0f 100644 --- a/app/views/questions/_question_for_show.html.slim +++ b/app/views/questions/_question_for_show.html.slim @@ -1,6 +1,6 @@ h4= @question.title p= @question.body -=render(partial: 'shared/manage_question_links', locals: { question: @question }) if current_user&.owned?(@question) +=render(partial: 'shared/question_links', locals: { question: @question }) if current_user&.owned?(@question) .question-errors =render 'shared/errors', resource: @question = form_with model: @question, class: 'hidden', html: { id: "edit-question-#{@question.id}" } do |f| diff --git a/app/views/shared/_manage_question_links.html.slim b/app/views/shared/_manage_question_links.html.slim deleted file mode 100644 index 1ceb9ff..0000000 --- a/app/views/shared/_manage_question_links.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -p - = link_to 'Edit', '#', class: 'edit-question-link', data: { question_id: question.id } - |  - = link_to 'Delete', question_path(question), method: :delete diff --git a/app/views/shared/_question_links.html.slim b/app/views/shared/_question_links.html.slim new file mode 100644 index 0000000..881b8bf --- /dev/null +++ b/app/views/shared/_question_links.html.slim @@ -0,0 +1,3 @@ +p + => link_to 'Edit', '#', class: 'edit-question-link', data: { question_id: question.id } + = link_to 'Delete', question_path(question), method: :delete diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index c677e48..2a431a7 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -23,25 +23,60 @@ context 'with valid attributes' do it 'saves a new answer in the database' do - expect { post :create, params: { question_id: question1.id, answer: attributes_for(:answer), format: :js } }.to change(question1.answers, :count).by(1) + expect { + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer) + }, + format: :js + }.to change(question1.answers, :count).by(1) end it 'creates answer by the name of logged user' do new_answer_params = attributes_for(:answer) - post :create, params: { question_id: question1.id, answer: new_answer_params, format: :js } + post :create, + params: { + question_id: question1.id, + answer: new_answer_params + }, + format: :js created_answer = question1.answers.find_by! new_answer_params - expect(created_answer.user).to eq user1 + expect(user1).to be_owned(created_answer) + end + + it 'renders create template', js: true do + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer) + }, + format: :js + + expect(response).to render_template 'create' end end context 'with invalid attributes' do it 'does not save a new answer with short body in the database' do - expect { post :create, params: { question_id: question1.id, answer: attributes_for(:answer, :invalid), format: :js } }.to_not change(Answer, :count) + expect { + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer, :invalid) + }, + format: :js + }.to_not change(Answer, :count) end it 'renders create template' do - post :create, params: { question_id: question1.id, answer: attributes_for(:answer, :invalid), format: :js } + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer, :invalid) + }, + format: :js expect(response).to render_template 'create' end end @@ -52,62 +87,100 @@ let(:user2) { create(:user) } let!(:answer2) { create(:answer, question: question1, user: user2) } - before { login(user1) } + context 'authenticated user' do + before { login(user1) } - it "deletes user's answer" do - expect { - delete :destroy, params: { id: answer1, format: :js } - }.to change(Answer, :count).by(-1) - expect { answer1.reload }.to raise_error ActiveRecord::RecordNotFound - end + it "deletes user's answer" do + expect { + delete :destroy, params: { id: answer1 }, format: :js + }.to change(question1.answers, :count).by(-1) + expect { answer1.reload }.to raise_error ActiveRecord::RecordNotFound + end - it 'renders destroy template' do - delete :destroy, params: { id: answer1, format: :js } + it 'renders destroy template' do + delete :destroy, params: { id: answer1 }, format: :js - expect(response).to render_template 'destroy' + expect(response).to render_template 'destroy' + end + + it "tries to delete another user's answer" do + expect { + delete :destroy, params: { id: answer2 }, format: :js + }.to_not change(Answer, :count) + end end - it "tries to delete another user's answer" do - expect { delete :destroy, params: { id: answer2, format: :js } }.to_not change(Answer, :count) + context 'authenticated user' do + it "tries to delete an answer" do + expect { + delete :destroy, params: { id: answer2 }, format: :js + }.to_not change(Answer, :count) + end end end describe 'PATCH #update' do + let(:user2) { create(:user) } let!(:answer1) { create(:answer, question: question1, user: user1) } + let!(:answer2) { create(:answer, question: question1, user: user2) } let!(:new_body) { "New answer's body #{"a" * 32}" } - before { login(user1) } + context 'authenticated user' do + before { login(user1) } - context 'with valid attributes' do - it 'updates an answer' do - patch :update, params: { id: answer1, answer: { body: new_body } }, format: :js - answer1.reload + context 'with valid attributes' do + it "updates user's answer" do + patch :update, + params: { id: answer1, answer: { body: new_body } }, + format: :js + answer1.reload + + expect(answer1.body).to eq new_body + end + + it "does not update another user's answer" do + expect { + patch :update, + params: { id: answer2, answer: { body: new_body } }, + format: :js + }.to_not change { answer2.reload.body } + end + + it 'renders update view' do + patch :update, + params: { id: answer1, answer: { body: new_body} }, + format: :js - expect(answer1.body).to eq new_body + expect(response).to render_template :update + end end - it 'renders update view' do - patch :update, params: { id: answer1, answer: { body: new_body } }, format: :js + context 'with invalid attributes' do + it 'does not update an answer' do + expect { + patch :update, + params: { id: answer1, answer: attributes_for(:answer, :invalid) }, + format: :js + }.to_not change { answer1.reload.body } + end + + it 'renders update view' do + patch :update, + params: { id: answer1, answer: attributes_for(:answer, :invalid) }, + format: :js - expect(response).to render_template :update + expect(response).to render_template :update + end end end - context 'with invalid attributes' do + context 'unauthenticated user' do it 'does not update an answer' do expect { patch :update, - params: { id: answer1, answer: attributes_for(:answer, :invalid) }, - format: :js - }.to_not change(answer1, :body) - end - - it 'renders update view' do - patch :update, - params: { id: answer1, answer: attributes_for(:answer, :invalid) }, - format: :js - - expect(response).to render_template :update + params: { id: answer1, answer: attributes_for(:answer) }, + format: :js + }.to_not change { answer1.reload.body } end end end @@ -119,26 +192,36 @@ let!(:answer2) { create(:answer, question: question1, user: user2) } let!(:answer3) { create(:answer, question: question1, user: user2) } - before { login(user1) } + context 'authenticated user' do + before { login(user1) } - it 'marks answers to his question' do - post :mark, params: { id: answer2 }, format: :js - answer2.reload + it 'marks answers to his question' do + post :mark, params: { id: answer2 }, format: :js + answer2.reload - expect(answer2.best).to eq true + expect(answer2).to be_best - post :mark, params: { id: answer3 }, format: :js - answer2.reload - answer3.reload + post :mark, params: { id: answer3 }, format: :js + answer2.reload + answer3.reload - expect(answer2.best).to eq false - expect(answer3.best).to eq true + expect(answer2).to_not be_best + expect(answer3).to be_best + end + + it "tries to mark an answer to another user's question" do + post :mark, params: { id: answer1 }, format: :js + + expect(answer2).to_not be_best + end end - it "tries to mark an answer to another user's question" do - post :mark, params: { id: answer1 }, format: :js + context 'unauthenticated user' do + it "tries to mark an answer to another user's question" do + post :mark, params: { id: answer1 }, format: :js - expect(answer2.best).to eq false + expect(answer2).to_not be_best + end end end end diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb index 6f23674..26ae93c 100644 --- a/spec/controllers/questions_controller_spec.rb +++ b/spec/controllers/questions_controller_spec.rb @@ -97,35 +97,87 @@ end describe 'PATCH #update' do - before { login(user) } - - context 'with valid attributes' do - it 'changes question attributes' do - patch :update, params: { id: question, question: { title: "New title for the first question", body: "#{"b" * 50}" }, format: :js } - question.reload - - expect(question.title).to eq("New title for the first question") - expect(question.body).to eq("#{"b" * 50}") + context 'authenticated user' do + let(:user2) { create(:user) } + let(:question2) { create(:question, user: user2) } + let(:new_title) { "New title for the first question" } + let(:new_body) { "#{"b" * 50}" } + + before { login(user) } + + context 'with valid attributes' do + it 'changes question attributes' do + patch :update, + params: { + id: question, + question: { title: new_title, body: new_body } + }, + format: :js + question.reload + + expect(question.title).to eq(new_title) + expect(question.body).to eq(new_body) + end + + it "tries to update another user's question" do + expect { + patch :update, + params: { + id: question2, + question: attributes_for(:question) + }, + format: :js + }.to_not change { question.reload.updated_at } + end + + it 'renders update view' do + patch :update, + params: { + id: question, + question: attributes_for(:question) + }, + format: :js + + expect(response).to render_template :update + end end - it 'redirects to updated question' do - patch :update, params: { id: question, question: attributes_for(:question), format: :js } - - expect(response).to redirect_to question + context 'with invalid attributes' do + it 'does not change the question' do + expect { + patch :update, + params: { + id: question, + question: attributes_for(:question, :invalid) + }, + format: :js + }.to_not change { question.reload.updated_at } + end + + it 'renders update view' do + patch :update, + params: { + id: question, + question: attributes_for(:question, :invalid) + }, + format: :js + + expect(response).to render_template :update + end end end - context 'with invalid attributes' do - it 'does not change the question' do + + context 'unauthenticated user' do + it 'tries to change question attributes' do expect { - patch :update, params: { id: question, question: attributes_for(:question, :invalid), format: :js } + patch :update, + params: { + id: question, + question: attributes_for(:question) + }, + format: :js }.to_not change { question.reload.updated_at } end - - it 'renders update view' do - patch :update, params: { id: question, question: attributes_for(:question, :invalid), format: :js } - - expect(response).to render_template :update - end end end @@ -148,7 +200,9 @@ end it "tries to delete another user's question" do - expect { delete :destroy, params: { id: question2 } }.to_not change(Question, :count) + expect { + delete :destroy, params: { id: question2 } + }.to_not change(Question, :count) end end end diff --git a/spec/features/question/destroy_spec.rb b/spec/features/question/destroy_spec.rb index 3968cc6..dc6b365 100644 --- a/spec/features/question/destroy_spec.rb +++ b/spec/features/question/destroy_spec.rb @@ -10,23 +10,35 @@ given(:question1) { create(:question, user: user1) } given(:question2) { create(:question, user: user2) } - before { login(user1) } + context 'authenticated user' do + before { login(user1) } - scenario 'user deletes his question' do - visit question_path(question1) - within "#question_#{question1.id}" do - click_on 'Delete' + scenario 'user deletes his question' do + visit question_path(question1) + within "#question_#{question1.id}" do + click_on 'Delete' + end + + expect(page).to have_content "Your question was successfully deleted!" + expect(page).to_not have_content question1.title end - expect(page).to have_content "Your question was successfully deleted!" - expect(page).to_not have_content question1.title + scenario "user tries to delete another's question" do + visit question_path(question2) + + within "#question_#{question2.id}" do + expect(page).to_not have_link 'Delete' + end + end end - scenario "user tries to delete another's question" do - visit question_path(question2) + context 'unauthenticated user' do + scenario "user tries to delete another's question" do + visit question_path(question1) - within "#question_#{question2.id}" do - expect(page).to_not have_link 'Delete' + within "#question_#{question1.id}" do + expect(page).to_not have_link 'Delete' + end end end end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index ef0a7e3..d0b25d9 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -10,34 +10,18 @@ let(:question) { create(:question, user: user) } let(:answer1) { create(:answer, question: question, user: user) } let(:answer2) { create(:answer, question: question, user: user) } - let(:answer3) { create(:answer, question: question, user: user) } - describe '#mark_best' do - it 'marks one answer' do - answer1.mark_as_best + before { answer1.mark_as_best } + describe '#mark_best' do + it 'marks one answer as the best' do expect(answer1).to be_best - end - - it 'marks one and then another answer' do - answer1.mark_as_best - answer2.mark_as_best - answer1.reload - - expect(answer1).to_not be_best - expect(answer2).to be_best + expect(answer2).to_not be_best end end describe 'default_scope order' do it 'sorting answers with the best as the first' do - answer3.mark_as_best - expect(answer3.question.answers.first).to eq answer3 - - answer2.mark_as_best - expect(answer2.question.answers.first).to eq answer2 - - answer1.mark_as_best expect(answer1.question.answers.first).to eq answer1 end end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb deleted file mode 100644 index 3e84d42..0000000 --- a/spec/support/wait_for_ajax.rb +++ /dev/null @@ -1,15 +0,0 @@ -module WaitForAjax - def wait_for_ajax - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? - end -end - -RSpec.configure do |config| - config.include WaitForAjax, type: :feature -end From a5ac82a6cb62c11f25a0114b9785d66068a78050 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Tue, 25 Jun 2019 18:14:21 +0300 Subject: [PATCH 16/19] Rename 'User#owned?' method to 'User#owner'?. --- app/controllers/answers_controller.rb | 6 +++--- app/controllers/questions_controller.rb | 4 ++-- app/models/user.rb | 2 +- app/views/questions/_question_for_show.html.slim | 2 +- app/views/shared/_answer_links.html.slim | 6 +++--- spec/controllers/answers_controller_spec.rb | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index e4555f9..31aeb26 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -14,18 +14,18 @@ def create end def update - return head :forbidden unless current_user.owned?(@answer) + return head :forbidden unless current_user.owner?(@answer) @answer.update(answer_params) @question = @answer.question end def destroy - return head :forbidden unless current_user.owned?(@answer) + return head :forbidden unless current_user.owner?(@answer) @answer.destroy end def mark - return head :forbidden unless current_user.owned?(@answer.question) + return head :forbidden unless current_user.owner?(@answer.question) @answer.mark_as_best @question = @answer.question end diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 24036ea..f23fa9d 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -29,12 +29,12 @@ def create end def update - return head :forbidden unless current_user.owned?(@question) + return head :forbidden unless current_user.owner?(@question) @question.update(question_params) end def destroy - return head :forbidden unless current_user.owned?(@question) + return head :forbidden unless current_user.owner?(@question) @question.destroy redirect_to questions_path, notice: "Your question was successfully deleted!" end diff --git a/app/models/user.rb b/app/models/user.rb index 58afc9d..dffc397 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,7 +8,7 @@ class User < ApplicationRecord :rememberable, :validatable - def owned?(content) + def owner?(content) content.user_id == id end end diff --git a/app/views/questions/_question_for_show.html.slim b/app/views/questions/_question_for_show.html.slim index 7af2f0f..fe7272f 100644 --- a/app/views/questions/_question_for_show.html.slim +++ b/app/views/questions/_question_for_show.html.slim @@ -1,6 +1,6 @@ h4= @question.title p= @question.body -=render(partial: 'shared/question_links', locals: { question: @question }) if current_user&.owned?(@question) +=render(partial: 'shared/question_links', locals: { question: @question }) if current_user&.owner?(@question) .question-errors =render 'shared/errors', resource: @question = form_with model: @question, class: 'hidden', html: { id: "edit-question-#{@question.id}" } do |f| diff --git a/app/views/shared/_answer_links.html.slim b/app/views/shared/_answer_links.html.slim index 0242f10..6d20456 100644 --- a/app/views/shared/_answer_links.html.slim +++ b/app/views/shared/_answer_links.html.slim @@ -1,7 +1,7 @@ p => link_to('Mark as the best', mark_answer_path(answer), method: :post, remote: true) \ - if current_user&.owned?(answer.question) && !answer.best? + if current_user&.owner?(answer.question) && !answer.best? => link_to('Edit', '#', class: 'edit-answer-links', data: { answer_id: answer.id }) \ - if current_user&.owned?(answer) + if current_user&.owner?(answer) = link_to('Delete', answer_path(answer), method: :delete, remote: true) \ - if current_user&.owned?(answer) + if current_user&.owner?(answer) diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index 2a431a7..8ba9b96 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -43,7 +43,7 @@ format: :js created_answer = question1.answers.find_by! new_answer_params - expect(user1).to be_owned(created_answer) + expect(user1).to be_owner(created_answer) end it 'renders create template', js: true do From dfd6a7a17b765428d057c5eff069ec5dc3bc21a7 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Tue, 25 Jun 2019 18:16:19 +0300 Subject: [PATCH 17/19] Fix User model spec. --- spec/models/user_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e992a2b..0f247f3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -7,18 +7,18 @@ it { should validate_presence_of :email } it { should validate_presence_of :password } - describe '#owned?' do + describe '#owner?' do let(:user1) { create(:user) } let(:user2) { create(:user) } let(:question1) { create(:question, user: user1) } let(:question2) { create(:question, user: user2) } it 'check if user is owner of resource created by himself' do - expect(user1).to be_owned(question1) + expect(user1).to be_owner(question1) end it 'check if user is not owner of resource created by another user' do - expect(user1).to_not be_owned(question2) + expect(user1).to_not be_owner(question2) end end end From 6573a526d17415a880f41c3d93166ab3408367a2 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Wed, 26 Jun 2019 00:01:47 +0300 Subject: [PATCH 18/19] Fix. --- spec/controllers/answers_controller_spec.rb | 97 ++++++++++++--------- spec/models/answer_spec.rb | 8 +- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index 8ba9b96..e4a78c7 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -19,65 +19,80 @@ end describe 'POST #create' do - before { login(user1) } + context 'authenticated user' do + before { login(user1) } - context 'with valid attributes' do - it 'saves a new answer in the database' do - expect { + context 'with valid attributes' do + it 'saves a new answer in the database' do + expect { + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer) + }, + format: :js + }.to change(question1.answers, :count).by(1) + end + + it 'creates answer by the name of logged user' do + new_answer_params = attributes_for(:answer) post :create, params: { question_id: question1.id, - answer: attributes_for(:answer) + answer: new_answer_params }, format: :js - }.to change(question1.answers, :count).by(1) - end + created_answer = question1.answers.find_by! new_answer_params - it 'creates answer by the name of logged user' do - new_answer_params = attributes_for(:answer) - post :create, - params: { - question_id: question1.id, - answer: new_answer_params - }, - format: :js - created_answer = question1.answers.find_by! new_answer_params + expect(user1).to be_owner(created_answer) + end + + it 'renders create template', js: true do + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer) + }, + format: :js - expect(user1).to be_owner(created_answer) + expect(response).to render_template 'create' + end end - it 'renders create template', js: true do - post :create, + context 'with invalid attributes' do + it 'does not save a new answer with short body in the database' do + expect { + post :create, + params: { + question_id: question1.id, + answer: attributes_for(:answer, :invalid) + }, + format: :js + }.to_not change(Answer, :count) + end + + it 'renders create template' do + post :create, params: { question_id: question1.id, - answer: attributes_for(:answer) + answer: attributes_for(:answer, :invalid) }, format: :js - - expect(response).to render_template 'create' + expect(response).to render_template 'create' + end end end - context 'with invalid attributes' do - it 'does not save a new answer with short body in the database' do + context 'unauthenticated user' do + it 'tries to create an answer' do expect { post :create, params: { question_id: question1.id, - answer: attributes_for(:answer, :invalid) + answer: attributes_for(:answer) }, format: :js - }.to_not change(Answer, :count) - end - - it 'renders create template' do - post :create, - params: { - question_id: question1.id, - answer: attributes_for(:answer, :invalid) - }, - format: :js - expect(response).to render_template 'create' + }.to_not change(question1.answers, :count) end end end @@ -110,12 +125,10 @@ end end - context 'authenticated user' do - it "tries to delete an answer" do - expect { - delete :destroy, params: { id: answer2 }, format: :js - }.to_not change(Answer, :count) - end + it "authenticated user tries to delete an answer" do + expect { + delete :destroy, params: { id: answer2 }, format: :js + }.to_not change(Answer, :count) end end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index d0b25d9..348157e 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -11,10 +11,10 @@ let(:answer1) { create(:answer, question: question, user: user) } let(:answer2) { create(:answer, question: question, user: user) } - before { answer1.mark_as_best } - describe '#mark_best' do it 'marks one answer as the best' do + answer1.mark_as_best + expect(answer1).to be_best expect(answer2).to_not be_best end @@ -22,7 +22,9 @@ describe 'default_scope order' do it 'sorting answers with the best as the first' do - expect(answer1.question.answers.first).to eq answer1 + answer1.mark_as_best + + expect(answer1.question.answers).to eq [answer1, answer2] end end end From 3cd0a05f7fda8995bc7f7b60d67879dbc71dce07 Mon Sep 17 00:00:00 2001 From: Oleg Sachev Date: Wed, 26 Jun 2019 15:06:49 +0300 Subject: [PATCH 19/19] Update Answers controller POST #mark tests. --- spec/controllers/answers_controller_spec.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/controllers/answers_controller_spec.rb b/spec/controllers/answers_controller_spec.rb index e4a78c7..3bf1d07 100644 --- a/spec/controllers/answers_controller_spec.rb +++ b/spec/controllers/answers_controller_spec.rb @@ -206,14 +206,12 @@ let!(:answer3) { create(:answer, question: question1, user: user2) } context 'authenticated user' do - before { login(user1) } + before do + login(user1) + answer2.best = true + end it 'marks answers to his question' do - post :mark, params: { id: answer2 }, format: :js - answer2.reload - - expect(answer2).to be_best - post :mark, params: { id: answer3 }, format: :js answer2.reload answer3.reload @@ -225,7 +223,7 @@ it "tries to mark an answer to another user's question" do post :mark, params: { id: answer1 }, format: :js - expect(answer2).to_not be_best + expect(answer1).to_not be_best end end