From 0214c97e65677cd2968f3211717f46d2c70b0b58 Mon Sep 17 00:00:00 2001 From: John Pinto Date: Mon, 11 Nov 2024 12:03:33 +0000 Subject: [PATCH 1/2] Fix for bugs in the Conditional Questions functionality: In the case of a conditional question with answers that removed questions from different sections of a phase, any answers of removed questions were not removed, just hidden. Nor were the removed answers deleted in the database. Changes: - Fixed the broken functionality. Rspec tests updated in next commit. --- CHANGELOG.md | 3 +++ app/controllers/answers_controller.rb | 30 ++++++++++++++++++----- app/helpers/conditions_helper.rb | 7 +++++- app/javascript/src/answers/edit.js | 9 +++---- app/javascript/src/utils/sectionUpdate.js | 29 ++++++++++++++++++++++ app/models/condition.rb | 8 +++++- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aeeb4e2f3..c76e9171f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- Fix for bug in Conditional questions functionality. In the case of a conditional question with answers that removed questions, any answers of removed questions was not removed. Nor were the removed answers deleted in the database. The Rspec tests were also updated to reflect this change. + + ## v4.2.0 **Note this upgrade is mainly a migration from Bootstrap 3 to Bootstrap 5.** diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 43ab3127fc..4445428b0d 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -98,14 +98,32 @@ def create_or_update @section = @plan.sections.find_by(id: @question.section_id) template = @section.phase.template - remove_list_after = remove_list(@plan) - + # Get list of questions to be removed from the plan based on any conditional questions. + questions_remove_list_before_destroying_answers = remove_list(@plan) all_question_ids = @plan.questions.pluck(:id) - # rubocop pointed out that these variable is not used - # all_answers = @plan.answers + + # Destroy all answers for removed questions + questions_remove_list_before_destroying_answers.each do |id| + Answer.where(question_id: id, plan: @plan).each do |a| + Answer.destroy(a.id) + end + end + # Now update @plan after removing answers of questions removed from the plan. + @plan = Plan.includes( + sections: { + questions: %i[ + answers + question_format + ] + } + ).find(p_params[:plan_id]) + + # Now get list of question ids to remove based on remaining answers. + remove_list_question_ids = remove_list(@plan) + qn_data = { - to_show: all_question_ids - remove_list_after, - to_hide: remove_list_after + to_show: all_question_ids - remove_list_question_ids, + to_hide: remove_list_question_ids } section_data = [] diff --git a/app/helpers/conditions_helper.rb b/app/helpers/conditions_helper.rb index 9ea082a326..a15526a5ba 100644 --- a/app/helpers/conditions_helper.rb +++ b/app/helpers/conditions_helper.rb @@ -27,7 +27,12 @@ def answer_remove_list(answer, user = nil) opts = cond.option_list.map(&:to_i).sort action = cond.action_type chosen = answer.question_option_ids.sort - if chosen == opts + + # If the chosen (options) include the opts (options list) in the condition, + # then we apply the action. + # Currently, the Template edit through the UI only allows an action to be added to a single + # option at a time, so the opts array is always length 0 or 1. + if !opts.empty? && !chosen.empty? && (chosen & opts) == opts if action == 'remove' rems = cond.remove_data.map(&:to_i) id_list += rems diff --git a/app/javascript/src/answers/edit.js b/app/javascript/src/answers/edit.js index 9dbbdd6b7b..cb8b042160 100644 --- a/app/javascript/src/answers/edit.js +++ b/app/javascript/src/answers/edit.js @@ -5,7 +5,7 @@ import { } from '../utils/isType'; import { Tinymce } from '../utils/tinymce.js'; import debounce from '../utils/debounce'; -import { updateSectionProgress, getQuestionDiv } from '../utils/sectionUpdate'; +import { updateSectionProgress, getQuestionDiv , deleteAllAnswersForQuestion } from '../utils/sectionUpdate'; import datePicker from '../utils/datePicker'; import TimeagoFactory from '../utils/timeagoFactory.js.erb'; @@ -23,7 +23,9 @@ $(() => { updateSectionProgress(section.sec_id, section.no_ans, section.no_qns); }); data.qn_data.to_hide.forEach((questionid) => { + deleteAllAnswersForQuestion(questionid); getQuestionDiv(questionid).slideUp(); + }); data.qn_data.to_show.forEach((questionid) => { getQuestionDiv(questionid).slideDown(); @@ -100,11 +102,6 @@ $(() => { const form = target.closest('form'); const id = questionId(target); -console.log('SUBMITTING'); -console.log(target); -console.log(form); -console.log(id); - if (debounceMap[id]) { // Cancels the delated execution of autoSaving // (e.g. user clicks the button before the delay is met) diff --git a/app/javascript/src/utils/sectionUpdate.js b/app/javascript/src/utils/sectionUpdate.js index a6bec9c0df..8a120fd573 100644 --- a/app/javascript/src/utils/sectionUpdate.js +++ b/app/javascript/src/utils/sectionUpdate.js @@ -1,3 +1,5 @@ +import { Tinymce } from '../utils/tinymce.js'; + // update details in section progress panel export const updateSectionProgress = (id, numSecAnswers, numSecQuestions) => { const progressDiv = $(`#section-panel-${id}`).find('.section-status'); @@ -25,3 +27,30 @@ export const updateSectionProgress = (id, numSecAnswers, numSecQuestions) => { // given a question id find the containing div // used inconditional questions export const getQuestionDiv = (id) => $(`#answer-form-${id}`).closest('.question-body'); + +// Clear an answers for a given question id. +export const deleteAllAnswersForQuestion = (questionid) => { + const answerFormDiv = $(`#answer-form-${questionid}`); + const editAnswerForm = $(`#answer-form-${questionid}`).find('.form-answer'); + + editAnswerForm.find('input:checkbox').prop('checked', false); + editAnswerForm.find('input:radio').prop('checked', false); + editAnswerForm.find('option').prop('selected', false); + editAnswerForm.find('input:text').text(''); + + // Get the TinyMce editor textarea and rest content to '' + const editorAnswerTextAreaId = `answer-text-${questionid}`; + const tinyMceAnswerEditor = Tinymce.findEditorById(editorAnswerTextAreaId); + if (tinyMceAnswerEditor) { + tinyMceAnswerEditor.setContent(''); + } + // Date fields in form are input of type="date" + // The editAnswerForm.find('input:date') throws error, so + // we need an alternate way to reset date. + editAnswerForm.find('#answer_text').each ( (el) => { + if($(el).attr('type') === 'date') { + $(el).val(''); + } + + }); +}; diff --git a/app/models/condition.rb b/app/models/condition.rb index ef0d79cca3..80d1055f44 100644 --- a/app/models/condition.rb +++ b/app/models/condition.rb @@ -34,12 +34,18 @@ class Condition < ApplicationRecord # Sort order: Number ASC default_scope { order(number: :asc) } - + # rubocop:disable Metrics/AbcSize def deep_copy(**options) copy = dup copy.question_id = options.fetch(:question_id, nil) + # Added to allow options to be passed in for all fields + copy.option_list = options.fetch(:option_list, option_list) if options.key?(:option_list) + copy.remove_data = options.fetch(:remove_data, remove_data) if options.key?(:remove_data) + copy.action_type = options.fetch(:action_type, action_type) if options.key?(:action_type) + copy.webhook_data = options.fetch(:webhook_data, webhook_data) if options.key?(:webhook_data) # TODO: why call validate false here copy.save!(validate: false) if options.fetch(:save, false) copy end + # rubocop:enable Metrics/AbcSize end From 5326377a4dd07f363b848e205b5d3ef768b38ead Mon Sep 17 00:00:00 2001 From: John Pinto Date: Mon, 11 Nov 2024 12:07:03 +0000 Subject: [PATCH 2/2] Updated tests for changes to Conditional Questions code. --- ...troller_with_conditional_questions_spec.rb | 540 ++++++++++++++++++ spec/factories/answers.rb | 6 + spec/factories/conditions.rb | 19 +- .../questions/conditions_questions_spec.rb | 533 +++++++++++++++++ spec/models/condition_spec.rb | 139 ++++- spec/rails_helper.rb | 1 + 6 files changed, 1235 insertions(+), 3 deletions(-) create mode 100644 spec/controllers/answers_controller_with_conditional_questions_spec.rb create mode 100644 spec/features/questions/conditions_questions_spec.rb diff --git a/spec/controllers/answers_controller_with_conditional_questions_spec.rb b/spec/controllers/answers_controller_with_conditional_questions_spec.rb new file mode 100644 index 0000000000..589de45b36 --- /dev/null +++ b/spec/controllers/answers_controller_with_conditional_questions_spec.rb @@ -0,0 +1,540 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AnswersController, type: :controller do + include RolesHelper + + before(:each) do + template = create(:template, phases: 1, sections: 3) + # 3 sections for ensuring that conditions involve questions in different sections. + @section1 = template.sections[0] + @section2 = template.sections[1] + @section3 = template.sections[2] + + # Different types of questions (than can have conditional options) + @checkbox_conditional_question = create(:question, :checkbox, section: @section1, options: 5) + @radiobutton_conditional_question = create(:question, :radiobuttons, section: @section2, options: 5) + @dropdown_conditional_question = create(:question, :dropdown, section: @section3, options: 5) + + @conditional_questions = [@checkbox_conditional_question, @radiobutton_conditional_question, + @dropdown_conditional_question] + + # Questions that do not have conditional options for adding or removing + @textarea_questions = create_list(:question, 7, :textarea, section: @section1) + @textfield_questions = create_list(:question, 7, :textfield, section: @section2) + @date_questions = create_list(:question, 7, :date, section: @section3) + @rda_metadata_questions = create_list(:question, 7, :rda_metadata, section: @section1, options: 3) + @checkbox_questions = create_list(:question, 7, :checkbox, section: @section2, options: 3) + @radiobuttons_questions = create_list(:question, 7, :radiobuttons, section: @section3, options: 3) + @dropdown_questions = create_list(:question, 7, :dropdown, section: @section1, options: 3) + @multiselectbox_questions = create_list(:question, 7, :multiselectbox, section: @section2, options: 3) + + @plan = create(:plan, :creator, template: template) + @user = @plan.owner + + # Answer the questions in List2 + @textarea_answers = @textarea_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + + @textfield_answers = @textfield_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + + @date_answers = @date_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + + @rda_metadata_answers = @rda_metadata_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + + @checkbox_answers = @checkbox_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + + @radiobuttons_answers = @radiobuttons_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + + @dropdown_answers = @dropdown_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + + @multiselectbox_answers = @multiselectbox_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + + @all_questions_ids = (@conditional_questions + @textarea_questions + @textfield_questions + + @date_questions + @rda_metadata_questions + + @checkbox_questions + @radiobuttons_questions + + @dropdown_questions + @multiselectbox_questions).map(&:id) + + @all_answers_ids = (@textarea_answers + @textfield_answers + + @date_answers + @rda_metadata_answers + + @checkbox_answers + @radiobuttons_answers + + @dropdown_answers + @multiselectbox_answers).map(&:id) + + sign_in(@user) + end + + # NOTE: Condition is only implemented for checkboxes, radio buttons and dropdowns. In these cases, currently + # the option_list only takes one option in the UI. + # As functionality for more than option per condition does not yet exist in code. + # So all Conditions are created with option_list with a single option id. + + describe 'AnswersController#create_or_update for action_type: remove' do + describe 'POST /answers/create_or_update (where atleast one question has one or more conditional options)' do + # NOTE: checkbox, radiobuttons and dropdowns are the only question types that have conditional options + + # NOTE: Checkboxes allow for multiple options to be selected. + context 'with conditional checkbox question' do + it 'handles single option (with condition) in option_list ' do + condition = create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[5].id, @textfield_questions[5].id, + @date_questions[5].id, @rda_metadata_questions[5].id, + @checkbox_questions[5].id, @radiobuttons_questions[5].id, + @dropdown_questions[5].id, @multiselectbox_questions[5].id]) + + # We chose an option that is in the option_list of the condition defined above. Note that + # the text sent by UI is an empty string. + args = { + text: '', + question_option_ids: [@checkbox_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @checkbox_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + # Check hide/show questions lists sent to frontend. + expected_to_show_question_ids = @all_questions_ids - condition.remove_data + expected_to_hide_question_ids = condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + + # Check Answers in database (persisted). Expect removed answers to be destroyed. + # Answers destroyed eare easier checked using array of ids rather than individually as in example + # expect(Answer.exists?(@textarea_answers[5].id)).to be_falsey. + removed_answers = [@textarea_answers[5].id, @textfield_answers[5].id, + @date_answers[5].id, @rda_metadata_answers[5].id, @checkbox_answers[5].id, + @radiobuttons_answers[5].id, @dropdown_answers[5].id, @multiselectbox_answers[5].id] + expect(Answer.where(id: removed_answers).pluck(:id)).to be_empty + # Answers left + expect(Answer.where(id: @all_answers_ids).pluck(:id)).to match_array( + @all_answers_ids - removed_answers + ) + end + it 'handles single option (without condition) in option_list' do + create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[3].id, @textfield_questions[3].id, + @date_questions[3].id, @rda_metadata_questions[3].id, + @checkbox_questions[3].id, @dropdown_questions[3].id, + @multiselectbox_questions[3].id]) + + create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + # We choose an option that is not in the option_list of the conditions defined above. + args = { + text: '', + question_option_ids: [@checkbox_conditional_question.question_options[0].id], + user_id: @user.id, + question_id: @checkbox_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + expect(json[:qn_data][:to_show]).to match_array(@all_questions_ids) + expect(json[:qn_data][:to_hide]).to match_array([]) + end + + it 'handles multiple options (some with conditions) in option_list' do + condition1 = create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + condition2 = create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[3].id, @textfield_questions[3].id, + @date_questions[3].id, @rda_metadata_questions[3].id, + @checkbox_questions[3].id, @dropdown_questions[3].id, + @multiselectbox_questions[3].id]) + + # We choose options that is in the option_list of the conditions defined above as well as an option + # with no condition defined. + args = { + question_option_ids: [@checkbox_conditional_question.question_options[1].id, + @checkbox_conditional_question.question_options[2].id, + @checkbox_conditional_question.question_options[4].id], + user_id: @user.id, + question_id: @checkbox_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + expected_to_show_question_ids = @all_questions_ids - condition1.remove_data - condition2.remove_data + expected_to_hide_question_ids = condition1.remove_data + condition2.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + end + end + # Note: radiobuttons only allow single selection. + context 'with conditional radiobuttons question' do + it 'handles single option (with condition) in option_list ' do + condition = create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[5].id, @textfield_questions[5].id, + @date_questions[5].id, @rda_metadata_questions[5].id, + @checkbox_questions[5].id, @radiobuttons_questions[5].id, + @dropdown_questions[5].id, @multiselectbox_questions[5].id]) + + # We choose an option that is in the option_list of the condition defined above. + args = { + text: '', + question_option_ids: [@radiobutton_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @radiobutton_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + expected_to_show_question_ids = @all_questions_ids - condition.remove_data + expected_to_hide_question_ids = condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + end + it 'handles single option (without condition) in option_list' do + create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[3].id, @textfield_questions[3].id, + @date_questions[3].id, @rda_metadata_questions[3].id, + @checkbox_questions[3].id, @dropdown_questions[3].id, + @multiselectbox_questions[3].id]) + + create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + # We choose an option that is not in the option_list of the conditions defined above. + args = { + text: '', + question_option_ids: [@radiobutton_conditional_question.question_options[0].id], + user_id: @user.id, + question_id: @radiobutton_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + expect(json[:qn_data][:to_show]).to match_array(@all_questions_ids) + expect(json[:qn_data][:to_hide]).to match_array([]) + end + end + + # NOTE: dropdowns only allow single selection. + context 'with conditional dropdown question' do + it 'handles single option (with condition) in option_list ' do + condition = create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[5].id, @textfield_questions[5].id, + @date_questions[5].id, @rda_metadata_questions[5].id, + @checkbox_questions[5].id, @radiobuttons_questions[5].id, + @dropdown_questions[5].id, @multiselectbox_questions[5].id]) + + # We chose an option that is in the option_list of the condition defined above. + args = { + text: @dropdown_conditional_question.question_options[2].text, + question_option_ids: [@dropdown_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @dropdown_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + expected_to_show_question_ids = @all_questions_ids - condition.remove_data + expected_to_hide_question_ids = condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + end + it 'handles single option (without condition) in option_list' do + create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[3].id, @textfield_questions[3].id, + @date_questions[3].id, @rda_metadata_questions[3].id, + @checkbox_questions[3].id, @dropdown_questions[3].id, + @multiselectbox_questions[3].id]) + + create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + # We choose an option that is not in the option_list of the conditions defined above. + args = { + text: '', + question_option_ids: [@dropdown_conditional_question.question_options[0].id], + user_id: @user.id, + question_id: @dropdown_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + expect(json[:qn_data][:to_show]).to match_array(@all_questions_ids) + expect(json[:qn_data][:to_hide]).to match_array([]) + end + end + end + end + + describe 'AnswersController#create_or_update for action_type: add_webhook' do + before(:each) do + ActionMailer::Base.deliveries = [] + end + describe 'POST /answers/create_or_update (with add_webhook conditional option)' do + # NOTE: checkbox, radiobuttons and dropdowns are the only question types + # that have conditional options. + it 'handles a checkbox option (with add_webhook condition)' do + add_webhook_condition = create( + :condition, :webhook, + question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id] + ) + # We chose an option that is in the option_list of the condition defined above. Note that + # the text sent by UI is an empty string. + args = { + text: '', + question_option_ids: [@checkbox_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @checkbox_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + # Check hide/show questions lists sent to frontend. + expected_to_show_question_ids = @all_questions_ids - add_webhook_condition.remove_data + expected_to_hide_question_ids = add_webhook_condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(add_webhook_condition.webhook_data) + + ActionMailer::Base.deliveries.first do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@checkbox_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@checkbox_conditional_question.text) + end + end + it 'handles multiple checkbox options (one of which is add_webhook condition)' do + add_webhook_condition = create(:condition, + :webhook, + question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id]) + + condition2 = create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[3].id, @textfield_questions[3].id, + @date_questions[3].id, @rda_metadata_questions[3].id, + @checkbox_questions[3].id, @dropdown_questions[3].id, + @multiselectbox_questions[3].id]) + + # We chose an option that is in the option_list of the condition defined above. Note that + # the text sent by UI is an empty string. + args = { + text: '', + question_option_ids: [@checkbox_conditional_question.question_options[2].id, + @checkbox_conditional_question.question_options[4].id, + @checkbox_conditional_question.question_options[1].id], + user_id: @user.id, + question_id: @checkbox_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + # Check hide/show questions lists sent to frontend. + removed_data = add_webhook_condition.remove_data + condition2.remove_data + expected_to_show_question_ids = @all_questions_ids - removed_data + expected_to_hide_question_ids = add_webhook_condition.remove_data + condition2.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(add_webhook_condition.webhook_data) + + ActionMailer::Base.deliveries.first do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@checkbox_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@checkbox_conditional_question.text) + end + end + + it 'handles selection of a dropdown option (with add_webhook condition)' do + add_webhook_condition = create(:condition, + :webhook, + question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[2].id]) + + # We chose an option that is in the option_list of the condition defined above. Note that + # the text sent by UI is an empty string. + args = { + text: '', + question_option_ids: [@dropdown_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @dropdown_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + # Check hide/show questions lists sent to frontend. + expected_to_show_question_ids = @all_questions_ids - add_webhook_condition.remove_data + expected_to_hide_question_ids = add_webhook_condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(add_webhook_condition.webhook_data) + + ActionMailer::Base.deliveries.first do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@dropdown_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@dropdown_conditional_question.text) + end + end + + it 'handles selection of a radiobutton option (with add_webhook condition)' do + add_webhook_condition = create(:condition, + :webhook, + question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[2].id]) + + # We chose an option that is in the option_list of the condition defined above. Note that + # the text sent by UI is an empty string. + args = { + text: '', + question_option_ids: [@radiobutton_conditional_question.question_options[2].id], + user_id: @user.id, + question_id: @radiobutton_conditional_question.id, + plan_id: @plan.id, + lock_version: 0 + } + + post :create_or_update, params: { answer: args } + + json = JSON.parse(response.body).with_indifferent_access + + # Check hide/show questions lists sent to frontend. + expected_to_show_question_ids = @all_questions_ids - add_webhook_condition.remove_data + expected_to_hide_question_ids = add_webhook_condition.remove_data + expect(json[:qn_data][:to_show]).to match_array(expected_to_show_question_ids) + expect(json[:qn_data][:to_hide]).to match_array(expected_to_hide_question_ids) + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(add_webhook_condition.webhook_data) + + ActionMailer::Base.deliveries.first do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@radiobutton_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@radiobutton_conditional_question.text) + end + end + end + end +end diff --git a/spec/factories/answers.rb b/spec/factories/answers.rb index 447c549bb1..205609f520 100644 --- a/spec/factories/answers.rb +++ b/spec/factories/answers.rb @@ -34,5 +34,11 @@ plan user question + trait :question_options do + question_options { [create(:question_option)] } + end + trait :lock_version do + lock_version { 0 } + end end end diff --git a/spec/factories/conditions.rb b/spec/factories/conditions.rb index afaf5fdea2..03d84b46bc 100644 --- a/spec/factories/conditions.rb +++ b/spec/factories/conditions.rb @@ -26,7 +26,22 @@ FactoryBot.define do factory :condition do - option_list { nil } - remove_data { nil } + option_list { [] } + remove_data { [] } + action_type { nil } + # the webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + trait :webhook do + action_type { 'add_webhook' } + webhook_data do + # Generates string from hash + JSON.generate({ + name: Faker::Name.name, + email: Faker::Internet.email, + subject: Faker::Lorem.sentence(word_count: 4), + message: Faker::Lorem.paragraph(sentence_count: 2) + }) + end + end end end diff --git a/spec/features/questions/conditions_questions_spec.rb b/spec/features/questions/conditions_questions_spec.rb new file mode 100644 index 0000000000..3e4066a2b5 --- /dev/null +++ b/spec/features/questions/conditions_questions_spec.rb @@ -0,0 +1,533 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.feature 'Question::Conditions questions', type: :feature do + before(:each) do + @user = create(:user) + @template = create(:template, :default, :published) + @plan = create(:plan, :creator, template: @template) + @phase = create(:phase, template: @template) + # 3 sections for ensuring that conditions involve questions in different sections. + @section1 = create(:section, phase: @phase) + @section2 = create(:section, phase: @phase) + @section3 = create(:section, phase: @phase) + + # Different types of questions (than can have conditional options) + @checkbox_conditional_question = create(:question, :checkbox, section: @section1, options: 5) + @radiobutton_conditional_question = create(:question, :radiobuttons, section: @section2, options: 5) + @dropdown_conditional_question = create(:question, :dropdown, section: @section3, options: 5) + + @conditional_questions = [@checkbox_conditional_question, @radiobutton_conditional_question, + @dropdown_conditional_question] + + # Questions that do not have conditional options for adding or removing + @textarea_questions = create_list(:question, 3, :textarea, section: @section1) + @textfield_questions = create_list(:question, 3, :textfield, section: @section2) + @date_questions = create_list(:question, 3, :date, section: @section3) + @rda_metadata_questions = create_list(:question, 3, :rda_metadata, section: @section1, options: 5) + @checkbox_questions = create_list(:question, 3, :checkbox, section: @section2, options: 5) + @radiobuttons_questions = create_list(:question, 3, :radiobuttons, section: @section3, options: 5) + @dropdown_questions = create_list(:question, 3, :dropdown, section: @section1, options: 5) + @multiselectbox_questions = create_list(:question, 3, :multiselectbox, section: @section2, options: 5) + + create(:role, :creator, :editor, :commenter, user: @user, plan: @plan) + + @all_questions_ids = (@conditional_questions + @textarea_questions + @textfield_questions + + @date_questions + @rda_metadata_questions + + @checkbox_questions + @radiobuttons_questions + + @dropdown_questions + @multiselectbox_questions).map(&:id) + + # Answer the non-conditional questions + @textarea_answers = @textarea_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + + @all_non_conditional_question_answers_ids = @textarea_answers.map(&:id) + + @textfield_answers = @textfield_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + @all_non_conditional_question_answers_ids += @textfield_answers.map(&:id) + + @date_answers = @date_questions.each.map do |question| + create(:answer, plan: @plan, question: question, user: @user) + end + @all_non_conditional_question_answers_ids += @date_answers.map(&:id) + + @rda_metadata_answers = @rda_metadata_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + @all_non_conditional_question_answers_ids += @rda_metadata_answers.map(&:id) + + @checkbox_answers = @checkbox_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + @all_non_conditional_question_answers_ids += @checkbox_answers.map(&:id) + + @radiobuttons_answers = @radiobuttons_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + @all_non_conditional_question_answers_ids += @radiobuttons_answers.map(&:id) + + @dropdown_answers = @dropdown_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + @all_non_conditional_question_answers_ids += @dropdown_answers.map(&:id) + + @multiselectbox_answers = @multiselectbox_questions.each.map do |question| + create(:answer, plan: @plan, question: question, question_options: [question.question_options[2]], user: @user) + end + @all_non_conditional_question_answers_ids += @multiselectbox_answers.map(&:id) + + sign_in(@user) + + # Ensure mailer box empty before test. + ActionMailer::Base.deliveries = [] + end + + # NOTE: Condition is only implemented for checkboxes, radio buttons and dropdowns. In these cases, currently + # the option_list only takes one option in the UI. + # As functionality for more than option per condition does not yet exist in code. + # So all Conditions are created with option_list with a single option id. + + describe 'conditions with action_type remove' do + feature 'User answers a checkboxes question with a condition' do + scenario 'User answers chooses checkbox option with a condition', :js do + condition = create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, + @textfield_questions[1].id, + @date_questions[2].id, + @rda_metadata_questions[0].id, + @checkbox_questions[1].id, + @radiobuttons_questions[2].id, + @dropdown_questions[0].id, + @multiselectbox_questions[1].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the checkbox_conditional_question. + within("#answer-form-#{@checkbox_conditional_question.id}") do + check @checkbox_conditional_question.question_options[2].text + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + # Expect 8 questions and answers that have ids in condition.remove_data to be removed, and 1 new answer added: + # 24 -8 + 1 = 17 (Answers left) + # 27 - 8 = 19 (Questions left) + expect(page).to have_text('17/19 answered') + condition.remove_data.each.map do |question_id| + expect(page).to have_no_selector("#answer-form-#{question_id}") + end + + expected_remaining_question_ids = @all_questions_ids - condition.remove_data + + expected_remaining_question_ids.each.map do |question_id| + expect(page).to have_selector("#answer-form-#{question_id}") + end + + # Now uncheck checkbox_conditional_question answer. + within("#answer-form-#{@checkbox_conditional_question.id}") do + uncheck @checkbox_conditional_question.question_options[2].text + click_button 'Save' + end + + # Expect 27 questions to appear again, but the 8 answers that were removed should not be there. + # Also 1 answer should be removed as we unchecked @checkbox_conditional_question.question_options[2].text + # 17 (from previous check) - 1 = 16 + expect(page).to have_text('16/27 answered') + end + + scenario 'User answers chooses checkbox option without a condition', :js do + create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[2].id, @textfield_questions[2].id, + @date_questions[2].id, @rda_metadata_questions[2].id, + @checkbox_questions[2].id, @dropdown_questions[2].id, + @multiselectbox_questions[2].id]) + + create(:condition, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + # We choose an option that is not in the option_list of the conditions defined above. + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the checkbox_conditional_question + within("#answer-form-#{@checkbox_conditional_question.id}") do + check @checkbox_conditional_question.question_options[0].text + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + expect(page).to have_text('25/27 answered') + end + end + + feature 'User answers a radiobutton question with a condition' do + scenario 'User answers selects radiobutton option with a condition', :js do + condition = create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, + @textfield_questions[1].id, + @date_questions[2].id, + @rda_metadata_questions[0].id, + @checkbox_questions[1].id, + @radiobuttons_questions[2].id, + @dropdown_questions[0].id, + @multiselectbox_questions[1].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the radiobutton_conditional_question. + within("#answer-form-#{@radiobutton_conditional_question.id}") do + choose @radiobutton_conditional_question.question_options[2].text + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + # Expect 8 questions and answers that have ids in condition.remove_data to be removed, and 1 new answer added: + # 24 -8 + 1 = 17 (Answers left) + # 27 - 8 = 19 (Questions left) + expect(page).to have_text('17/19 answered') + condition.remove_data.each.map do |question_id| + expect(page).to have_no_selector("#answer-form-#{question_id}") + end + + expected_remaining_question_ids = @all_questions_ids - condition.remove_data + + expected_remaining_question_ids.each.map do |question_id| + expect(page).to have_selector("#answer-form-#{question_id}") + end + + # Now for radiobutton_conditional_question answer, there in no unchoose option, + # so we switch options to a different option without any conditions. + within("#answer-form-#{@radiobutton_conditional_question.id}") do + choose @radiobutton_conditional_question.question_options[0].text + click_button 'Save' + end + + # Check questions answered in progress bar. + # Expect 27 questions to appear again, but the 8 answers that were removed should not be there. + # Also 1 answer should be removed as we unchecked @radiobutton_conditional_question.question_options[2].text + # 17 (from previous check) - 1 = 16 + expect(page).to have_text('17/27 answered') + end + + scenario 'User answers selects radiobutton option without a condition', :js do + create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[2].id, @textfield_questions[2].id, + @date_questions[2].id, @rda_metadata_questions[2].id, + @checkbox_questions[2].id, @dropdown_questions[2].id, + @multiselectbox_questions[2].id]) + + create(:condition, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + + # We choose an option that is not in the option_list of the conditions defined above. + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the radiobutton_conditional_question. + within("#answer-form-#{@radiobutton_conditional_question.id}") do + choose @radiobutton_conditional_question.question_options[0].text + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + expect(page).to have_text('25/27 answered') + end + end + + feature 'User answers a dropdown question with a condition' do + scenario 'User answers chooses dropdown option with a condition', :js do + condition = create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[2].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, + @textfield_questions[1].id, + @date_questions[2].id, + @rda_metadata_questions[0].id, + @checkbox_questions[1].id, + @radiobuttons_questions[2].id, + @dropdown_questions[0].id, + @multiselectbox_questions[1].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the dropdown_conditional_question + within("#answer-form-#{@dropdown_conditional_question.id}") do + select(@dropdown_conditional_question.question_options[2].text, from: 'answer_question_option_ids') + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + # Expect 8 questions and answers that have ids in condition.remove_data to be removed, and 1 new answer added: + # 24 -8 + 1 = 17 (Answers left) + # 27 - 8 = 19 (Questions left) + expect(page).to have_text('17/19 answered') + condition.remove_data.each.map do |question_id| + expect(page).to have_no_selector("#answer-form-#{question_id}") + end + + expected_remaining_question_ids = @all_questions_ids - condition.remove_data + + expected_remaining_question_ids.each.map do |question_id| + expect(page).to have_selector("#answer-form-#{question_id}") + end + + # Now select another option for dropdown_conditional_question. + within("#answer-form-#{@dropdown_conditional_question.id}") do + select(@dropdown_conditional_question.question_options[1].text, from: 'answer_question_option_ids') + click_button 'Save' + end + + # Check questions answered in progress bar. + # Expect 27 questions to appear again, but the 8 answers that were removed should not be there. + # 17 (from previous check as we switched answer from same dropdown) + expect(page).to have_text('17/27 answered') + end + + scenario 'User answers select dropdown option without a condition', :js do + create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[1].id], + action_type: 'remove', + remove_data: [@textarea_questions[2].id, @textfield_questions[2].id, + @date_questions[2].id, @rda_metadata_questions[2].id, + @checkbox_questions[2].id, @dropdown_questions[2].id, + @multiselectbox_questions[2].id]) + + create(:condition, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[4].id], + action_type: 'remove', + remove_data: [@textarea_questions[0].id, @textfield_questions[0].id, + @date_questions[0].id, @rda_metadata_questions[0].id, + @checkbox_questions[0].id, @dropdown_questions[0].id, + @multiselectbox_questions[0].id]) + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the dropdown_conditional_question. + within("#answer-form-#{@dropdown_conditional_question.id}") do + select(@dropdown_conditional_question.question_options[0].text, from: 'answer_question_option_ids') + click_button 'Save' + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + expect(page).to have_text('25/27 answered') + end + end + end + describe 'conditions with action_type add_webhook' do + scenario 'User answers chooses checkbox option with a condition (with action_type: add_webhook)', :js do + condition = create(:condition, :webhook, question: @checkbox_conditional_question, + option_list: [@checkbox_conditional_question.question_options[2].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the checkbox_conditional_question. + within("#answer-form-#{@checkbox_conditional_question.id}") do + check @checkbox_conditional_question.question_options[2].text + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + # Expect one extra answer to be added. + expect(page).to have_text('25/27 answered') + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(condition.webhook_data) + + ActionMailer::Base.deliveries.last do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@checkbox_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@checkbox_conditional_question.text) + end + end + + scenario 'User answers chooses radiobutton option with a condition (with action_type: add_webhook)', :js do + condition = create(:condition, :webhook, question: @radiobutton_conditional_question, + option_list: [@radiobutton_conditional_question.question_options[0].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Now for radiobutton_conditional_question answer, there in no unchoose option, + # so we switch options to a different option without any conditions. + within("#answer-form-#{@radiobutton_conditional_question.id}") do + choose @radiobutton_conditional_question.question_options[0].text + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + # Expect one extra answer to be added. + expect(page).to have_text('25/27 answered') + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(condition.webhook_data) + + ActionMailer::Base.deliveries.last do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@radiobutton_conditional_question.question_options[0].text) + expect(mail.body.encoded).to include(@radiobutton_conditional_question.text) + end + end + + scenario 'User answers chooses dropdown option with a condition (with action_type: add_webhook)', :js do + condition = create(:condition, :webhook, question: @dropdown_conditional_question, + option_list: [@dropdown_conditional_question.question_options[2].id]) + + visit overview_plan_path(@plan) + + click_link 'Write plan' + + # Expand all sections + find('a[data-toggle-direction=show]').click + + # Check questions answered in progress bar. + # 24 non-conditional questions in total answered. + expect(page).to have_text('24/27 answered') + + # Answer the dropdown_conditional_question + within("#answer-form-#{@dropdown_conditional_question.id}") do + select(@dropdown_conditional_question.question_options[2].text, from: 'answer_question_option_ids') + end + + expect(page).to have_text('Answered just now') + + # Check questions answered in progress bar. + # Expect one extra answer to be added. + expect(page).to have_text('25/27 answered') + + # An email should have been sent to the configured recipient in the webhook. + # The webhook_data is a Json string of form: + # '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' + expect(ActionMailer::Base.deliveries.count).to eq(1) + webhook_data = JSON.parse(condition.webhook_data) + + ActionMailer::Base.deliveries.last do |mail| + expect(mail.to).to eq([webhook_data['email']]) + expect(mail.subject).to eq(webhook_data['subject']) + expect(mail.body.encoded).to include(webhook_data['message']) + # To see structure of email sent see app/views/user_mailer/question_answered.html.erb. + # Message should have @user.name, chosen option text and question text. + expect(mail.body.encoded).to include(@user.name) + expect(mail.body.encoded).to include(@dropdown_conditional_question.question_options[2].text) + expect(mail.body.encoded).to include(@dropdown_conditional_question.text) + end + end + end +end diff --git a/spec/models/condition_spec.rb b/spec/models/condition_spec.rb index f5b8d10d45..ee2e5ce49b 100644 --- a/spec/models/condition_spec.rb +++ b/spec/models/condition_spec.rb @@ -3,5 +3,142 @@ require 'rails_helper' RSpec.describe Condition, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + context 'associations' do + it { is_expected.to belong_to :question } + end + + describe 'condition with action_type "remove"' do + describe '.deep_copy with no options passed in.' do + let!(:question) { build(:question) } + + let!(:condition) do + build(:condition, question: question, option_list: [1, 5], + action_type: 'remove', + remove_data: [7, 8, 9]) + end + + subject { condition.deep_copy } + + it 'creates a new record' do + expect(subject).not_to eql(condition) + end + it 'copies the option_list attribute' do + expect(subject.option_list).to contain_exactly(1, 5) + end + + it 'copies the action_type attribute' do + expect(subject.action_type).to eql('remove') + end + + it 'copies the remove_data attribute' do + expect(subject.remove_data).to contain_exactly(7, 8, 9) + end + + it 'copies the webhook_data attribute' do + expect(subject.webhook_data).to be nil + end + end + + describe '.deep_copy with options passed in.' do + let!(:question) { build(:question) } + + let!(:condition) do + build(:condition, question: question, option_list: [1, 5], + action_type: 'remove', + remove_data: [7, 8, 9]) + end + let!(:options) { { option_list: [100, 101], action_type: 'remove', remove_data: [200, 220] } } + + subject { condition.deep_copy(**options) } + + it 'creates a new record' do + expect(subject).not_to eql(condition) + end + it 'replaces the option_list attribute with passed option_list' do + expect(subject.option_list).to contain_exactly(100, 101) + end + + it 'replaces the action_type attribute with passed in action_type' do + expect(subject.action_type).to eql('remove') + end + + it 'replaces the remove_data attribute with passed in remove_data' do + expect(subject.remove_data).to contain_exactly(200, 220) + end + + it 'copies the webhook_data attribute' do + expect(subject.webhook_data).to eql(condition.webhook_data) + end + end + end + + describe 'condition with action_type "add_webhook"' do + describe '.deep_copy with no options passed in.' do + let!(:question) { build(:question) } + + # condition with action_type "add_webhook" using :webhook trait + let!(:condition) do + build(:condition, :webhook, question: question) + end + + subject { condition.deep_copy } + + it 'creates a new record' do + expect(subject).not_to eql(condition) + end + it 'copies the option_list attribute' do + expect(subject.option_list).to eq([]) + end + + it 'copies the action_type attribute' do + expect(subject.action_type).to eql('add_webhook') + end + + it 'copies the remove_data attribute' do + expect(subject.remove_data).to eq([]) + end + + it 'copies the webhook_data attribute' do + expect(subject.webhook_data).to eq(condition.webhook_data) + end + end + + describe '.deep_copy with options passed in.' do + let!(:question) { build(:question) } + + let!(:condition) do + build(:condition, :webhook, question: question) + end + + # rubocop:disable Layout/LineLength + let!(:option_web_data) { '{"name":"Joe Bloggs","email":"joe.bloggs@example.com","subject":"Large data volume","message":"A message."}' } + # rubocop:enable Layout/LineLength + + let!(:options) do + { option_list: [], action_type: 'add_webhook', remove_data: [], + webhook_data: option_web_data } + end + + subject { condition.deep_copy(**options) } + + it 'creates a new record' do + expect(subject).not_to eql(condition) + end + it 'replaces the option_list attribute with passed option_list' do + expect(subject.option_list).to eq([]) + end + + it 'replaces the action_type attribute with passed in action_type' do + expect(subject.action_type).to eql('add_webhook') + end + + it 'replaces the remove_data attribute with passed in remove_data' do + expect(subject.remove_data).to eq([]) + end + + it 'copies the webhook_data attribute' do + expect(subject.webhook_data).to eq(option_web_data) + end + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4be8e3b936..0961c94e06 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -48,6 +48,7 @@ RSpec.configure do |config| config.include(AutoCompleteHelper, type: :feature) config.include(CapybaraHelper, type: :feature) + config.include(LinksHelper, type: :feature) config.include(SessionsHelper, type: :feature) config.include(TinyMceHelper, type: :feature)