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`.
This commit is contained in:
Javi Martín
2024-05-16 02:43:55 +02:00
parent fd14c55615
commit a7e1b42b6c
35 changed files with 612 additions and 687 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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