diff --git a/app/controllers/pages/exit_pages_controller.rb b/app/controllers/pages/exit_pages_controller.rb
index 43a53f0186..e8a93aeb2b 100644
--- a/app/controllers/pages/exit_pages_controller.rb
+++ b/app/controllers/pages/exit_pages_controller.rb
@@ -1,6 +1,7 @@
class Pages::ExitPagesController < PagesController
- before_action :check_multiple_branches_enabled
before_action :check_user_has_permission
+ before_action :check_multiple_branches_enabled
+ before_action :check_page_can_have_exit_pages
def new
exit_page_input = Pages::ExitPageInput.new(page: page)
@@ -18,6 +19,46 @@ def create
end
end
+ def edit
+ exit_page_input = Pages::UpdateExitPageInput.new(exit_page:).assign_exit_page_values
+
+ render locals: { exit_page_input:, preview_html: preview_html(exit_page_input), check_preview_validation: false }
+ end
+
+ def update
+ exit_page_input = Pages::UpdateExitPageInput.new(update_exit_page_input_params.merge(exit_page:))
+
+ if exit_page_input.submit
+ redirect_to routes_path(form_id: current_form.id), success: t("banner.success.exit_page_saved")
+ else
+ render :edit, locals: { exit_page_input:, preview_html: preview_html(exit_page_input), check_preview_validation: true }, status: :unprocessable_content
+ end
+ end
+
+ def delete
+ delete_confirmation_input = Pages::DeleteExitPageInput.new
+
+ render locals: { delete_confirmation_input:, page:, exit_page: }
+ end
+
+ def destroy
+ delete_confirmation_input = Pages::DeleteExitPageInput.new(params.require(:pages_delete_exit_page_input).permit(:confirm))
+
+ unless delete_confirmation_input.valid?
+ return render :delete, locals: { delete_confirmation_input:, page:, exit_page: }, status: :unprocessable_content
+ end
+
+ unless delete_confirmation_input.confirmed?
+ return redirect_to edit_exit_page_path(@current_form.id, page.id, exit_page.id)
+ end
+
+ current_form.save_question_changes! do
+ exit_page.destroy!
+ end
+
+ redirect_to routes_path(@current_form.id), success: t("banner.success.exit_page_deleted")
+ end
+
def render_preview
exit_page_input = Pages::ExitPageInput.new(markdown: params[:markdown])
exit_page_input.validate if params[:check_preview_validation] == "true"
@@ -27,10 +68,18 @@ def render_preview
private
+ def exit_page
+ @exit_page ||= page.exit_pages.find(params.require(:id))
+ end
+
def exit_page_input_params
params.require(:pages_exit_page_input).permit(:heading, :markdown).merge(page:)
end
+ def update_exit_page_input_params
+ params.require(:pages_update_exit_page_input).permit(:heading, :markdown).merge(page:)
+ end
+
def check_user_has_permission
authorize current_form, :can_edit_form?
end
@@ -41,6 +90,12 @@ def check_multiple_branches_enabled
render "errors/not_found", status: :not_found, formats: :html
end
+ def check_page_can_have_exit_pages
+ return if Forms::RoutesInput.route_with_selection_options?(page)
+
+ render "errors/not_found", status: :not_found, formats: :html
+ end
+
def preview_html(exit_page_input_object)
return t("exit_page.no_content_added_html") if exit_page_input_object.markdown.blank?
diff --git a/app/input_objects/pages/delete_exit_page_input.rb b/app/input_objects/pages/delete_exit_page_input.rb
new file mode 100644
index 0000000000..ba4f0bb96b
--- /dev/null
+++ b/app/input_objects/pages/delete_exit_page_input.rb
@@ -0,0 +1,2 @@
+class Pages::DeleteExitPageInput < DeleteConfirmationInput
+end
diff --git a/app/input_objects/pages/exit_page_input.rb b/app/input_objects/pages/exit_page_input.rb
index c51bc9ed62..b84494bc63 100644
--- a/app/input_objects/pages/exit_page_input.rb
+++ b/app/input_objects/pages/exit_page_input.rb
@@ -1,13 +1,13 @@
class Pages::ExitPageInput < BaseInput
attr_accessor :page, :markdown, :heading
- validates :heading, :markdown, presence: true
- validates :heading, length: { maximum: 250 }
- validates :markdown, markdown: { allow_headings: true }
+ include ExitPageValidation
def submit
return false if invalid?
- ExitPage.create!(question_page: page, heading:, markdown:)
+ page.form.save_question_changes! do
+ ExitPage.create!(question_page: page, heading:, markdown:)
+ end
end
end
diff --git a/app/input_objects/pages/update_exit_page_input.rb b/app/input_objects/pages/update_exit_page_input.rb
new file mode 100644
index 0000000000..732b86409f
--- /dev/null
+++ b/app/input_objects/pages/update_exit_page_input.rb
@@ -0,0 +1,21 @@
+class Pages::UpdateExitPageInput < BaseInput
+ attr_accessor :page, :markdown, :heading, :exit_page
+
+ include ExitPageValidation
+
+ def assign_exit_page_values
+ self.page = exit_page.question_page
+ self.heading = exit_page.heading
+ self.markdown = exit_page.markdown
+
+ self
+ end
+
+ def submit
+ return false if invalid?
+
+ page.form.save_question_changes! do
+ exit_page.update!(heading: heading, markdown: markdown)
+ end
+ end
+end
diff --git a/app/models/concerns/exit_page_validation.rb b/app/models/concerns/exit_page_validation.rb
new file mode 100644
index 0000000000..002b843ca5
--- /dev/null
+++ b/app/models/concerns/exit_page_validation.rb
@@ -0,0 +1,9 @@
+module ExitPageValidation
+ extend ActiveSupport::Concern
+
+ included do
+ validates :heading, :markdown, presence: true
+ validates :heading, length: { maximum: 250 }
+ validates :markdown, markdown: { allow_headings: true }
+ end
+end
diff --git a/app/models/exit_page.rb b/app/models/exit_page.rb
index 94ad3eb143..e28c2c0b0f 100644
--- a/app/models/exit_page.rb
+++ b/app/models/exit_page.rb
@@ -25,4 +25,10 @@ def self.positions_for_page(question_page)
[exit_page_id, index + 1]
end
end
+
+ def options_to_this_exit_page
+ return [] if conditions.nil?
+
+ conditions.pluck(:answer_value)
+ end
end
diff --git a/app/models/form.rb b/app/models/form.rb
index 22cf9c7ce6..7dc3f9cebe 100644
--- a/app/models/form.rb
+++ b/app/models/form.rb
@@ -51,11 +51,18 @@ class Form < ApplicationRecord
attr_accessor :task_status_service
- def save_question_changes!
- self.question_section_completed = false
- # Make sure the updated_at is updated as we use this to determine if the form has changed in forms-runner.
- touch unless changed?
- save_draft!
+ # Takes an optional blocl which will be called in the same transaction
+ # as the save.
+ def save_question_changes!(&block)
+ ActiveRecord::Base.transaction do
+ self.question_section_completed = false
+
+ block.call if block_given?
+
+ # Make sure the updated_at is updated as we use this to determine if the form has changed in forms-runner.
+ touch unless changed?
+ save_draft!
+ end
end
def save_draft!
diff --git a/app/views/pages/exit_pages/delete.html.erb b/app/views/pages/exit_pages/delete.html.erb
new file mode 100644
index 0000000000..134e472bb7
--- /dev/null
+++ b/app/views/pages/exit_pages/delete.html.erb
@@ -0,0 +1,13 @@
+<% set_page_title(title_with_error_prefix(t("page_titles.delete_exit_page"), delete_confirmation_input.errors&.any?)) %>
+<% content_for :back_link, govuk_back_link_to(edit_exit_page_path(form_id: @current_form.id, page_id: page.id, id: exit_page.id), t(".back_link")) %>
+
+
+
+ <%= render(
+ delete_confirmation_input,
+ url: exit_page_path(@current_form.id, page.id, exit_page.id),
+ caption_text: t(".caption", exit_page_number: exit_page.position, heading: exit_page.heading),
+ legend_text: t(".title"),
+ ) %>
+
+
diff --git a/app/views/pages/exit_pages/edit.html.erb b/app/views/pages/exit_pages/edit.html.erb
new file mode 100644
index 0000000000..294a27bf05
--- /dev/null
+++ b/app/views/pages/exit_pages/edit.html.erb
@@ -0,0 +1,58 @@
+<% set_page_title(title_with_error_prefix(t("page_titles.exit_page_edit"), exit_page_input.errors&.any?)) %>
+<% content_for :back_link, govuk_back_link_to(routes_path(@current_form.id), t(".back_link")) %>
+
+
+
+
+ <%= form_with model: exit_page_input, url: exit_page_path(@current_form.id, exit_page_input.exit_page.question_page.id, exit_page_input.exit_page.id), method: 'PATCH' do |f| %>
+ <%= f.govuk_error_summary %>
+
+ <%= t("page_titles.exit_page_new_caption", question_number: exit_page_input.page.position) %>
+ -
+ <%= t("page_titles.exit_page_edit") %>
+
+
+ <% options = exit_page_input.exit_page.options_to_this_exit_page %>
+
+ <%= govuk_summary_list(actions: false) do |summary_list|
+ summary_list.with_row do |row|
+ row.with_key { t("page_route_card.question_name_short", question_number: exit_page_input.page.position) }
+ row.with_value { "#{exit_page_input.page.question_text}" }
+ end;
+
+ summary_list.with_row do |row|
+ row.with_key { t(".options_to_this_exit_page") }
+ row.with_value do
+ if options.empty?
+ t(".no_options_to_this_exit_page")
+ elsif options.one?
+ options.first
+ else
+ content_tag(:ul, class: "govuk-list govuk-list--bullet") do
+ options.collect { |option| concat(content_tag(:li, option)) }
+ end
+ end
+ end
+ end;
+ end %>
+
+ <%= f.govuk_text_field( :heading, label: { size: 'm' } ) %>
+
+ <%= render MarkdownEditorComponent::View.new(:markdown,
+ form_builder: f,
+ render_preview_path: render_preview_exit_pages_path(form_id: @current_form.id, page_id: exit_page_input.exit_page.question_page.id, check_preview_validation:),
+ preview_html: preview_html,
+ form_model: exit_page_input,
+ label: t("helpers.label.pages_exit_page_input.markdown"),
+ hint: t("helpers.hint.pages_exit_page_input.markdown"),
+ allow_headings: true) %>
+
+
+ <%= f.govuk_submit t("save_and_continue") %>
+ <%= govuk_button_link_to t('.delete_exit_page'), delete_exit_page_path(@current_form.id, exit_page_input.exit_page.question_page.id, exit_page_input.exit_page.id), warning: true %>
+ <%= govuk_link_to t('cancel'), routes_path(@current_form.id) %>
+
+ <% end %>
+
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b1caffb3a5..61f853c676 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1741,10 +1741,12 @@ en:
copy_of_answers: Give people the option to get a copy of their answers by email
date_settings: Are you asking for someone’s date of birth?
declaration: Add a declaration for people to agree to
+ delete_exit_page: Are you sure you want to delete this exit page?
delete_secondary_skip: Are you sure you want to delete the route for any other answer?
edit_exit_page: Edit exit page
email_code_sent: Confirmation code sent
error_prefix: 'Error: '
+ exit_page_edit: Edit exit page
exit_page_new: Add exit page
exit_page_new_caption: Question %{question_number}’s exit pages
forbidden: You cannot view this page
@@ -1897,6 +1899,15 @@ en:
destroy:
success: The question, ‘%{question_text}’, has been deleted
exit_pages:
+ delete:
+ back_link: Back to edit exit page
+ caption: 'Exit page %{exit_page_number}: %{heading}'
+ title: Are you sure you want to delete this exit page?
+ edit:
+ back_link: Back to edit question routes
+ delete_exit_page: Delete exit page
+ no_options_to_this_exit_page: No route to this exit page
+ options_to_this_exit_page: Options that go to this exit page
new:
back_link: Back to edit question routes
heading: Edit question
diff --git a/config/locales/input_objects/update_exit_pages.yml b/config/locales/input_objects/update_exit_pages.yml
new file mode 100644
index 0000000000..3fecc8b811
--- /dev/null
+++ b/config/locales/input_objects/update_exit_pages.yml
@@ -0,0 +1,23 @@
+---
+en:
+ activemodel:
+ errors:
+ models:
+ pages/update_exit_page_input:
+ attributes:
+ heading:
+ blank: Enter a page heading
+ too_long: Page heading must be %{count} characters or less
+ markdown:
+ blank: Enter page content
+ too_long: Page content must be %{count} characters or less
+ unsupported_markdown_syntax: Page content can only contain formatting for links, subheadings (##), bulleted lists (*), or numbered lists (1.)
+ helpers:
+ hint:
+ pages_update_exit_page_input:
+ heading: Use a heading that summarises why someone cannot continue with the form. For example, ‘You’re not eligible for this service’.
+ markdown: Explain why they cannot continue to use the form and, if possible, tell them what they should do instead.
+ label:
+ pages_update_exit_page_input:
+ heading: Page heading
+ markdown: Page content
diff --git a/config/routes.rb b/config/routes.rb
index 537e7dda89..6063a23a94 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -160,7 +160,11 @@
end
end
- resources :exit_pages, only: %i[new create], path: "exit-pages", module: :pages do
+ resources :exit_pages, except: %i[index show], path: "exit-pages", module: :pages do
+ member do
+ get "/delete" => "exit_pages#delete", as: :delete
+ end
+
collection do
post "/preview" => "exit_pages#render_preview", as: :render_preview
end
diff --git a/spec/input_objects/pages/exit_page_input_spec.rb b/spec/input_objects/pages/exit_page_input_spec.rb
index 05e8da2dbc..7569f6507d 100644
--- a/spec/input_objects/pages/exit_page_input_spec.rb
+++ b/spec/input_objects/pages/exit_page_input_spec.rb
@@ -6,32 +6,8 @@
let(:heading) { "the heading" }
let(:markdown) { "some markdown" }
- describe "validations" do
- it "is invalid if heading is nil" do
- error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.heading.blank")
- exit_page_input.heading = nil
- expect(exit_page_input).to be_invalid
- expect(exit_page_input.errors.full_messages_for(:heading)).to include("Heading #{error_message}")
- end
-
- it "is invalid if markdown is nil" do
- error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.markdown.blank")
- exit_page_input.markdown = nil
- expect(exit_page_input).to be_invalid
- expect(exit_page_input.errors.full_messages_for(:markdown)).to include("Markdown #{error_message}")
- end
-
- it "is invalid if heading is too long" do
- error_message = I18n.t("activemodel.errors.models.pages/exit_page_input.attributes.heading.too_long", count: 250)
- exit_page_input.heading = "a" * 251
- expect(exit_page_input).to be_invalid
- expect(exit_page_input.errors.full_messages_for(:heading)).to include("Heading #{error_message}")
- end
-
- it_behaves_like "a markdown field with headings allowed", :mark_complete do
- let(:model) { exit_page_input }
- let(:attribute) { :markdown }
- end
+ it_behaves_like "validates exit pages" do
+ let(:model) { exit_page_input }
end
describe "#submit" do
@@ -40,6 +16,7 @@
let(:heading) { "the heading" }
let(:markdown) { "some markdown" }
let(:page) { create :page }
+ let(:form) { page.form }
it "returns a truthy value" do
expect(exit_page_input.submit).to be_truthy
@@ -55,6 +32,12 @@
expect(page.exit_pages.first.markdown).to eq(markdown)
end
+ it "sets question_section_completed to false and updates the draft" do
+ form.question_section_completed = true
+ expect { exit_page_input.submit }.to change(form, :question_section_completed).from(true).to(false)
+ .and(change { form.draft_form_document.reload.updated_at })
+ end
+
context "when the exit page is invalid" do
let(:heading) { nil }
diff --git a/spec/input_objects/pages/update_exit_page_input_spec.rb b/spec/input_objects/pages/update_exit_page_input_spec.rb
new file mode 100644
index 0000000000..b9b6be59af
--- /dev/null
+++ b/spec/input_objects/pages/update_exit_page_input_spec.rb
@@ -0,0 +1,47 @@
+require "rails_helper"
+
+RSpec.describe Pages::UpdateExitPageInput, type: :model do
+ subject(:exit_page_input) { described_class.new(heading:, markdown:) }
+
+ let(:heading) { "the heading" }
+ let(:markdown) { "some markdown" }
+
+ it_behaves_like "validates exit pages" do
+ let(:model) { exit_page_input }
+ end
+
+ describe "#submit" do
+ subject(:exit_page_input) { described_class.new(heading:, markdown:, page:, exit_page:) }
+
+ let(:heading) { "the heading" }
+ let(:markdown) { "some markdown" }
+ let(:form) { page.form }
+ let(:page) { create :page }
+ let(:exit_page) { create :exit_page, question_page: page }
+
+ it "returns a truthy value" do
+ expect(exit_page_input.submit).to be_truthy
+ end
+
+ it "updates the exit page" do
+ exit_page_input.submit
+ expect(exit_page.question_page).to eq(page)
+ expect(exit_page.heading).to eq(heading)
+ expect(exit_page.markdown).to eq(markdown)
+ end
+
+ it "sets question_section_completed to false and updates the draft" do
+ form.question_section_completed = true
+ expect { exit_page_input.submit }.to change(form, :question_section_completed).from(true).to(false)
+ .and(change { form.draft_form_document.reload.updated_at })
+ end
+
+ context "when the exit page is invalid" do
+ let(:heading) { nil }
+
+ it "returns false" do
+ expect(exit_page_input.submit).to be false
+ end
+ end
+ end
+end
diff --git a/spec/models/exit_page_spec.rb b/spec/models/exit_page_spec.rb
index 4da57eae6d..959e7eb019 100644
--- a/spec/models/exit_page_spec.rb
+++ b/spec/models/exit_page_spec.rb
@@ -101,4 +101,23 @@
)
end
end
+
+ describe "#options_to_this_exit_page" do
+ it "when the exit_page has linked conditions, returns an array of the answer values of conditions that go to this exit page" do
+ question_page = create(:page)
+ exit_page = create(:exit_page, question_page:)
+ create(:condition, routing_page: question_page, exit_page_id: exit_page.id, answer_value: "option 1")
+ create(:condition, exit_page:, answer_value: "option 2")
+ create(:condition, exit_page:, answer_value: "option 3")
+
+ expect(exit_page.options_to_this_exit_page).to eq(["option 1", "option 2", "option 3"])
+ end
+
+ it "when the exit_page has no linked conditions, returns an empty array" do
+ question_page = create(:page)
+ exit_page = create(:exit_page, question_page:)
+
+ expect(exit_page.options_to_this_exit_page).to eq([])
+ end
+ end
end
diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb
index 8140b71aeb..f66f33ab1f 100644
--- a/spec/models/form_spec.rb
+++ b/spec/models/form_spec.rb
@@ -666,6 +666,25 @@
}.to(change { form.reload.draft_form_document.content["updated_at"] })
end
end
+
+ context "when a block is passed" do
+ it "calls the block" do
+ expect {
+ form.save_question_changes! do
+ form.name = "New name"
+ end
+ }.to change { form.reload.name }.to("New name")
+ end
+
+ it "does not update draft if the block fails" do
+ expect {
+ form.save_question_changes! do
+ form.name = "New name"
+ raise "Something went wrong"
+ end
+ }.to raise_error("Something went wrong").and(not_change { form.reload.draft_form_document.content["name"] }).and(not_change { form.reload.updated_at })
+ end
+ end
end
describe "#save_draft!" do
diff --git a/spec/requests/pages/exit_pages_controller_spec.rb b/spec/requests/pages/exit_pages_controller_spec.rb
index 4da13932a4..ad611a422d 100644
--- a/spec/requests/pages/exit_pages_controller_spec.rb
+++ b/spec/requests/pages/exit_pages_controller_spec.rb
@@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe Pages::ExitPagesController, :feature_multiple_branches, type: :request do
- let(:form) { create(:form, :with_group, :with_pages, pages_count: 1, group:) }
+ let(:form) { create(:form, :with_group, :with_pages, pages: [create(:page, :with_selection_settings)], group:) }
let(:page) { form.pages.first }
let(:group) { create(:group, organisation: test_org, memberships: [create(:membership, user: standard_user)]) }
@@ -75,6 +75,146 @@
end
end
+ describe "#edit" do
+ let(:exit_page) { create(:exit_page, question_page: page) }
+
+ before do
+ get edit_exit_page_path(form.id, page.id, exit_page.id)
+ end
+
+ it "renders the template" do
+ expect(response).to have_rendered("exit_pages/edit")
+ expect(response.body).to include("Edit exit page")
+ end
+
+ context "when the multiple branches feature is not enabled", feature_multiple_branches: false do
+ it "is forbidden" do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context "when the user is not in the form's group" do
+ let(:group) { create(:group, organisation: test_org, memberships: []) }
+
+ it "returns a forbidden status code" do
+ expect(response).to have_http_status :forbidden
+ end
+ end
+ end
+
+ describe "#update" do
+ let(:exit_page) { create(:exit_page, question_page: page) }
+
+ before do
+ patch exit_page_path(form.id, page.id, exit_page.id), params: { pages_update_exit_page_input: { heading: "Exit Page Heading", markdown: "Exit Page Markdown" } }
+ end
+
+ it "updates the exit page" do
+ expect(response).to redirect_to(routes_path(form.id))
+ expect(flash[:success]).to eq(I18n.t("banner.success.exit_page_saved"))
+
+ expect(exit_page.reload.heading).to eq("Exit Page Heading")
+ expect(exit_page.reload.markdown).to eq("Exit Page Markdown")
+ end
+
+ context "when the exit page is invalid" do
+ before do
+ patch exit_page_path(form.id, page.id, exit_page.id), params: { pages_update_exit_page_input: { heading: nil, markdown: nil } }
+ end
+
+ it "renders the exit page template" do
+ expect(response).to have_http_status(:unprocessable_content)
+ expect(response).to render_template("exit_pages/edit")
+ end
+ end
+
+ context "when the multiple branches feature is not enabled", feature_multiple_branches: false do
+ it "is forbidden" do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context "when the user is not in the form's group" do
+ let(:group) { create(:group, organisation: test_org, memberships: []) }
+
+ it "returns a forbidden status code" do
+ expect(response).to have_http_status :forbidden
+ end
+ end
+ end
+
+ describe "#delete" do
+ let(:exit_page) { create(:exit_page, question_page: page) }
+
+ before do
+ get delete_exit_page_path(form_id: form.id, page_id: page.id, id: exit_page.id)
+ end
+
+ it "renders the template" do
+ expect(response).to have_rendered("exit_pages/delete")
+ expect(response.body).to include("Are you sure you want to delete this exit page?")
+ end
+
+ context "when the multiple branches feature is not enabled", feature_multiple_branches: false do
+ it "is forbidden" do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context "when the user is not in the form's group" do
+ let(:group) { create(:group, organisation: test_org, memberships: []) }
+
+ it "returns a forbidden status code" do
+ expect(response).to have_http_status :forbidden
+ end
+ end
+ end
+
+ describe "#destroy" do
+ subject!(:exit_page) { create(:exit_page, question_page: page) }
+
+ let(:form) { super().tap { |f| f.question_section_completed = true } }
+ let(:exit_page_url) { exit_page_path(form_id: form.id, page_id: page.id, id: exit_page.id) }
+ let(:params) { { pages_delete_exit_page_input: { confirm: "yes" } } }
+
+ it "deletes the exit page" do
+ expect { delete exit_page_url, params: }.to change(page.reload.exit_pages, :count).by(-1)
+ end
+
+ it "sets question_section_completed to false and updates the draft" do
+ expect { delete exit_page_url, params: }.to change { form.reload.question_section_completed? }.from(true).to(false)
+ .and(change { form.draft_form_document.reload.updated_at })
+ end
+
+ it "redirects to the routes page" do
+ delete(exit_page_url, params:)
+ expect(response).to redirect_to(routes_path(form_id: form.id))
+ end
+
+ it "displays a success flash message" do
+ delete(exit_page_url, params:)
+ expect(flash[:success]).to eq(I18n.t("banner.success.exit_page_deleted"))
+ end
+
+ context "when not confirmed" do
+ let(:params) { { pages_delete_exit_page_input: { confirm: "no" } } }
+
+ it "redirects to the edit exit page page" do
+ delete(exit_page_url, params:)
+ expect(response).to redirect_to(edit_exit_page_path(form_id: form.id, page_id: page.id, id: exit_page.id))
+ end
+ end
+
+ context "when the params are invalid" do
+ let(:params) { { pages_delete_exit_page_input: { confirm: nil } } }
+
+ it "returns an unprocessable content response" do
+ delete(exit_page_url, params:)
+ expect(response).to have_http_status :unprocessable_content
+ end
+ end
+ end
+
describe "#render_preview" do
let(:markdown) { "[Markdown](https://example.com)" }
diff --git a/spec/support/shared_examples/exit_page_validation.rb b/spec/support/shared_examples/exit_page_validation.rb
new file mode 100644
index 0000000000..db965c3430
--- /dev/null
+++ b/spec/support/shared_examples/exit_page_validation.rb
@@ -0,0 +1,23 @@
+RSpec.shared_examples "validates exit pages" do
+ it "is invalid if heading is nil" do
+ model.heading = nil
+ expect(model).to be_invalid
+ expect(model.errors.where(:heading, :blank)).to be_present
+ end
+
+ it "is invalid if markdown is nil" do
+ exit_page_input.markdown = nil
+ expect(exit_page_input).to be_invalid
+ expect(model.errors.where(:markdown, :blank)).to be_present
+ end
+
+ it "is invalid if heading is too long" do
+ exit_page_input.heading = "a" * 251
+ expect(exit_page_input).to be_invalid
+ expect(model.errors.where(:heading, :too_long)).to be_present
+ end
+
+ it_behaves_like "a markdown field with headings allowed" do
+ let(:attribute) { :markdown }
+ end
+end
diff --git a/spec/views/pages/exit_pages/delete.html.erb_spec.rb b/spec/views/pages/exit_pages/delete.html.erb_spec.rb
new file mode 100644
index 0000000000..4ea07acee5
--- /dev/null
+++ b/spec/views/pages/exit_pages/delete.html.erb_spec.rb
@@ -0,0 +1,43 @@
+require "rails_helper"
+
+RSpec.describe "pages/exit_pages/delete" do
+ let(:delete_confirmation_input) { Forms::DeleteConfirmationInput.new }
+ let(:current_form) { create :form }
+ let(:page) { create :page, form: current_form }
+ let(:exit_page) { create :exit_page, question_page: page, heading: "the heading" }
+
+ before do
+ assign(:current_form, current_form)
+ render locals: { page:, exit_page:, delete_confirmation_input: }
+ end
+
+ it "has a page title" do
+ expect(view.content_for(:title)).to include "Are you sure you want to delete this exit page?"
+ end
+
+ it "has a heading" do
+ expect(rendered).to have_css "h1", text: "Are you sure you want to delete this exit page?"
+ end
+
+ it "has a heading caption with the question text" do
+ expect(rendered).to have_css ".govuk-caption-l", text: "Exit page 1: the heading"
+ end
+
+ it "has a back link to the edit exit page" do
+ expect(view.content_for(:back_link)).to have_link("Back to edit exit page", href: edit_exit_page_path(form_id: current_form.id, page_id: page.id, id: exit_page.id))
+ end
+
+ it "has a delete confirmation input to confirm deletion of the page" do
+ expect(rendered).to render_template "input_objects/_delete_confirmation_input"
+ end
+
+ describe "delete confirmation input" do
+ it "posts to the destroy action" do
+ expect(rendered).to have_element "form", action: "/forms/#{current_form.id}/pages/#{page.id}/exit-pages/#{exit_page.id}", method: "post"
+ end
+
+ it "does not have a hint" do
+ expect(rendered).not_to have_css ".govuk-hint"
+ end
+ end
+end
diff --git a/spec/views/pages/exit_pages/edit.html.erb_spec.rb b/spec/views/pages/exit_pages/edit.html.erb_spec.rb
new file mode 100644
index 0000000000..a9853ba8e7
--- /dev/null
+++ b/spec/views/pages/exit_pages/edit.html.erb_spec.rb
@@ -0,0 +1,87 @@
+require "rails_helper"
+
+describe "exit_pages/edit.html.erb" do
+ let(:form) { build_stubbed :form, :with_pages, pages: [build_stubbed(:page, id: 1)] }
+ let(:page) { form.pages.first }
+ let(:exit_page) { build_stubbed(:exit_page, question_page: page) }
+ let(:exit_page_input) { Pages::UpdateExitPageInput.new(page:, exit_page:) }
+
+ def render_page
+ assign(:current_form, form)
+ assign(:page, page)
+ render template: "pages/exit_pages/edit", locals: { exit_page_input:, preview_html: "", check_preview_validation: false }
+ end
+
+ it "has the correct title" do
+ render_page
+ expect(view.content_for(:title)).to have_content("Edit exit page")
+ end
+
+ it "has the correct back link" do
+ render_page
+ expect(view.content_for(:back_link)).to have_link("Back to edit question routes", href: routes_path(form.id))
+ end
+
+ it "has the correct heading and caption" do
+ render_page
+ expect(rendered).to have_selector "h1", text: "Question 1’s exit pages"
+ expect(rendered).to have_selector "h1", text: "Edit exit page"
+ end
+
+ it "shows the page title as a summary list" do
+ render_page
+ expect(rendered).to have_css(".govuk-summary-list__key", text: "Question #{page.position}")
+ expect(rendered).to have_css(".govuk-summary-list__value", text: page.question_text)
+ end
+
+ context "when the exit page has no options" do
+ before do
+ allow(exit_page).to receive(:options_to_this_exit_page).and_return([])
+ end
+
+ it "shows the page title as a summary list" do
+ render_page
+
+ expect(rendered).to have_css(".govuk-summary-list__key", text: "Options that go to this exit page")
+ expect(rendered).to have_css(".govuk-summary-list__value", text: "No route to this exit page")
+ end
+ end
+
+ context "when the exit page has has a single option" do
+ let(:options) { %w[option1] }
+
+ before do
+ allow(exit_page).to receive(:options_to_this_exit_page).and_return(options)
+ end
+
+ it "shows the page title as a summary list" do
+ render_page
+
+ expect(rendered).to have_css(".govuk-summary-list__key", text: "Options that go to this exit page")
+ expect(rendered).to have_css(".govuk-summary-list__value:text()", text: "option1", exact: true)
+ end
+ end
+
+ context "when the exit page has multiple options" do
+ let(:options) { %w[option1 option2 option3] }
+
+ before do
+ allow(exit_page).to receive(:options_to_this_exit_page).and_return(options)
+ end
+
+ it "shows the page title as a summary list" do
+ render_page
+
+ expect(rendered).to have_css(".govuk-summary-list__key", text: "Options that go to this exit page")
+ expect(rendered).to have_css("li", text: options.first)
+ expect(rendered).to have_css("li", text: options.second)
+ expect(rendered).to have_css("li", text: options.third)
+ end
+ end
+
+ it "shows error messages" do
+ exit_page_input.errors.add(:heading, "Error: Heading can't be blank")
+ render_page
+ expect(rendered).to have_text("Error: Heading can't be blank")
+ end
+end