diff --git a/.rubocop.yml b/.rubocop.yml index ca3696b9b..6f0d769a4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -571,6 +571,9 @@ RSpec/BeNil: Enabled: true EnforcedStyle: be +RSpec/ChangeByZero: + Enabled: true + RSpec/ContextMethod: Enabled: true diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 8700b6ef0..09b22bd4f 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -1396,8 +1396,7 @@ position: relative; } -.public .poll, -.poll-question { +.public .poll { border: 1px solid $border; margin-bottom: calc($line-height / 2); padding: calc($line-height / 2); @@ -1413,47 +1412,6 @@ } } -.poll-question { - padding: 0 $line-height; - - h3 { - padding-top: $line-height; - } -} - -.poll-question-options { - @include flex-with-gap($line-height * 0.25); - flex-wrap: wrap; - - .button { - min-width: rem-calc(168); - - @include breakpoint(medium down) { - width: 100%; - } - - &.answered { - background: #f4f8ec; - border: 2px solid #92ba48; - color: color-pick-contrast(#f4f8ec); - position: relative; - - &::after { - background: #92ba48; - border-radius: rem-calc(20); - color: #fff; - content: "\6c"; - font-family: "icons" !important; - font-size: rem-calc(12); - padding: calc($line-height / 4); - position: absolute; - right: -6px; - top: -6px; - } - } - } -} - // 09. Polls results and stats // --------------------------- diff --git a/app/assets/stylesheets/polls/form.scss b/app/assets/stylesheets/polls/form.scss new file mode 100644 index 000000000..e85879c24 --- /dev/null +++ b/app/assets/stylesheets/polls/form.scss @@ -0,0 +1,29 @@ +.poll-form { + label { + @include radio-or-checkbox-and-label-alignment; + font-weight: normal; + + &:first-of-type { + margin-top: calc($line-height / 2); + } + } + + fieldset + fieldset { + margin-top: calc($line-height / 2); + } + + legend { + @include header-font-size(h3); + margin-bottom: 0; + } + + .help-text { + font-size: 1em; + font-style: normal; + font-weight: bold; + } + + [type=submit] { + margin-top: calc($line-height * 3 / 4); + } +} diff --git a/app/components/polls/form_component.html.erb b/app/components/polls/form_component.html.erb index 6516b9152..f3d83701b 100644 --- a/app/components/polls/form_component.html.erb +++ b/app/components/polls/form_component.html.erb @@ -1,3 +1,7 @@ -<% questions.each do |question| %> - <%= render Polls::Questions::QuestionComponent.new(question) %> +<%= form_for poll, form_attributes do |f| %> + <% questions.each do |question| %> + <%= render Polls::Questions::QuestionComponent.new(question, disabled: disabled?) %> + <% end %> + + <%= f.submit(class: "button", value: t("polls.form.vote"), disabled: disabled?) %> <% end %> diff --git a/app/components/polls/form_component.rb b/app/components/polls/form_component.rb index 0a2bc5668..48dafc51f 100644 --- a/app/components/polls/form_component.rb +++ b/app/components/polls/form_component.rb @@ -1,7 +1,19 @@ class Polls::FormComponent < ApplicationComponent - attr_reader :questions + attr_reader :web_vote + use_helpers :cannot?, :current_user + delegate :poll, :questions, to: :web_vote - def initialize(questions) - @questions = questions + def initialize(web_vote) + @web_vote = web_vote end + + private + + def form_attributes + { url: answer_poll_path(poll), method: :post, html: { class: "poll-form" }} + end + + def disabled? + cannot?(:answer, poll) || poll.voted_in_booth?(current_user) + end end diff --git a/app/components/polls/questions/options_component.html.erb b/app/components/polls/questions/options_component.html.erb deleted file mode 100644 index a183b2aac..000000000 --- a/app/components/polls/questions/options_component.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -
- <% if can?(:answer, question) && !question.poll.voted_in_booth?(current_user) %> - <% question_options.each do |question_option| %> - <% if already_answered?(question_option) %> - <%= button_to question_option.title, - question_answer_path(question, user_answer(question_option)), - method: :delete, - remote: true, - title: t("poll_questions.show.voted", answer: question_option.title), - class: "button answered", - "aria-pressed": true %> - <% else %> - <%= button_to question_option.title, - question_answers_path(question, option_id: question_option.id), - remote: true, - title: t("poll_questions.show.vote_answer", answer: question_option.title), - class: "button secondary hollow", - "aria-pressed": false, - disabled: disable_option?(question_option) %> - <% end %> - <% end %> - <% elsif !user_signed_in? %> - <% question_options.each do |question_option| %> - <%= link_to question_option.title, new_user_session_path, class: "button secondary hollow" %> - <% end %> - <% elsif !current_user.level_two_or_three_verified? %> - <% question_options.each do |question_option| %> - <%= link_to question_option.title, verification_path, class: "button secondary hollow" %> - <% end %> - <% else %> - <% question_options.each do |question_option| %> - <%= question_option.title %> - <% end %> - <% end %> -
diff --git a/app/components/polls/questions/options_component.rb b/app/components/polls/questions/options_component.rb deleted file mode 100644 index 25875cd6e..000000000 --- a/app/components/polls/questions/options_component.rb +++ /dev/null @@ -1,30 +0,0 @@ -class Polls::Questions::OptionsComponent < ApplicationComponent - attr_reader :question - use_helpers :can?, :current_user, :user_signed_in? - - def initialize(question) - @question = question - end - - def already_answered?(question_option) - user_answer(question_option).present? - end - - def question_options - question.question_options - end - - def user_answer(question_option) - user_answers.find_by(answer: question_option.title) - end - - def disable_option?(question_option) - question.multiple? && user_answers.count == question.max_votes - end - - private - - def user_answers - @user_answers ||= question.answers.by_author(current_user) - end -end diff --git a/app/components/polls/questions/question_component.html.erb b/app/components/polls/questions/question_component.html.erb index 46563a87b..89b90f9bd 100644 --- a/app/components/polls/questions/question_component.html.erb +++ b/app/components/polls/questions/question_component.html.erb @@ -1,22 +1,22 @@ -
-

- <%= question.title %> -

+
> + <%= question.title %> - <% if question.votation_type.present? %> - - <%= t("poll_questions.description.#{question.vote_type}", maximum: question.max_votes) %> - + <% if multiple_choice? %> + <%= multiple_choice_help_text %> + + <% question.question_options.each do |option| %> + <%= multiple_choice_field(option) %> + <% end %> + <% else %> + <% question.question_options.each do |option| %> + <%= single_choice_field(option) %> + <% end %> <% end %> -
- <%= render Polls::Questions::OptionsComponent.new(question) %> -
- <% if question.options_with_read_more? %>

<%= t("poll_questions.read_more_about") %>

<%= options_read_more_links %>

<% end %> -
+ diff --git a/app/components/polls/questions/question_component.rb b/app/components/polls/questions/question_component.rb index 627abc6e9..6961ca412 100644 --- a/app/components/polls/questions/question_component.rb +++ b/app/components/polls/questions/question_component.rb @@ -1,13 +1,65 @@ class Polls::Questions::QuestionComponent < ApplicationComponent - attr_reader :question + attr_reader :question, :disabled + alias_method :disabled?, :disabled + use_helpers :current_user - def initialize(question) + def initialize(question, disabled: false) @question = question + @disabled = disabled end - def options_read_more_links - safe_join(question.options_with_read_more.map do |option| - link_to option.title, "#option_#{option.id}" - end, ", ") - end + private + + def fieldset_attributes + tag.attributes( + id: dom_id(question), + disabled: ("disabled" if disabled?), + data: { max_votes: question.max_votes } + ) + end + + def options_read_more_links + safe_join(question.options_with_read_more.map do |option| + link_to option.title, "#option_#{option.id}" + end, ", ") + end + + def multiple_choice? + question.multiple? + end + + def multiple_choice_help_text + tag.span( + t("poll_questions.description.multiple", maximum: question.max_votes), + class: "help-text" + ) + end + + def multiple_choice_field(option) + choice_field(option) do + check_box_tag "web_vote[#{question.id}][option_id][]", + option.id, + checked?(option), + id: "web_vote_option_#{option.id}" + end + end + + def single_choice_field(option) + choice_field(option) do + radio_button_tag "web_vote[#{question.id}][option_id]", + option.id, + checked?(option), + id: "web_vote_option_#{option.id}" + end + end + + def choice_field(option, &block) + label_tag("web_vote_option_#{option.id}") do + block.call + option.title + end + end + + def checked?(option) + question.answers.where(author: current_user, option: option).any? + end end diff --git a/app/controllers/polls/answers_controller.rb b/app/controllers/polls/answers_controller.rb deleted file mode 100644 index a969c5edf..000000000 --- a/app/controllers/polls/answers_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Polls::AnswersController < ApplicationController - load_and_authorize_resource :question, class: "::Poll::Question" - load_and_authorize_resource :answer, class: "::Poll::Answer", - through: :question, - through_association: :answers, - only: :destroy - - def create - authorize! :answer, @question - - answer = @question.find_or_initialize_user_answer(current_user, params[:option_id]) - answer.save_and_record_voter_participation - - respond_to do |format| - format.html do - redirect_to request.referer - end - format.js do - render :show - end - end - end - - def destroy - @answer.destroy_and_remove_voter_participation - - respond_to do |format| - format.html do - redirect_to request.referer - end - format.js do - render :show - end - end - end -end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index 80b72309b..f32332f3c 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -9,7 +9,7 @@ class PollsController < ApplicationController load_and_authorize_resource has_filters %w[current expired] - has_orders %w[most_voted newest oldest], only: :show + has_orders %w[most_voted newest oldest], only: [:show, :answer] def index @polls = Kaminari.paginate_array( @@ -18,10 +18,21 @@ class PollsController < ApplicationController end def show - @questions = @poll.questions.for_render.sort_for_list + @web_vote = Poll::WebVote.new(@poll, current_user) @comment_tree = CommentTree.new(@poll, params[:page], @current_order) end + def answer + @web_vote = Poll::WebVote.new(@poll, current_user) + + if @web_vote.update(answer_params) + redirect_to @poll, notice: t("flash.actions.create.poll_voter") + else + @comment_tree = CommentTree.new(@poll, params[:page], @current_order) + render :show + end + end + def stats @stats = Poll::Stats.new(@poll).tap(&:generate) end @@ -38,4 +49,8 @@ class PollsController < ApplicationController def load_active_poll @active_poll = ActivePoll.first end + + def answer_params + params[:web_vote] || {} + end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 369630981..429ff7ef7 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -110,12 +110,6 @@ module Abilities can :answer, Poll do |poll| poll.answerable_by?(user) end - can :answer, Poll::Question do |question| - question.answerable_by?(user) - end - can :destroy, Poll::Answer do |answer| - answer.author == user && answer.question.answerable_by?(user) - end end can [:create, :show], ProposalNotification, proposal: { author_id: user.id } diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index b9c52fb38..d4b0150e2 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -17,32 +17,13 @@ class Poll::Answer < ApplicationRecord scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } - def save_and_record_voter_participation - author.with_lock do - save! - Poll::Voter.find_or_create_by!(user: author, poll: poll, origin: "web") - end - end - - def destroy_and_remove_voter_participation - transaction do - destroy! - - if author.poll_answers.where(question_id: poll.question_ids).none? - Poll::Voter.find_by(user: author, poll: poll, origin: "web").destroy! - end - end - end - private def max_votes return if !question || !author || persisted? - author.with_lock do - if question.answers.by_author(author).count >= question.max_votes - errors.add(:answer, "Maximum number of votes per user exceeded") - end + if question.answers.by_author(author).count >= question.max_votes + errors.add(:answer, "Maximum number of votes per user exceeded") end end end diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index c11d8c654..93e9c4453 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -41,8 +41,6 @@ class Poll::Question < ApplicationRecord end end - delegate :answerable_by?, to: :poll - def options_total_votes question_options.reduce(0) { |total, question_option| total + question_option.total_votes } end diff --git a/app/models/poll/web_vote.rb b/app/models/poll/web_vote.rb new file mode 100644 index 000000000..2dba50fe5 --- /dev/null +++ b/app/models/poll/web_vote.rb @@ -0,0 +1,43 @@ +class Poll::WebVote + attr_reader :poll, :user + + def initialize(poll, user) + @poll = poll + @user = user + end + + def questions + poll.questions.for_render.sort_for_list + end + + def update(params) + all_valid = true + + user.with_lock do + unless questions.any? { |question| params.dig(question.id.to_s, :option_id).present? } + Poll::Voter.find_by(user: user, poll: poll, origin: "web")&.destroy! + end + + questions.each do |question| + question.answers.where(author: user).destroy_all + next unless params[question.id.to_s] + + option_ids = params[question.id.to_s][:option_id] + + answers = Array(option_ids).map do |option_id| + question.find_or_initialize_user_answer(user, option_id) + end + + if answers.map(&:valid?).all?(true) + Poll::Voter.find_or_create_by!(user: user, poll: poll, origin: "web") + answers.each(&:save!) + else + all_valid = false + raise ActiveRecord::Rollback + end + end + end + + all_valid + end +end diff --git a/app/views/polls/answers/show.js.erb b/app/views/polls/answers/show.js.erb deleted file mode 100644 index 1cfab79f7..000000000 --- a/app/views/polls/answers/show.js.erb +++ /dev/null @@ -1 +0,0 @@ -$("#<%= dom_id(@question) %>_options").html("<%= j render Polls::Questions::OptionsComponent.new(@question) %>"); diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb index 808ae2816..79d010a63 100644 --- a/app/views/polls/show.html.erb +++ b/app/views/polls/show.html.erb @@ -31,7 +31,7 @@ <% end %> <% end %> - <%= render Polls::FormComponent.new(@questions) %> + <%= render Polls::FormComponent.new(@web_vote) %> @@ -46,7 +46,7 @@
- <%= render Polls::Questions::ReadMoreComponent.with_collection(@questions) %> + <%= render Polls::Questions::ReadMoreComponent.with_collection(@web_vote.questions) %>
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 0c0745e00..d48184f08 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -577,6 +577,8 @@ en: polls: dates: "From %{open_at} to %{closed_at}" final_date: "Final recounts/Results" + form: + vote: "Vote" index: filters: current: "Open" @@ -634,11 +636,7 @@ en: poll_header: back_to_proposal: Back to proposal poll_questions: - show: - vote_answer: "Vote %{answer}" - voted: "You have voted %{answer}" description: - unique: "You can select a maximum of 1 answer." multiple: "You can select a maximum of %{maximum} answers." read_more_about: "Read more about:" proposal_notifications: diff --git a/config/locales/en/responders.yml b/config/locales/en/responders.yml index 550a5b9f1..dc5527d48 100644 --- a/config/locales/en/responders.yml +++ b/config/locales/en/responders.yml @@ -10,6 +10,7 @@ en: poll_question_option: "Answer created successfully" poll_question_option_video: "Video created successfully" poll_question_option_image: "Image uploaded successfully" + poll_voter: "Thank you for voting!" proposal: "Proposal created successfully." proposal_notification: "Your message has been sent correctly." budget_investment: "Budget Investment created successfully." diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index db8622d81..0ee5caf11 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -577,6 +577,8 @@ es: polls: dates: "Desde el %{open_at} hasta el %{closed_at}" final_date: "Recuento final/Resultados" + form: + vote: "Votar" index: filters: current: "Abiertas" @@ -634,11 +636,7 @@ es: poll_header: back_to_proposal: Volver a la propuesta poll_questions: - show: - vote_answer: "Votar %{answer}" - voted: "Has votado %{answer}" description: - unique: "Puedes seleccionar un máximo de 1 respuesta." multiple: "Puedes seleccionar un máximo de %{maximum} respuestas." read_more_about: "Leer más:" proposal_notifications: diff --git a/config/locales/es/responders.yml b/config/locales/es/responders.yml index 49a78fad1..50fddc758 100644 --- a/config/locales/es/responders.yml +++ b/config/locales/es/responders.yml @@ -10,6 +10,7 @@ es: poll_question_option: "Respuesta creada correctamente" poll_question_option_video: "Vídeo creado correctamente" poll_question_option_image: "Imagen cargada correctamente" + poll_voter: "¡Gracias por votar!" proposal: "Propuesta creada correctamente." proposal_notification: "Tu mensaje ha sido enviado correctamente." budget_investment: "Proyecto de gasto creado correctamente." diff --git a/config/routes/poll.rb b/config/routes/poll.rb index 754c7c4d2..9c44c0f21 100644 --- a/config/routes/poll.rb +++ b/config/routes/poll.rb @@ -2,9 +2,6 @@ resources :polls, only: [:show, :index] do member do get :stats get :results - end - - resources :questions, controller: "polls/questions", shallow: true, only: [] do - resources :answers, controller: "polls/answers", only: [:create, :destroy], shallow: false + post :answer end end diff --git a/spec/components/polls/form_component_spec.rb b/spec/components/polls/form_component_spec.rb new file mode 100644 index 000000000..754b7f72e --- /dev/null +++ b/spec/components/polls/form_component_spec.rb @@ -0,0 +1,70 @@ +require "rails_helper" + +describe Polls::FormComponent do + let(:user) { create(:user, :level_two) } + let(:poll) { create(:poll) } + let(:web_vote) { Poll::WebVote.new(poll, user) } + before { create(:poll_question, :yes_no, poll: poll) } + + it "renders disabled fields when the user has already voted in a booth" do + create(:poll_voter, :from_booth, poll: poll, user: user) + sign_in(user) + + render_inline Polls::FormComponent.new(web_vote) + + page.find("fieldset[disabled]") do |fieldset| + expect(fieldset).to have_field "Yes" + expect(fieldset).to have_field "No" + end + + expect(page).to have_button "Vote", disabled: true + end + + context "expired poll" do + let(:poll) { create(:poll, :expired) } + + it "renders disabled fields when the poll has expired" do + sign_in(user) + + render_inline Polls::FormComponent.new(web_vote) + + page.find("fieldset[disabled]") do |fieldset| + expect(fieldset).to have_field "Yes" + expect(fieldset).to have_field "No" + end + + expect(page).to have_button "Vote", disabled: true + end + end + + context "geozone restricted poll" do + let(:poll) { create(:poll, geozone_restricted: true) } + let(:geozone) { create(:geozone) } + + it "renders disabled fields for users from another geozone" do + poll.geozones << geozone + sign_in(user) + + render_inline Polls::FormComponent.new(web_vote) + + page.find("fieldset[disabled]") do |fieldset| + expect(fieldset).to have_field "Yes" + expect(fieldset).to have_field "No" + end + + expect(page).to have_button "Vote", disabled: true + end + + it "renders enabled fields for same-geozone users" do + poll.geozones << geozone + sign_in(create(:user, :level_two, geozone: geozone)) + + render_inline Polls::FormComponent.new(web_vote) + + expect(page).not_to have_css "fieldset[disabled]" + expect(page).to have_field "Yes" + expect(page).to have_field "No" + expect(page).to have_button "Vote" + end + end +end diff --git a/spec/components/polls/questions/options_component_spec.rb b/spec/components/polls/questions/options_component_spec.rb deleted file mode 100644 index c50416cd5..000000000 --- a/spec/components/polls/questions/options_component_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require "rails_helper" - -describe Polls::Questions::OptionsComponent do - include Rails.application.routes.url_helpers - let(:poll) { create(:poll) } - let(:question) { create(:poll_question, :yes_no, poll: poll) } - - it "renders answers in given order" do - render_inline Polls::Questions::OptionsComponent.new(question) - - expect("Yes").to appear_before("No") - end - - it "renders buttons to vote question answers" do - sign_in(create(:user, :verified)) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_button "Yes" - expect(page).to have_button "No" - expect(page).to have_css "button[aria-pressed='false']", count: 2 - end - - it "renders button to destroy current user answers" do - user = create(:user, :verified) - create(:poll_answer, author: user, question: question, answer: "Yes") - sign_in(user) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_button "You have voted Yes" - expect(page).to have_button "Vote No" - expect(page).to have_css "button[aria-pressed='true']", text: "Yes" - end - - it "renders disabled buttons when max votes is reached" do - user = create(:user, :verified) - question = create(:poll_question_multiple, :abc, max_votes: 2, author: user) - create(:poll_answer, author: user, question: question, answer: "Answer A") - create(:poll_answer, author: user, question: question, answer: "Answer C") - sign_in(user) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_button "You have voted Answer A" - expect(page).to have_button "Vote Answer B", disabled: true - expect(page).to have_button "You have voted Answer C" - end - - it "when user is not signed in, renders answers links pointing to user sign in path" do - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_link "Yes", href: new_user_session_path - expect(page).to have_link "No", href: new_user_session_path - end - - it "when user is not verified, renders answers links pointing to user verification in path" do - sign_in(create(:user)) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_link "Yes", href: verification_path - expect(page).to have_link "No", href: verification_path - end - - it "when user already voted in booth it renders disabled answers" do - user = create(:user, :level_two) - create(:poll_voter, :from_booth, poll: poll, user: user) - sign_in(user) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_css "span.disabled", text: "Yes" - expect(page).to have_css "span.disabled", text: "No" - end - - it "user cannot vote when poll expired it renders disabled answers" do - question = create(:poll_question, :yes_no, poll: create(:poll, :expired)) - sign_in(create(:user, :level_two)) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_css "span.disabled", text: "Yes" - expect(page).to have_css "span.disabled", text: "No" - end - - describe "geozone" do - let(:poll) { create(:poll, geozone_restricted: true) } - let(:geozone) { create(:geozone) } - let(:question) { create(:poll_question, :yes_no, poll: poll) } - - it "when geozone which is not theirs it renders disabled answers" do - poll.geozones << geozone - sign_in(create(:user, :level_two)) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_css "span.disabled", text: "Yes" - expect(page).to have_css "span.disabled", text: "No" - end - - it "reading a same-geozone poll it renders buttons to vote question answers" do - poll.geozones << geozone - sign_in(create(:user, :level_two, geozone: geozone)) - - render_inline Polls::Questions::OptionsComponent.new(question) - - expect(page).to have_button "Yes" - expect(page).to have_button "No" - end - end -end diff --git a/spec/components/polls/questions/question_component_spec.rb b/spec/components/polls/questions/question_component_spec.rb index aba98dd94..e03851222 100644 --- a/spec/components/polls/questions/question_component_spec.rb +++ b/spec/components/polls/questions/question_component_spec.rb @@ -1,18 +1,83 @@ require "rails_helper" describe Polls::Questions::QuestionComponent do + let(:poll) { create(:poll) } + let(:question) { create(:poll_question, :yes_no, poll: poll) } + let(:option_yes) { question.question_options.find_by(title: "Yes") } + let(:option_no) { question.question_options.find_by(title: "No") } + it "renders more information links when any question option has additional information" do - question = create(:poll_question) - option_a = create(:poll_question_option, question: question, title: "Answer A") - option_b = create(:poll_question_option, question: question, title: "Answer B") allow_any_instance_of(Poll::Question::Option).to receive(:with_read_more?).and_return(true) render_inline Polls::Questions::QuestionComponent.new(question) - poll_question = page.find("#poll_question_#{question.id}") - expect(poll_question).to have_content("Read more about") - expect(poll_question).to have_link("Answer A", href: "#option_#{option_a.id}") - expect(poll_question).to have_link("Answer B", href: "#option_#{option_b.id}") - expect(poll_question).to have_content("Answer A, Answer B") + page.find("#poll_question_#{question.id}") do |poll_question| + expect(poll_question).to have_content "Read more about" + expect(poll_question).to have_link "Yes", href: "#option_#{option_yes.id}" + expect(poll_question).to have_link "No", href: "#option_#{option_no.id}" + expect(poll_question).to have_content "Yes, No" + end + end + + it "renders answers in given order" do + render_inline Polls::Questions::QuestionComponent.new(question) + + expect("Yes").to appear_before("No") + end + + it "renders disabled answers when given the disabled parameter" do + render_inline Polls::Questions::QuestionComponent.new(question, disabled: true) + + page.find("fieldset[disabled]") do |fieldset| + expect(fieldset).to have_field "Yes" + expect(fieldset).to have_field "No" + end + end + + skip "disables fields when maximum votes has been reached" do # TODO: requires JavaScript + user = create(:user, :verified) + question = create(:poll_question_multiple, :abc, max_votes: 2, author: user) + + create(:poll_answer, author: user, question: question, answer: "Answer A") + create(:poll_answer, author: user, question: question, answer: "Answer C") + sign_in(user) + + render_inline Polls::Questions::QuestionComponent.new(question) + + expect(page).to have_field "Answer A", type: :checkbox, checked: true + expect(page).to have_field "Answer B", type: :checkbox, checked: false + expect(page).to have_field "Answer C", type: :checkbox, checked: true + end + + context "Verified user" do + let(:user) { create(:user, :level_two) } + before { sign_in(user) } + + it "renders radio buttons for single-choice questions" do + render_inline Polls::Questions::QuestionComponent.new(question) + + expect(page).to have_field "Yes", type: :radio + expect(page).to have_field "No", type: :radio + expect(page).to have_field type: :radio, checked: false, count: 2 + end + + it "renders checkboxes for multiple-choice questions" do + render_inline Polls::Questions::QuestionComponent.new(create(:poll_question_multiple, :abc)) + + expect(page).to have_field "Answer A", type: :checkbox + expect(page).to have_field "Answer B", type: :checkbox + expect(page).to have_field "Answer C", type: :checkbox + expect(page).to have_field type: :checkbox, checked: false, count: 3 + expect(page).not_to have_field type: :checkbox, checked: true + end + + it "selects the option when users have already voted" do + create(:poll_answer, author: user, question: question, option: option_yes) + + render_inline Polls::Questions::QuestionComponent.new(question) + + expect(page).to have_field "Yes", type: :radio, checked: true + expect(page).to have_field "No", type: :radio, checked: false + end end end diff --git a/spec/controllers/polls/answers_controller_spec.rb b/spec/controllers/polls/answers_controller_spec.rb deleted file mode 100644 index a5286da3c..000000000 --- a/spec/controllers/polls/answers_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "rails_helper" - -describe Polls::AnswersController do - describe "POST create" do - it "doesn't create duplicate records on simultaneous requests", :race_condition do - question = create(:poll_question_multiple, :abc) - sign_in(create(:user, :level_two)) - - 2.times.map do - Thread.new do - post :create, params: { - question_id: question.id, - option_id: question.question_options.find_by(title: "Answer A").id, - format: :js - } - rescue ActiveRecord::RecordInvalid - end - end.each(&:join) - - expect(Poll::Answer.count).to eq 1 - end - end -end diff --git a/spec/controllers/polls_controller_spec.rb b/spec/controllers/polls_controller_spec.rb index 296c1a0d6..6a0a03c2e 100644 --- a/spec/controllers/polls_controller_spec.rb +++ b/spec/controllers/polls_controller_spec.rb @@ -8,4 +8,25 @@ describe PollsController do expect { get :index }.to raise_exception(FeatureFlags::FeatureDisabled) end end + + describe "POST answer" do + it "doesn't create duplicate records on simultaneous requests", :race_condition do + question = create(:poll_question_multiple, :abc) + sign_in(create(:user, :level_two)) + + 2.times.map do + Thread.new do + post :answer, params: { + id: question.poll.id, + web_vote: { + question.id.to_s => { option_id: question.question_options.find_by(title: "Answer A").id } + } + } + rescue AbstractController::DoubleRenderError + end + end.each(&:join) + + expect(Poll::Answer.count).to eq 1 + end + end end diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index f79295d13..3aa184bf7 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -51,18 +51,6 @@ describe Abilities::Common do let(:poll_from_own_geozone) { create(:poll, geozone_restricted_to: [geozone]) } let(:poll_from_other_geozone) { create(:poll, geozone_restricted_to: [create(:geozone)]) } - let(:poll_question_from_own_geozone) { create(:poll_question, poll: poll_from_own_geozone) } - let(:poll_question_from_other_geozone) { create(:poll_question, poll: poll_from_other_geozone) } - let(:poll_question_from_all_geozones) { create(:poll_question, poll: poll) } - - let(:expired_poll_question_from_own_geozone) do - create(:poll_question, poll: expired_poll_from_own_geozone) - end - let(:expired_poll_question_from_other_geozone) do - create(:poll_question, poll: expired_poll_from_other_geozone) - end - let(:expired_poll_question_from_all_geozones) { create(:poll_question, poll: expired_poll) } - let(:own_proposal_document) { build(:document, documentable: own_proposal) } let(:proposal_document) { build(:document, documentable: proposal) } let(:own_budget_investment_document) { build(:document, documentable: own_investment_in_accepting_budget) } @@ -252,39 +240,25 @@ describe Abilities::Common do end describe "Poll" do - it { should be_able_to(:answer, current_poll) } - it { should_not be_able_to(:answer, expired_poll) } + it { should be_able_to(:answer, current_poll) } + it { should_not be_able_to(:answer, expired_poll) } - it { should be_able_to(:answer, poll_question_from_own_geozone) } - it { should be_able_to(:answer, poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + it { should be_able_to(:answer, poll_from_own_geozone) } + it { should be_able_to(:answer, poll) } + it { should_not be_able_to(:answer, poll_from_other_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } - - context "Poll::Answer" do - let(:own_answer) { create(:poll_answer, author: user) } - let(:other_user_answer) { create(:poll_answer) } - let(:expired_poll) { create(:poll, :expired) } - let(:question) { create(:poll_question, :yes_no, poll: expired_poll) } - let(:expired_poll_answer) { create(:poll_answer, author: user, question: question, answer: "Yes") } - - it { should be_able_to(:destroy, own_answer) } - it { should_not be_able_to(:destroy, other_user_answer) } - it { should_not be_able_to(:destroy, expired_poll_answer) } - end + it { should_not be_able_to(:answer, expired_poll_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_other_geozone) } context "without geozone" do before { user.geozone = nil } - it { should_not be_able_to(:answer, poll_question_from_own_geozone) } - it { should be_able_to(:answer, poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + it { should_not be_able_to(:answer, poll_from_own_geozone) } + it { should be_able_to(:answer, poll) } + it { should_not be_able_to(:answer, poll_from_other_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_other_geozone) } end end @@ -339,26 +313,25 @@ describe Abilities::Common do it { should be_able_to(:show, own_direct_message) } it { should_not be_able_to(:show, create(:direct_message)) } - it { should be_able_to(:answer, current_poll) } - it { should_not be_able_to(:answer, expired_poll) } + it { should be_able_to(:answer, current_poll) } + it { should_not be_able_to(:answer, expired_poll) } - it { should be_able_to(:answer, poll_question_from_own_geozone) } - it { should be_able_to(:answer, poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + it { should be_able_to(:answer, poll_from_own_geozone) } + it { should be_able_to(:answer, poll) } + it { should_not be_able_to(:answer, poll_from_other_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_other_geozone) } context "without geozone" do before { user.geozone = nil } - it { should_not be_able_to(:answer, poll_question_from_own_geozone) } - it { should be_able_to(:answer, poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, poll_question_from_other_geozone) } + it { should_not be_able_to(:answer, poll_from_own_geozone) } + it { should be_able_to(:answer, poll) } + it { should_not be_able_to(:answer, poll_from_other_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone) } - it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones) } - it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone) } + it { should_not be_able_to(:answer, expired_poll_from_own_geozone) } + it { should_not be_able_to(:answer, expired_poll) } + it { should_not be_able_to(:answer, expired_poll_from_other_geozone) } end end diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb index 4f3ca154e..aec882fc5 100644 --- a/spec/models/poll/answer_spec.rb +++ b/spec/models/poll/answer_spec.rb @@ -101,136 +101,5 @@ describe Poll::Answer do expect(build(:poll_answer, question: question, answer: "Four")).not_to be_valid end - - context "creating answers at the same time", :race_condition do - it "validates max votes on single-answer questions" do - author = create(:user) - question = create(:poll_question, :yes_no) - - answer = build(:poll_answer, author: author, question: question, answer: "Yes") - other_answer = build(:poll_answer, author: author, question: question, answer: "No") - - [answer, other_answer].map do |poll_answer| - Thread.new { poll_answer.save } - end.each(&:join) - - expect(Poll::Answer.count).to be 1 - end - - it "validates max votes on multiple-answer questions" do - author = create(:user, :level_two) - question = create(:poll_question_multiple, :abc, max_votes: 2) - create(:poll_answer, question: question, answer: "Answer A", author: author) - answer = build(:poll_answer, question: question, answer: "Answer B", author: author) - other_answer = build(:poll_answer, question: question, answer: "Answer C", author: author) - - [answer, other_answer].map do |poll_answer| - Thread.new { poll_answer.save } - end.each(&:join) - - expect(Poll::Answer.count).to be 2 - end - end - end - - describe "#save_and_record_voter_participation" do - let(:author) { create(:user, :level_two) } - let(:poll) { create(:poll) } - let(:question) { create(:poll_question, :yes_no, poll: poll) } - - it "creates a poll_voter with user and poll data" do - answer = create(:poll_answer, question: question, author: author, answer: "Yes") - expect(answer.poll.voters).to be_blank - - answer.save_and_record_voter_participation - expect(poll.reload.voters.size).to eq(1) - voter = poll.voters.first - - expect(voter.document_number).to eq(answer.author.document_number) - expect(voter.poll_id).to eq(answer.poll.id) - expect(voter.officer_id).to be nil - end - - it "updates a poll_voter with user and poll data" do - answer = create(:poll_answer, question: question, author: author, answer: "Yes") - answer.save_and_record_voter_participation - - expect(poll.reload.voters.size).to eq(1) - - updated_answer = answer.question.find_or_initialize_user_answer( - answer.author, - answer.question.question_options.excluding(answer.option).sample.id - ) - updated_answer.save_and_record_voter_participation - - expect(poll.reload.voters.size).to eq(1) - - voter = poll.voters.first - expect(voter.document_number).to eq(updated_answer.author.document_number) - expect(voter.poll_id).to eq(updated_answer.poll.id) - end - - it "does not save the answer if the voter is invalid" do - allow_any_instance_of(Poll::Voter).to receive(:valid?).and_return(false) - answer = build(:poll_answer) - - expect do - answer.save_and_record_voter_participation - end.to raise_error(ActiveRecord::RecordInvalid) - - expect(answer).not_to be_persisted - end - - it "does not create two voters when creating two answers at the same time", :race_condition do - answer = build(:poll_answer, question: question, author: author, answer: "Yes") - other_answer = build(:poll_answer, question: question, author: author, answer: "No") - - [answer, other_answer].map do |poll_answer| - Thread.new do - poll_answer.save_and_record_voter_participation - rescue ActiveRecord::RecordInvalid - end - end.each(&:join) - - expect(Poll::Voter.count).to be 1 - end - - it "does not create two voters when calling the method twice at the same time", :race_condition do - answer = create(:poll_answer, question: question, author: author, answer: "Yes") - - 2.times.map do - Thread.new { answer.save_and_record_voter_participation } - end.each(&:join) - - expect(Poll::Voter.count).to be 1 - end - end - - describe "#destroy_and_remove_voter_participation" do - let(:poll) { create(:poll) } - let(:question) { create(:poll_question, :yes_no, poll: poll) } - - it "destroys voter record and answer when it was the only user's answer" do - answer = build(:poll_answer, question: question) - answer.save_and_record_voter_participation - - expect { answer.destroy_and_remove_voter_participation } - .to change { Poll::Answer.count }.by(-1) - .and change { Poll::Voter.count }.by(-1) - end - - it "destroys the answer but does not destroy the voter record when the user - has answered other poll questions" do - answer = build(:poll_answer, question: question) - answer.save_and_record_voter_participation - other_question = create(:poll_question, :yes_no, poll: poll) - other_answer = build(:poll_answer, question: other_question, author: answer.author) - other_answer.save_and_record_voter_participation - - expect(other_answer).to be_persisted - expect { answer.destroy_and_remove_voter_participation } - .to change { Poll::Answer.count }.by(-1) - .and change { Poll::Voter.count }.by(0) - end end end diff --git a/spec/models/poll/web_vote_spec.rb b/spec/models/poll/web_vote_spec.rb new file mode 100644 index 000000000..3b4b86eb1 --- /dev/null +++ b/spec/models/poll/web_vote_spec.rb @@ -0,0 +1,110 @@ +require "rails_helper" + +describe Poll::WebVote do + describe "#update" do + let(:user) { create(:user, :level_two) } + let(:poll) { create(:poll) } + let!(:question) { create(:poll_question, :yes_no, poll: poll) } + let(:option_yes) { question.question_options.find_by(title: "Yes") } + let(:option_no) { question.question_options.find_by(title: "No") } + let(:web_vote) { Poll::WebVote.new(poll, user) } + + it "creates a poll_voter with user and poll data" do + expect(poll.voters).to be_blank + expect(question.answers).to be_blank + + web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s }) + + expect(poll.reload.voters.size).to eq 1 + expect(question.reload.answers.size).to eq 1 + + voter = poll.voters.first + answer = question.answers.first + + expect(answer.author).to eq user + expect(voter.document_number).to eq user.document_number + expect(voter.poll_id).to eq answer.poll.id + expect(voter.officer_id).to be nil + end + + it "updates a poll_voter with user and poll data" do + create(:poll_answer, question: question, author: user, option: option_yes) + + web_vote.update(question.id.to_s => { option_id: option_no.id.to_s }) + + expect(poll.reload.voters.size).to eq 1 + expect(question.reload.answers.size).to eq 1 + + voter = poll.voters.first + answer = question.answers.first + + expect(answer.author).to eq user + expect(answer.option).to eq option_no + expect(voter.document_number).to eq answer.author.document_number + expect(voter.poll_id).to eq answer.poll.id + end + + it "does not save the answer if the voter is invalid" do + allow_any_instance_of(Poll::Voter).to receive(:valid?).and_return(false) + + expect do + web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s }) + end.to raise_error(ActiveRecord::RecordInvalid) + + expect(poll.voters).to be_blank + expect(question.answers).to be_blank + end + + it "does not create voters or answers when leaving everything blank" do + web_vote.update({}) + + expect(poll.reload.voters.size).to eq 0 + expect(question.reload.answers.size).to eq 0 + end + + it "deletes existing answers and voter when no answers are given" do + create(:poll_answer, question: question, author: user, option: option_yes) + create(:poll_voter, poll: poll, user: user) + + web_vote.update({}) + + expect(poll.reload.voters.size).to eq 0 + expect(question.reload.answers.size).to eq 0 + end + + context "creating answers at the same time", :race_condition do + it "does not create two voters or two answers for two different answers" do + [option_yes, option_no].map do |option| + Thread.new { web_vote.update(question.id.to_s => { option_id: option.id.to_s }) } + end.each(&:join) + + expect(Poll::Voter.count).to be 1 + expect(Poll::Answer.count).to be 1 + end + + it "does not create two voters for duplicate answers" do + 2.times.map do + Thread.new { web_vote.update(question.id.to_s => { option_id: option_yes.id.to_s }) } + end.each(&:join) + + expect(Poll::Voter.count).to be 1 + end + + it "validates max votes on multiple-answer questions" do + question = create(:poll_question_multiple, :abc, poll: poll, max_votes: 2) + option_a = question.question_options.find_by(title: "Answer A") + option_b = question.question_options.find_by(title: "Answer B") + option_c = question.question_options.find_by(title: "Answer C") + create(:poll_answer, question: question, author: user, option: option_a) + + [option_b, option_c].map do |option| + Thread.new do + web_vote.update(question.id.to_s => { option_id: [option_a.id.to_s, option.id.to_s] }) + end + end.each(&:join) + + expect(Poll::Answer.count).to be 2 + end + end + end +end diff --git a/spec/support/common_actions/polls.rb b/spec/support/common_actions/polls.rb index 0340567f6..f6d064f51 100644 --- a/spec/support/common_actions/polls.rb +++ b/spec/support/common_actions/polls.rb @@ -1,13 +1,14 @@ module Polls - def vote_for_poll_via_web(poll, question, option) + def vote_for_poll_via_web(poll, questions_with_options) visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - click_button option - - expect(page).to have_button("You have voted #{option}") - expect(page).not_to have_button("Vote #{option}") + questions_with_options.each do |question, option| + within_fieldset(question.title) { choose option } end + + click_button "Vote" + + expect(page).to have_content "Thank you for voting!" end def vote_for_poll_via_booth diff --git a/spec/system/polls/polls_spec.rb b/spec/system/polls/polls_spec.rb index 0030460c8..49043ccf2 100644 --- a/spec/system/polls/polls_spec.rb +++ b/spec/system/polls/polls_spec.rb @@ -96,7 +96,7 @@ describe "Polls" do expect(page).not_to have_css ".already-answer" - vote_for_poll_via_web(poll_with_question, question, "Yes") + vote_for_poll_via_web(poll_with_question, question => "Yes") visit polls_path @@ -199,41 +199,82 @@ describe "Polls" do scenario "Level 2 users answering" do poll.update!(geozone_restricted_to: [geozone]) + create(:poll_question, :yes_no, poll: poll, title: "Do you agree?") - question = create(:poll_question, :yes_no, poll: poll) - user = create(:user, :level_two, geozone: geozone) - - login_as user + login_as(create(:user, :level_two, geozone: geozone)) visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - click_button "Vote Yes" + within_fieldset("Do you agree?") { choose "Yes" } + click_button "Vote" - expect(page).to have_button "You have voted Yes" - expect(page).to have_button "Vote No" + expect(page).to have_content "Thank you for voting!" + expect(page).to have_content "You have already participated in this poll. " \ + "If you vote again it will be overwritten." + + within_fieldset("Do you agree?") do + expect(page).to have_field "Yes", type: :radio, checked: true end + + expect(page).to have_button "Vote" end scenario "Level 2 users changing answer" do - poll.update!(geozone_restricted_to: [geozone]) - - question = create(:poll_question, :yes_no, poll: poll) user = create(:user, :level_two, geozone: geozone) + question = create(:poll_question, :yes_no, poll: poll, title: "Do you agree?") + + poll.update!(geozone_restricted_to: [geozone]) + create(:poll_answer, author: user, question: question, answer: "Yes") + create(:poll_voter, poll: poll, user: user) login_as user visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - click_button "Yes" + expect(page).to have_content "You have already participated in this poll. " \ + "If you vote again it will be overwritten." - expect(page).to have_button "You have voted Yes" - expect(page).to have_button "Vote No" + within_fieldset("Do you agree?") do + expect(page).to have_field "Yes", type: :radio, checked: true - click_button "No" - - expect(page).to have_button "Vote Yes" - expect(page).to have_button "You have voted No" + choose "No" end + + click_button "Vote" + + expect(page).to have_content "Thank you for voting!" + + within_fieldset("Do you agree?") do + expect(page).to have_field "No", type: :radio, checked: true + expect(page).to have_field "Yes", type: :radio, checked: false + end + + expect(page).to have_button "Vote" + end + + scenario "Level 2 users deleting their answer" do + user = create(:user, :level_two, geozone: geozone) + question = create(:poll_question_multiple, :abc, poll: poll, title: "Which ones are better?") + + create(:poll_answer, author: user, question: question, answer: "Answer A") + create(:poll_voter, poll: poll, user: user) + + login_as user + visit poll_path(poll) + + expect(page).to have_content "You have already participated in this poll. " \ + "If you vote again it will be overwritten." + + within_fieldset("Which ones are better?") { uncheck "Answer A" } + click_button "Vote" + + expect(page).to have_content "Thank you for voting!" + expect(page).not_to have_content "You have already participated" + + within_fieldset("Which ones are better?") do + expect(page).to have_field type: :checkbox, checked: false, count: 3 + expect(page).not_to have_field type: :checkbox, checked: true + end + + expect(page).to have_button "Vote" end scenario "Shows SDG tags when feature is enabled" do @@ -259,20 +300,6 @@ describe "Polls" do expect("Not restricted").to appear_before("Geozone Poll") expect("Geozone Poll").to appear_before("A Poll") end - - scenario "Level 2 users answering in a browser without javascript", :no_js do - question = create(:poll_question, :yes_no, poll: poll) - user = create(:user, :level_two) - login_as user - visit poll_path(poll) - - within("#poll_question_#{question.id}_options") do - click_button "Yes" - - expect(page).to have_button "You have voted Yes" - expect(page).to have_button "No" - end - end end context "Booth & Website", :with_frozen_time do @@ -283,7 +310,7 @@ describe "Polls" do scenario "Already voted on booth cannot vote on website" do create(:poll_shift, officer: officer, booth: booth, date: Date.current, task: :vote_collection) create(:poll_officer_assignment, officer: officer, poll: poll, booth: booth, date: Date.current) - question = create(:poll_question, :yes_no, poll: poll) + create(:poll_question, :yes_no, poll: poll, title: "Have you voted using a booth?") user = create(:user, :level_two, :in_census) login_as(officer.user) @@ -304,12 +331,9 @@ describe "Polls" do expect(page).to have_content "You have already participated in a physical booth. " \ "You can not participate again." - within("#poll_question_#{question.id}_options") do - expect(page).to have_content("Yes") - expect(page).to have_content("No") - - expect(page).not_to have_button "Yes" - expect(page).not_to have_button "No" + within_fieldset("Have you voted using a booth?") do + expect(page).to have_field "Yes", type: :radio, disabled: true + expect(page).to have_field "No", type: :radio, disabled: true end end end diff --git a/spec/system/polls/results_spec.rb b/spec/system/polls/results_spec.rb index 78b45d997..9e269d3ff 100644 --- a/spec/system/polls/results_spec.rb +++ b/spec/system/polls/results_spec.rb @@ -17,18 +17,15 @@ describe "Poll Results" do option5 = create(:poll_question_option, question: question2, title: "Yellow") login_as user1 - vote_for_poll_via_web(poll, question1, "Yes") - vote_for_poll_via_web(poll, question2, "Blue") + vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Blue") logout login_as user2 - vote_for_poll_via_web(poll, question1, "Yes") - vote_for_poll_via_web(poll, question2, "Green") + vote_for_poll_via_web(poll, question1 => "Yes", question2 => "Green") logout login_as user3 - vote_for_poll_via_web(poll, question1, "No") - vote_for_poll_via_web(poll, question2, "Yellow") + vote_for_poll_via_web(poll, question1 => "No", question2 => "Yellow") logout travel_to(poll.ends_at + 1.day) diff --git a/spec/system/polls/votation_types_spec.rb b/spec/system/polls/votation_types_spec.rb index 46d444376..e77ad2d7b 100644 --- a/spec/system/polls/votation_types_spec.rb +++ b/spec/system/polls/votation_types_spec.rb @@ -7,63 +7,38 @@ describe "Poll Votation Type" do login_as(author) end - scenario "Unique answer" do - question = create(:poll_question_unique, :yes_no) + scenario "Unique and multiple answers" do + poll = create(:poll) + create(:poll_question_unique, :yes_no, poll: poll, title: "Is it that bad?") + create(:poll_question_multiple, :abcde, poll: poll, max_votes: 3, title: "Which ones do you prefer?") - visit poll_path(question.poll) + visit poll_path(poll) - expect(page).to have_content "You can select a maximum of 1 answer." - expect(page).to have_content(question.title) - expect(page).to have_button("Vote Yes") - expect(page).to have_button("Vote No") + within_fieldset("Is it that bad?") { choose "Yes" } - within "#poll_question_#{question.id}_options" do - click_button "Yes" - - expect(page).to have_button("You have voted Yes") - expect(page).to have_button("Vote No") - - click_button "No" - - expect(page).to have_button("Vote Yes") - expect(page).to have_button("You have voted No") + within_fieldset("Which ones do you prefer?") do + check "Answer A" + check "Answer C" end - end - scenario "Multiple answers" do - question = create(:poll_question_multiple, :abc, max_votes: 2) - visit poll_path(question.poll) + click_button "Vote" - expect(page).to have_content "You can select a maximum of 2 answers." - expect(page).to have_content(question.title) - expect(page).to have_button("Vote Answer A") - expect(page).to have_button("Vote Answer B") - expect(page).to have_button("Vote Answer C") + expect(page).to have_content "Thank you for voting!" + expect(page).to have_content "You have already participated in this poll. " \ + "If you vote again it will be overwritten." - within "#poll_question_#{question.id}_options" do - click_button "Vote Answer A" - - expect(page).to have_button("You have voted Answer A") - - click_button "Vote Answer C" - - expect(page).to have_button("You have voted Answer C") - expect(page).to have_button("Vote Answer B", disabled: true) - - click_button "You have voted Answer A" - - expect(page).to have_button("Vote Answer A") - expect(page).to have_button("Vote Answer B") - - click_button "You have voted Answer C" - - expect(page).to have_button("Vote Answer C") - - click_button "Vote Answer B" - - expect(page).to have_button("You have voted Answer B") - expect(page).to have_button("Vote Answer A") - expect(page).to have_button("Vote Answer C") + within_fieldset("Is it that bad?") do + expect(page).to have_field "Yes", type: :radio, checked: true end + + within_fieldset("Which ones do you prefer?") do + expect(page).to have_field "Answer A", type: :checkbox, checked: true + expect(page).to have_field "Answer B", type: :checkbox, checked: false + expect(page).to have_field "Answer C", type: :checkbox, checked: true + expect(page).to have_field "Answer D", type: :checkbox, checked: false + expect(page).to have_field "Answer E", type: :checkbox, checked: false + end + + expect(page).to have_button "Vote" end end diff --git a/spec/system/polls/voter_spec.rb b/spec/system/polls/voter_spec.rb index 8ff4cfce5..2dd53d315 100644 --- a/spec/system/polls/voter_spec.rb +++ b/spec/system/polls/voter_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" describe "Voter" do context "Origin", :with_frozen_time do let(:poll) { create(:poll) } - let!(:question) { create(:poll_question, :yes_no, poll: poll) } + let!(:question) { create(:poll_question, :yes_no, poll: poll, title: "Is this question stupid?") } let(:booth) { create(:poll_booth) } let(:officer) { create(:poll_officer) } let(:admin) { create(:administrator) } @@ -20,41 +20,12 @@ describe "Voter" do login_as user visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - click_button "Vote Yes" + within_fieldset("Is this question stupid?") { choose "Yes" } + click_button "Vote" - expect(page).to have_button("You have voted Yes") - expect(page).not_to have_button("Vote Yes") - end - - refresh - - expect(page).to have_content("You have already participated in this poll.") - expect(page).to have_content("If you vote again it will be overwritten") - end - - scenario "Remove vote via web - Standard" do - user = create(:user, :level_two) - create(:poll_answer, question: question, author: user, answer: "Yes") - create(:poll_voter, poll: poll, user: user) - - login_as user - visit poll_path(poll) - - expect(page).to have_content("You have already participated in this poll.") - expect(page).to have_content("If you vote again it will be overwritten") - - within("#poll_question_#{question.id}_options") do - click_button "You have voted Yes" - - expect(page).to have_button("Vote Yes") - expect(page).to have_button("Vote No") - end - - refresh - - expect(page).not_to have_content("You have already participated in this poll.") - expect(page).not_to have_content("If you vote again it will be overwritten") + expect(page).to have_content "Thank you for voting!" + expect(page).to have_content "You have already participated in this poll. " \ + "If you vote again it will be overwritten." end scenario "Voting via web as unverified user" do @@ -63,9 +34,9 @@ describe "Voter" do login_as user visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - expect(page).to have_link("Yes", href: verification_path) - expect(page).to have_link("No", href: verification_path) + within_fieldset "Is this question stupid?" do + expect(page).to have_field "Yes", type: :radio, disabled: true + expect(page).to have_field "No", type: :radio, disabled: true end expect(page).to have_content "You must verify your account in order to answer" @@ -142,7 +113,7 @@ describe "Voter" do scenario "Trying to vote in web and then in booth" do login_as user - vote_for_poll_via_web(poll, question, "Yes") + vote_for_poll_via_web(poll, question => "Yes") logout login_through_form_as_officer(officer) @@ -165,8 +136,9 @@ describe "Voter" do login_as user visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - expect(page).not_to have_button("Yes") + within_fieldset "Is this question stupid?" do + expect(page).to have_field "Yes", type: :radio, disabled: true + expect(page).to have_field "No", type: :radio, disabled: true end expect(page).to have_content "You have already participated in a physical booth. " \ "You can not participate again." @@ -203,8 +175,9 @@ describe "Voter" do visit poll_path(poll) - within("#poll_question_#{question.id}_options") do - expect(page).not_to have_button("Yes") + within_fieldset "Is this question stupid?" do + expect(page).to have_field "Yes", type: :radio, disabled: true + expect(page).to have_field "No", type: :radio, disabled: true end expect(page).to have_content "You have already participated in a physical booth. " \