From a7e1b42b6cf6b61b38522808a75cb0a97cdecc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Thu, 16 May 2024 02:43:55 +0200 Subject: [PATCH] Use checkboxes and radio buttons on poll forms Our original interface to vote in a poll had a few issues: * Since there was no button to send the form, it wasn't clear that selecting an option would automatically store it in the database. * The interface was almost identical for single-choice questions and multiple-choice questions, which made it hard to know which type of question we were answering. * Adding other type of questions, like open answers, was hard since we would have to add a different submit button for each answer. So we're now using radio buttons for single-choice questions and checkboxes for multiple-choice questions, which are the native controls designed for these purposes, and a button to send the whole form. Since we don't have a database table for poll ballots like we have for budget ballots, we're adding a new `Poll::WebVote` model to manage poll ballots. We're using WebVote instead of Ballot or Vote because they could be mistaken with other vote classes. Note that browsers don't allow removing answers with radio buttons, so once somebody has voted in a single-choice question, they can't remove the vote unless they manually edit their HTML. This is the same behavior we had before commit 7df0e9a96. As mentioned in c2010f975, we're now adding the `ChangeByZero` rubocop rule, since we've removed the test that used `and change`. --- .rubocop.yml | 3 + app/assets/stylesheets/participation.scss | 44 +----- app/assets/stylesheets/polls/form.scss | 29 ++++ app/components/polls/form_component.html.erb | 8 +- app/components/polls/form_component.rb | 18 ++- .../questions/options_component.html.erb | 35 ----- .../polls/questions/options_component.rb | 30 ---- .../questions/question_component.html.erb | 26 ++-- .../polls/questions/question_component.rb | 66 ++++++++- app/controllers/polls/answers_controller.rb | 36 ----- app/controllers/polls_controller.rb | 19 ++- app/models/abilities/common.rb | 6 - app/models/poll/answer.rb | 23 +-- app/models/poll/question.rb | 2 - app/models/poll/web_vote.rb | 43 ++++++ app/views/polls/answers/show.js.erb | 1 - app/views/polls/show.html.erb | 4 +- config/locales/en/general.yml | 6 +- config/locales/en/responders.yml | 1 + config/locales/es/general.yml | 6 +- config/locales/es/responders.yml | 1 + config/routes/poll.rb | 5 +- spec/components/polls/form_component_spec.rb | 70 ++++++++++ .../polls/questions/options_component_spec.rb | 112 --------------- .../questions/question_component_spec.rb | 81 +++++++++-- .../polls/answers_controller_spec.rb | 23 --- spec/controllers/polls_controller_spec.rb | 21 +++ spec/models/abilities/common_spec.rb | 77 ++++------ spec/models/poll/answer_spec.rb | 131 ------------------ spec/models/poll/web_vote_spec.rb | 110 +++++++++++++++ spec/support/common_actions/polls.rb | 13 +- spec/system/polls/polls_spec.rb | 106 ++++++++------ spec/system/polls/results_spec.rb | 9 +- spec/system/polls/votation_types_spec.rb | 75 ++++------ spec/system/polls/voter_spec.rb | 59 +++----- 35 files changed, 612 insertions(+), 687 deletions(-) create mode 100644 app/assets/stylesheets/polls/form.scss delete mode 100644 app/components/polls/questions/options_component.html.erb delete mode 100644 app/components/polls/questions/options_component.rb delete mode 100644 app/controllers/polls/answers_controller.rb create mode 100644 app/models/poll/web_vote.rb delete mode 100644 app/views/polls/answers/show.js.erb create mode 100644 spec/components/polls/form_component_spec.rb delete mode 100644 spec/components/polls/questions/options_component_spec.rb delete mode 100644 spec/controllers/polls/answers_controller_spec.rb create mode 100644 spec/models/poll/web_vote_spec.rb 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. " \