From 22bb08efe82f0504516163e11b4716ab77f0b4dc Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 21 Oct 2026 17:15:56 +0100 Subject: [PATCH 01/24] Add a method for loading an Incomplete Submission into session memory --- app/lib/store/session_answer_store.rb | 4 ++++ spec/lib/store/session_answer_store_spec.rb | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/lib/store/session_answer_store.rb b/app/lib/store/session_answer_store.rb index 043824e76..6b7c3152b 100644 --- a/app/lib/store/session_answer_store.rb +++ b/app/lib/store/session_answer_store.rb @@ -17,6 +17,10 @@ def save_step(step, answer) @store[ANSWERS_KEY][@form_key][page_key(step)] = answer end + def load_answers(saved_answers) + @store[ANSWERS_KEY][@form_key] = saved_answers + end + def get_stored_answer(step) @store.dig(ANSWERS_KEY, @form_key, page_key(step)) end diff --git a/spec/lib/store/session_answer_store_spec.rb b/spec/lib/store/session_answer_store_spec.rb index aab57c100..ed505e9f1 100644 --- a/spec/lib/store/session_answer_store_spec.rb +++ b/spec/lib/store/session_answer_store_spec.rb @@ -134,6 +134,21 @@ end end + describe "#load_answers" do + let(:saved_answers) do + { + "1" => { "selection" => "Option 1" }, + "2" => { "text" => "Example text" }, + } + end + + it "stores the answers for the form" do + answer_store.load_answers(saved_answers) + + expect(answer_store.answers).to eq(saved_answers) + end + end + describe "#locales_used" do context "when no locales have been added" do it "returns []" do From 483a5e008dda97af48879164b02456bf1d9a4e28 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 21 Oct 2026 18:19:06 +0100 Subject: [PATCH 02/24] Add a method for setting onto the session store the submission reference This value gets loaded out of the database and we needs this method to set it into the session. --- app/lib/store/submission_details_store.rb | 5 +++++ spec/lib/store/submission_details_store_spec.rb | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/app/lib/store/submission_details_store.rb b/app/lib/store/submission_details_store.rb index 2fa2b9dfe..0b95d5ef1 100644 --- a/app/lib/store/submission_details_store.rb +++ b/app/lib/store/submission_details_store.rb @@ -26,6 +26,11 @@ def get_submission_reference @store.dig(CONFIRMATION_KEY, @form_key, SUBMISSION_REFERENCE_KEY.to_s) end + def set_submission_reference(reference) + @store[CONFIRMATION_KEY][@form_key] ||= {} + @store[CONFIRMATION_KEY][@form_key][SUBMISSION_REFERENCE_KEY.to_s] = reference + end + def requested_email_confirmation? @store.dig(CONFIRMATION_KEY, @form_key, REQUESTED_EMAIL_CONFIRMATION_KEY.to_s) end diff --git a/spec/lib/store/submission_details_store_spec.rb b/spec/lib/store/submission_details_store_spec.rb index 73bd1c103..222704673 100644 --- a/spec/lib/store/submission_details_store_spec.rb +++ b/spec/lib/store/submission_details_store_spec.rb @@ -33,6 +33,14 @@ end end + describe "#store_submission_reference" do + it "stores the given submission reference" do + submission_details_store.set_submission_reference("SAVEDREF") + + expect(submission_details_store.get_submission_reference).to eq("SAVEDREF") + end + end + describe "#save_requested_email_confirmation" do it "stores and returns the requested email confirmation flag" do submission_details_store.save_requested_email_confirmation(requested_email_confirmation) From f3d9c0f559602e97ffb9092758f2f69ae4db5b12 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 21 Oct 2026 23:21:57 +0100 Subject: [PATCH 03/24] Add LoadFormProgressService --- app/lib/flow/context.rb | 3 +- app/services/load_form_progress_service.rb | 20 ++++++++ .../load_form_progress_service_spec.rb | 48 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 app/services/load_form_progress_service.rb create mode 100644 spec/services/load_form_progress_service_spec.rb diff --git a/app/lib/flow/context.rb b/app/lib/flow/context.rb index c02179e8d..b44ad3556 100644 --- a/app/lib/flow/context.rb +++ b/app/lib/flow/context.rb @@ -11,9 +11,10 @@ def initialize(form:, form_document:, store:) delegate :support_details, to: :form delegate :step_by_id, :previous_step, :next_step_slug, :next_step, :can_visit?, :completed_steps, :all_steps, to: :journey - delegate :clear_stored_answer, :clear, :form_submitted?, :starting_new_form?, :answers, to: :answer_store + delegate :clear_stored_answer, :clear, :form_submitted?, :load_answers, :add_locale, :starting_new_form?, :answers, to: :answer_store delegate :generate_submission_reference, :get_submission_reference, + :set_submission_reference, :save_requested_email_confirmation, :requested_email_confirmation?, :clear_submission_details, diff --git a/app/services/load_form_progress_service.rb b/app/services/load_form_progress_service.rb new file mode 100644 index 000000000..a1c2b587f --- /dev/null +++ b/app/services/load_form_progress_service.rb @@ -0,0 +1,20 @@ +class LoadFormProgressService + def initialize(current_context:, user_id:) + @current_context = current_context + @user_id = user_id + end + + def load! + incomplete_submission = IncompleteSubmission.find_by(user_id: @user_id, form_id: @current_context.form.id) + return nil unless incomplete_submission + + @current_context.clear + @current_context.clear_submission_details + + @current_context.load_answers(incomplete_submission.answers) + @current_context.set_submission_reference(incomplete_submission.submission_reference) + @current_context.add_locale(incomplete_submission.locale) + + incomplete_submission + end +end diff --git a/spec/services/load_form_progress_service_spec.rb b/spec/services/load_form_progress_service_spec.rb new file mode 100644 index 000000000..a3e191551 --- /dev/null +++ b/spec/services/load_form_progress_service_spec.rb @@ -0,0 +1,48 @@ +require "rails_helper" + +RSpec.describe LoadFormProgressService do + subject(:service) { described_class.new(current_context:, user_id:) } + + let(:user_id) { "one-login-sub" } + let(:form_document) { build(:form_document, form_id: 1, start_page: 1, steps: [build(:question_step, :with_text_settings, id: 1)]) } + let(:form) { Form.new(form_document) } + let(:current_context) { Flow::Context.new(form:, form_document:, store: {}) } + + describe "#load!" do + context "when the user has saved progress for the form" do + let(:saved_answers) { { "1" => { "text" => "a saved answer" } } } + + let!(:incomplete_submission) do + IncompleteSubmission.create!(user_id:, form_id: form.id, answers: saved_answers, submission_reference: "SAVEDREF", locale: "cy") + end + + it "returns the saved progress" do + expect(service.load!).to eq(incomplete_submission) + end + + it "loads the saved answers into the session" do + service.load! + + expect(current_context.answers).to eq(saved_answers) + end + + it "loads the submission reference into the session" do + service.load! + + expect(current_context.get_submission_reference).to eq("SAVEDREF") + end + + it "loads the locale the form was being filled in" do + service.load! + + expect(current_context.submission_locale).to eq(:cy) + end + end + + context "when the user has no saved progress for the form" do + it "returns nil" do + expect(service.load!).to be_nil + end + end + end +end From 123dc106b62cb49a4c38d4341d3a2d58ea98f105 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 23 Oct 2026 12:01:09 +0100 Subject: [PATCH 04/24] Add routes for ResumeProgress controller --- .../forms/resume_progress_controller.rb | 2 ++ spec/routing/resume_progress_routing_spec.rb | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 app/controllers/forms/resume_progress_controller.rb create mode 100644 spec/routing/resume_progress_routing_spec.rb diff --git a/app/controllers/forms/resume_progress_controller.rb b/app/controllers/forms/resume_progress_controller.rb new file mode 100644 index 000000000..b1f51dff4 --- /dev/null +++ b/app/controllers/forms/resume_progress_controller.rb @@ -0,0 +1,2 @@ +class Forms::ResumeProgressController < ApplicationController +end diff --git a/spec/routing/resume_progress_routing_spec.rb b/spec/routing/resume_progress_routing_spec.rb new file mode 100644 index 000000000..5af465eb2 --- /dev/null +++ b/spec/routing/resume_progress_routing_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe Forms::ResumeProgressController, type: :routing do + describe "routing" do + describe "#resume_progress" do + it "routes to #new" do + expect(get: resume_progress_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/resume_progress#new", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + + describe "#resume_progress_signed_in" do + it "routes to #signed_in" do + expect(get: resume_progress_signed_in_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/resume_progress#signed_in", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + end +end From 3cd0c94d0f921cdde2c42ba3c7a5f4c023007a14 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 23 Oct 2026 15:48:15 +0100 Subject: [PATCH 05/24] Add routing for Forms::ResumeOptionsController This is the controller and routes for screens where we ask the user do they want to start a new submission or resume an existing incomplete one. --- .../forms/resume_options_controller.rb | 2 ++ config/routes.rb | 4 ++++ spec/routing/resume_options_routing_spec.rb | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 app/controllers/forms/resume_options_controller.rb create mode 100644 spec/routing/resume_options_routing_spec.rb diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb new file mode 100644 index 000000000..609149c5b --- /dev/null +++ b/app/controllers/forms/resume_options_controller.rb @@ -0,0 +1,2 @@ +class Forms::ResumeOptionsController < ApplicationController +end diff --git a/config/routes.rb b/config/routes.rb index 3fdb69c06..074ce56a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,10 @@ get "/continue-to-login" => "forms/continue_to_one_login#show", as: :continue_to_one_login get "/#{CheckYourAnswersStep::CHECK_YOUR_ANSWERS_STEP_SLUG}" => "forms/check_your_answers#show", as: :check_your_answers post "/#{CheckYourAnswersStep::CHECK_YOUR_ANSWERS_STEP_SLUG}" => "forms/check_your_answers#submit_answers", as: :form_submit_answers + get "what-do-you-want-to-do" => "forms/resume_options#show", as: :resume_options + post "what-do-you-want-to-do" => "forms/resume_options#continue", as: :resume_options_continue + get "continue-saved-form" => "forms/resume_progress#new", as: :resume_progress + get "continue-saved-form/signed-in" => "forms/resume_progress#signed_in", as: :resume_progress_signed_in get "/save-your-progress" => "forms/save_progress#new", as: :save_progress get "/save-your-progress/after-sign-in" => "forms/save_progress#after_sign_in", as: :save_progress_after_sign_in post "/save-your-progress" => "forms/save_progress#create", as: :create_save_progress diff --git a/spec/routing/resume_options_routing_spec.rb b/spec/routing/resume_options_routing_spec.rb new file mode 100644 index 000000000..6b6fcb8fd --- /dev/null +++ b/spec/routing/resume_options_routing_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe Forms::ResumeOptionsController, type: :routing do + describe "routing" do + describe "#resume_options" do + it "routes to #show" do + expect(get: resume_options_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/resume_options#show", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + + describe "#resume_options_continue" do + it "routes to #continue" do + expect(post: resume_options_continue_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/resume_options#continue", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + end +end From 265b7510f819ec74912a749f7b7d9598e0e63433 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 28 Oct 2026 13:10:07 +0000 Subject: [PATCH 06/24] Update base_controller to also handle redirecting for save and return For forms that have the save and return setting on, we want to show them the page that asks if they want to start a new submission or resume one they already started. We also update the feature spec as the first page the user sees has changed. --- app/controllers/forms/base_controller.rb | 3 +++ spec/features/save_form_progress_spec.rb | 6 +++--- spec/requests/forms/base_controller_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/controllers/forms/base_controller.rb b/app/controllers/forms/base_controller.rb index cd2a5abe5..4c63fd9b4 100644 --- a/app/controllers/forms/base_controller.rb +++ b/app/controllers/forms/base_controller.rb @@ -4,7 +4,10 @@ class BaseController < ApplicationController around_action :set_locale def redirect_to_friendly_url_start + return redirect_to resume_options_path(@form.id, @form.form_slug) if @form.save_and_return_enabled? + redirect_to form_step_path(params.require(:form_id), @form.form_slug, @form.start_page) + LogEventService.log_form_start unless mode.preview? end diff --git a/spec/features/save_form_progress_spec.rb b/spec/features/save_form_progress_spec.rb index 480ab08bc..318937892 100644 --- a/spec/features/save_form_progress_spec.rb +++ b/spec/features/save_form_progress_spec.rb @@ -44,7 +44,7 @@ end scenario "As a form filler, I sign in with GOV.UK One Login to save my progress" do - when_i_visit_the_form_start_page + when_i_visit_the_first_question and_i_answer_the_first_question then_i_see_the_save_and_exit_link @@ -56,8 +56,8 @@ and_my_answers_are_saved end - def when_i_visit_the_form_start_page - visit form_path(mode: "form", form_id: 1, form_slug: form.form_slug) + def when_i_visit_the_first_question + visit form_step_path(mode: "form", form_id: 1, form_slug: form.form_slug, step_slug: 1) expect(page.find("h1")).to have_text first_question_text end diff --git a/spec/requests/forms/base_controller_spec.rb b/spec/requests/forms/base_controller_spec.rb index 17c38c7e0..c1f6e1f14 100644 --- a/spec/requests/forms/base_controller_spec.rb +++ b/spec/requests/forms/base_controller_spec.rb @@ -14,9 +14,11 @@ declaration_markdown: "agree to the declaration", steps: steps_data, language:, + save_and_return:, ) end let(:start_page) { 1 } + let(:save_and_return) { "disabled" } let(:language) { "en" } let(:no_data_found_response) do @@ -301,6 +303,14 @@ it "Logs the form_visit event" do expect(LogEventService).to have_received(:log_form_start) end + + context "and save and return is enabled" do + let(:save_and_return) { "enabled" } + + it "redirects to the page asking whether to start a new form or continue a saved one" do + expect(response).to redirect_to(resume_options_path(mode: "form", form_id: 2, form_slug: form_response_data.form_slug)) + end + end end context "when the form has no start page" do From 032ce84fe169c22dc3fd7a62d3a2bc35e2b8a6db Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 28 Oct 2026 14:18:01 +0000 Subject: [PATCH 07/24] Add spec that checks logging correctly occurs in BaseController --- app/controllers/forms/base_controller.rb | 4 ++-- spec/requests/forms/base_controller_spec.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/forms/base_controller.rb b/app/controllers/forms/base_controller.rb index 4c63fd9b4..eb8bc16f9 100644 --- a/app/controllers/forms/base_controller.rb +++ b/app/controllers/forms/base_controller.rb @@ -4,11 +4,11 @@ class BaseController < ApplicationController around_action :set_locale def redirect_to_friendly_url_start + LogEventService.log_form_start unless mode.preview? + return redirect_to resume_options_path(@form.id, @form.form_slug) if @form.save_and_return_enabled? redirect_to form_step_path(params.require(:form_id), @form.form_slug, @form.start_page) - - LogEventService.log_form_start unless mode.preview? end rescue_from ActiveResource::ResourceNotFound, RepeatableStep::AnswerIndexError do diff --git a/spec/requests/forms/base_controller_spec.rb b/spec/requests/forms/base_controller_spec.rb index c1f6e1f14..d23ad9efd 100644 --- a/spec/requests/forms/base_controller_spec.rb +++ b/spec/requests/forms/base_controller_spec.rb @@ -310,6 +310,10 @@ it "redirects to the page asking whether to start a new form or continue a saved one" do expect(response).to redirect_to(resume_options_path(mode: "form", form_id: 2, form_slug: form_response_data.form_slug)) end + + it "Logs the form_visit event" do + expect(LogEventService).to have_received(:log_form_start) + end end end From 3527b9e8bfa2df850d901f81b64d1d259a552197 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 28 Oct 2026 17:00:40 +0000 Subject: [PATCH 08/24] Add spec and stub files for ResumeOptions screen Where the user will be presented with options to either start a new submission or resume an existing one. --- .../forms/resume_options_controller.rb | 1 + app/views/forms/resume_options/show.html.erb | 0 .../forms/resume_options_controller_spec.rb | 35 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 app/views/forms/resume_options/show.html.erb create mode 100644 spec/requests/forms/resume_options_controller_spec.rb diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index 609149c5b..c94f0ee74 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -1,2 +1,3 @@ class Forms::ResumeOptionsController < ApplicationController + def show; end end diff --git a/app/views/forms/resume_options/show.html.erb b/app/views/forms/resume_options/show.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/spec/requests/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb new file mode 100644 index 000000000..52e146344 --- /dev/null +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -0,0 +1,35 @@ +require "rails_helper" + +RSpec.describe Forms::ResumeOptionsController, type: :request do + let(:form) do + build(:form_document, :with_support, form_id: 2, start_page: 1, steps:, save_and_return: "enabled") + end + + let(:steps) do + [ + build(:question_step, :with_text_settings, id: 1, next_step_id: 2), + build(:question_step, :with_text_settings, id: 2), + ] + end + + let(:req_headers) { { "Accept" => "application/json" } } + let(:api_url_suffix) { "/draft" } + let(:mode) { "preview-draft" } + + before do + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/api/v2/forms/#{form.form_id}#{api_url_suffix}", req_headers, form.to_json, 200 + end + end + + describe "GET #show" do + before do + get resume_options_path(mode:, form_id: form.form_id, form_slug: form.form_slug) + end + + it "renders the page asking whether to start a new form or continue a saved one" do + expect(response).to have_http_status(:ok) + expect(response).to render_template(:show) + end + end +end From 8bdbc966b4f2853142dc54fe693a8503feb07766 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Wed, 28 Oct 2026 17:17:09 +0000 Subject: [PATCH 09/24] Update ResumeOptionsController handle default behaviour --- .../forms/resume_options_controller.rb | 17 +++++++++++++++-- .../forms/resume_options_controller_spec.rb | 10 ++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index c94f0ee74..bcfa5b14d 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -1,3 +1,16 @@ -class Forms::ResumeOptionsController < ApplicationController - def show; end +module Forms + class ResumeOptionsController < BaseController + before_action :back_to_start + def show; end + + private + + def back_to_start + unless @current_context.form.save_and_return_enabled? + redirect_to form_step_path(@current_context.form.id, + @current_context.form.form_slug, + @current_context.form.start_page) + end + end + end end diff --git a/spec/requests/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb index 52e146344..42cd9b764 100644 --- a/spec/requests/forms/resume_options_controller_spec.rb +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -31,5 +31,15 @@ expect(response).to have_http_status(:ok) expect(response).to render_template(:show) end + + context "when save and return is disabled on the form" do + let(:form) do + build(:form_document, :with_support, form_id: 2, start_page: 1, steps:, save_and_return: "disabled") + end + + it "redirects to the first question" do + expect(response).to redirect_to(form_step_path(form.form_id, form.form_slug, 1, mode:)) + end + end end end From a5a71fa585c1a02bb7f8622df92d5e6d29929bee Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 29 Oct 2026 12:40:47 +0000 Subject: [PATCH 10/24] Add view for showing user options for resuming or starting a submission --- app/views/forms/resume_options/show.html.erb | 7 +++++ config/locales/cy.yml | 3 ++ config/locales/en.yml | 3 ++ .../resume_options/show.html.erb_spec.rb | 28 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 spec/views/forms/resume_options/show.html.erb_spec.rb diff --git a/app/views/forms/resume_options/show.html.erb b/app/views/forms/resume_options/show.html.erb index e69de29bb..758e2e0f3 100644 --- a/app/views/forms/resume_options/show.html.erb +++ b/app/views/forms/resume_options/show.html.erb @@ -0,0 +1,7 @@ +<% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> + +
+
+

<%= t('.title') %>

+
+
diff --git a/config/locales/cy.yml b/config/locales/cy.yml index ab1c6a3a1..ff95f7d5f 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -425,6 +425,9 @@ cy: show: radios_legend: Ydych chi’n siŵr yr hoffech chi dynnu’ch ffeil? title: Tynnu’ch ffeil + resume_options: + show: + title: What do you want to do? review_file: show: check_file: Gwiriwch y ffeil sydd wedi’i huwchlwytho diff --git a/config/locales/en.yml b/config/locales/en.yml index e7a276e19..c52bcefc6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -425,6 +425,9 @@ en: show: radios_legend: Are you sure you want to remove your file? title: Remove your file + resume_options: + show: + title: What do you want to do? review_file: show: check_file: Check your uploaded file diff --git a/spec/views/forms/resume_options/show.html.erb_spec.rb b/spec/views/forms/resume_options/show.html.erb_spec.rb new file mode 100644 index 000000000..6147ec6fc --- /dev/null +++ b/spec/views/forms/resume_options/show.html.erb_spec.rb @@ -0,0 +1,28 @@ +require "rails_helper" + +describe "forms/resume_options/show.html.erb" do + let(:form) { build :form } + let(:mode) { OpenStruct.new(preview_draft?: false, preview_archived?: false, preview_live?: false) } + let(:resume_options_input) { ResumeOptionsInput.new } + + before do + assign(:current_context, OpenStruct.new(form:)) + assign(:form, form) + assign(:mode, mode) + assign(:resume_options_input, resume_options_input) + + without_partial_double_verification do + allow(view).to receive(:save_resume_options_path).and_return("/save_resume_options") + end + + render + end + + it "has the correct page title" do + expect(view.content_for(:title)).to eq "#{I18n.t('forms.resume_options.show.title')} - #{form.name}" + end + + it "has the correct heading" do + expect(rendered).to have_css("h1", text: I18n.t("forms.resume_options.show.title")) + end +end From 4074a02c19b827df4f1c96fb4aa37a38a7b16b4b Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 29 Oct 2026 13:15:52 +0000 Subject: [PATCH 11/24] Add ResumeOptionsInput input object and spec --- app/input_objects/resume_options_input.rb | 18 ++++++++ config/locales/cy.yml | 8 ++++ config/locales/en.yml | 8 ++++ .../resume_options_input_spec.rb | 46 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 app/input_objects/resume_options_input.rb create mode 100644 spec/input_objects/resume_options_input_spec.rb diff --git a/app/input_objects/resume_options_input.rb b/app/input_objects/resume_options_input.rb new file mode 100644 index 000000000..791353a04 --- /dev/null +++ b/app/input_objects/resume_options_input.rb @@ -0,0 +1,18 @@ +class ResumeOptionsInput + include ActiveModel::Model + include ActiveModel::Validations + + attr_accessor :resume_options + + RADIO_OPTIONS = { start_new: "start_new", continue_saved: "continue_saved" }.freeze + + validates :resume_options, presence: true, inclusion: { in: RADIO_OPTIONS.values } + + def continue_saved? + resume_options == RADIO_OPTIONS[:continue_saved] + end + + def values + RADIO_OPTIONS.keys + end +end diff --git a/config/locales/cy.yml b/config/locales/cy.yml index ff95f7d5f..40b6c2304 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -200,6 +200,10 @@ cy: attributes: remove: blank: Rhaid i chi ddewis opsiwn + resume_options_input: + attributes: + resume_options: + blank: Select whether you want to start a new form or continue filling in a saved form autocomplete: assistive_hint: 'Pan fo canlyniadau awtolenwi ar gael, defnyddiwch y saethau i fyny ac i lawr i adolygu a defnyddiwch ‘enter’ i ddewis. Defnyddwyr dyfeisiau cyffwrdd: chwiliwch drwy gyffwrdd neu sweipio' no_results: Dim canlyniadau wedi’u canfod @@ -480,6 +484,10 @@ cy: options: 'no': Na 'yes': Ydw + resume_options_input: + options: + continue_saved: Continue filling in a saved form + start_new: Start a new form legend: email_confirmation_input: send_confirmation: Ydych chi eisiau derbyn e-bost yn cadarnhau bod eich ffurflen wedi’i chyflwyno? diff --git a/config/locales/en.yml b/config/locales/en.yml index c52bcefc6..8bcde5a64 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -200,6 +200,10 @@ en: attributes: remove: blank: You must select an option + resume_options_input: + attributes: + resume_options: + blank: Select whether you want to start a new form or continue filling in a saved form autocomplete: assistive_hint: When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures. no_results: No results found @@ -480,6 +484,10 @@ en: options: 'no': 'No' 'yes': 'Yes' + resume_options_input: + options: + continue_saved: Continue filling in a saved form + start_new: Start a new form legend: email_confirmation_input: send_confirmation: Do you want to get an email confirming your form has been submitted? diff --git a/spec/input_objects/resume_options_input_spec.rb b/spec/input_objects/resume_options_input_spec.rb new file mode 100644 index 000000000..0542e4059 --- /dev/null +++ b/spec/input_objects/resume_options_input_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +RSpec.describe ResumeOptionsInput do + let(:input) { described_class.new(resume_options: "continue_saved") } + + describe "validations" do + it "is valid with valid attributes" do + expect(input).to be_valid + end + + it "is valid with each of the radio options offered" do + input.values.map(&:to_s).each do |value| + input.resume_options = value + expect(input).to be_valid, "expected #{value} to be valid" + end + end + + it "is not valid without a resume_options" do + input.resume_options = nil + expect(input).not_to be_valid + expect(input.errors[:resume_options]).to include(I18n.t("activemodel.errors.models.resume_options_input.attributes.resume_options.blank")) + end + + it "is not valid with an invalid resume_options" do + input.resume_options = "invalid" + expect(input).not_to be_valid + expect(input.errors[:resume_options]).to include("is not included in the list") + end + end + + describe "#continue_saved?" do + it 'returns true when resume_options is "continue_saved"' do + expect(described_class.new(resume_options: "continue_saved").continue_saved?).to be true + end + + it 'returns false when resume_options is "start_new"' do + expect(described_class.new(resume_options: "start_new").continue_saved?).to be false + end + end + + describe "#values" do + it "returns the radio options" do + expect(described_class.new.values).to eq(%i[start_new continue_saved]) + end + end +end From 4ed4b8e3c0b0930d9506238f4a557bec282ade23 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 29 Oct 2026 17:20:16 +0000 Subject: [PATCH 12/24] Update show view for ResumeOptions screen --- .../forms/resume_options_controller.rb | 4 +++- app/views/forms/resume_options/show.html.erb | 24 +++++++++++++++---- config/locales/cy.yml | 4 ++++ config/locales/en.yml | 4 ++++ .../resume_options/show.html.erb_spec.rb | 17 ++++++++++++- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index bcfa5b14d..187090f93 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -1,7 +1,9 @@ module Forms class ResumeOptionsController < BaseController before_action :back_to_start - def show; end + def show + @resume_options_input = ResumeOptionsInput.new + end private diff --git a/app/views/forms/resume_options/show.html.erb b/app/views/forms/resume_options/show.html.erb index 758e2e0f3..d421a6370 100644 --- a/app/views/forms/resume_options/show.html.erb +++ b/app/views/forms/resume_options/show.html.erb @@ -1,7 +1,23 @@ <% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> -
-
-

<%= t('.title') %>

+ +<%= form_with(model: @resume_options_input, url: resume_options_continue_path(form_id: @form.id, form_slug: @form.form_slug)) do |f| %> +
+
+ <% if @resume_options_input&.errors&.any? %> + <%= f.govuk_error_summary(t("error_summary_title")) %> + <% end %> + + <%= f.govuk_collection_radio_buttons :resume_options, + @resume_options_input.values, + ->(option) { option }, + ->(option) { t("helpers.label.resume_options_input.options.#{option}") }, + ->(option) { t("helpers.hint.resume_options_input.options.#{option}", default: nil) }, + legend: { text: t('.title'), tag: 'h1', size: 'l' }, + bold_labels: false %> + + + <%= f.govuk_submit(t("continue")) %> +
-
+<% end %> diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 40b6c2304..7eb013219 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -431,6 +431,7 @@ cy: title: Tynnu’ch ffeil resume_options: show: + heading: What do you want to do? title: What do you want to do? review_file: show: @@ -466,6 +467,9 @@ cy: hint: email_confirmation_input: send_confirmation: Byddwn ond yn defnyddio’r cyfeiriad e-bost rydych yn ei ddarparu yma i anfon cadarnhad bod eich ffurflen wedi’i chyflwyno’n llwyddiannus. Ni fydd yn cynnwys copi o’ch atebion. + resume_options_input: + options: + continue_saved: You’ll need to sign in to your GOV.UK One Login label: add_another_answer_input: options: diff --git a/config/locales/en.yml b/config/locales/en.yml index 8bcde5a64..a60bb3dc8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -431,6 +431,7 @@ en: title: Remove your file resume_options: show: + heading: What do you want to do? title: What do you want to do? review_file: show: @@ -466,6 +467,9 @@ en: hint: email_confirmation_input: send_confirmation: We’ll only use the email address you provide here to send a confirmation that your form’s been successfully submitted. It will not contain a copy of your answers. + resume_options_input: + options: + continue_saved: You’ll need to sign in to your GOV.UK One Login label: add_another_answer_input: options: diff --git a/spec/views/forms/resume_options/show.html.erb_spec.rb b/spec/views/forms/resume_options/show.html.erb_spec.rb index 6147ec6fc..cbd6b9321 100644 --- a/spec/views/forms/resume_options/show.html.erb_spec.rb +++ b/spec/views/forms/resume_options/show.html.erb_spec.rb @@ -12,7 +12,7 @@ assign(:resume_options_input, resume_options_input) without_partial_double_verification do - allow(view).to receive(:save_resume_options_path).and_return("/save_resume_options") + allow(view).to receive(:resume_options_continue_path).and_return("/resume_options_continue") end render @@ -25,4 +25,19 @@ it "has the correct heading" do expect(rendered).to have_css("h1", text: I18n.t("forms.resume_options.show.title")) end + + it "displays the start a new form and continue filling in a saved form options" do + expect(rendered).to have_field(I18n.t("helpers.label.resume_options_input.options.start_new"), type: :radio) + expect(rendered).to have_field(I18n.t("helpers.label.resume_options_input.options.continue_saved"), type: :radio) + end + + it "has a hint on the continue filling in a saved form option" do + continue_option = Capybara.string(rendered).find(".govuk-radios__item", text: I18n.t("helpers.label.resume_options_input.options.continue_saved")) + + expect(continue_option).to have_css(".govuk-radios__hint", text: I18n.t("helpers.hint.resume_options_input.options.continue_saved")) + end + + it "has a continue button" do + expect(rendered).to have_button(I18n.t("continue")) + end end From 8a18b0e2a3bc2488ba43c7249d2bef65d04ea529 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 29 Oct 2026 19:13:41 +0000 Subject: [PATCH 13/24] Update ResumeOptionsController to send filler to chosen path --- .../forms/resume_options_controller.rb | 16 +++++++++ .../forms/resume_options_controller_spec.rb | 33 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index 187090f93..22e1f8ff0 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -5,6 +5,18 @@ def show @resume_options_input = ResumeOptionsInput.new end + def continue + @resume_options_input = ResumeOptionsInput.new(resume_options_params) + + unless @resume_options_input.valid? + return render :show, status: :unprocessable_entity + end + + return redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) unless @resume_options_input.continue_saved? + + redirect_to resume_progress_path(form_id: @current_context.form.id, form_slug: @current_context.form.form_slug) + end + private def back_to_start @@ -14,5 +26,9 @@ def back_to_start @current_context.form.start_page) end end + + def resume_options_params + params.require(:resume_options_input).permit(:resume_options) + end end end diff --git a/spec/requests/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb index 42cd9b764..f6d273455 100644 --- a/spec/requests/forms/resume_options_controller_spec.rb +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -42,4 +42,37 @@ end end end + + describe "POST #continue" do + before do + post resume_options_continue_path(mode:, form_id: form.form_id, form_slug: form.form_slug), + params: { resume_options_input: { resume_options: } } + end + + context "when the user chooses to start a new form" do + let(:resume_options) { "start_new" } + + it "redirects to the first question" do + expect(response).to redirect_to(form_step_path(form.form_id, form.form_slug, 1, mode:)) + end + end + + context "when the user chooses to continue filling in a saved form" do + let(:resume_options) { "continue_saved" } + + it "redirects to resume their saved progress" do + expect(response).to redirect_to(resume_progress_path(form.form_id, form.form_slug, mode:)) + end + end + + context "when the user does not choose an option" do + let(:resume_options) { "" } + + it "shows the page again with an error" do + expect(response).to have_http_status(:unprocessable_content) + expect(response).to render_template(:show) + expect(response.body).to include(I18n.t("activemodel.errors.models.resume_options_input.attributes.resume_options.blank")) + end + end + end end From f7d9aa450ca15a93215662fed4b8cb84c06a56db Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 14:52:07 +0000 Subject: [PATCH 14/24] Rename ResumeProgressController to ContinueSavedFormController Was too similar to ResumeOptionsController and confused me. --- .../forms/continue_saved_form_controller.rb | 2 ++ .../forms/resume_options_controller.rb | 2 +- .../forms/resume_progress_controller.rb | 2 -- config/routes.rb | 4 ++-- .../forms/resume_options_controller_spec.rb | 2 +- .../continue_saved_form_routing_spec.rb | 19 +++++++++++++++++++ spec/routing/resume_progress_routing_spec.rb | 19 ------------------- 7 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 app/controllers/forms/continue_saved_form_controller.rb delete mode 100644 app/controllers/forms/resume_progress_controller.rb create mode 100644 spec/routing/continue_saved_form_routing_spec.rb delete mode 100644 spec/routing/resume_progress_routing_spec.rb diff --git a/app/controllers/forms/continue_saved_form_controller.rb b/app/controllers/forms/continue_saved_form_controller.rb new file mode 100644 index 000000000..e6dc49df3 --- /dev/null +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -0,0 +1,2 @@ +class Forms::ContinueSavedFormController < ApplicationController +end diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index 22e1f8ff0..0bb932388 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -14,7 +14,7 @@ def continue return redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) unless @resume_options_input.continue_saved? - redirect_to resume_progress_path(form_id: @current_context.form.id, form_slug: @current_context.form.form_slug) + redirect_to continue_saved_form_path(form_id: @current_context.form.id, form_slug: @current_context.form.form_slug) end private diff --git a/app/controllers/forms/resume_progress_controller.rb b/app/controllers/forms/resume_progress_controller.rb deleted file mode 100644 index b1f51dff4..000000000 --- a/app/controllers/forms/resume_progress_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Forms::ResumeProgressController < ApplicationController -end diff --git a/config/routes.rb b/config/routes.rb index 074ce56a5..c01b73dc4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,8 +40,8 @@ post "/#{CheckYourAnswersStep::CHECK_YOUR_ANSWERS_STEP_SLUG}" => "forms/check_your_answers#submit_answers", as: :form_submit_answers get "what-do-you-want-to-do" => "forms/resume_options#show", as: :resume_options post "what-do-you-want-to-do" => "forms/resume_options#continue", as: :resume_options_continue - get "continue-saved-form" => "forms/resume_progress#new", as: :resume_progress - get "continue-saved-form/signed-in" => "forms/resume_progress#signed_in", as: :resume_progress_signed_in + get "continue-saved-form" => "forms/continue_saved_form#new", as: :continue_saved_form + get "continue-saved-form/signed-in" => "forms/continue_saved_form#signed_in", as: :continue_saved_form_signed_in get "/save-your-progress" => "forms/save_progress#new", as: :save_progress get "/save-your-progress/after-sign-in" => "forms/save_progress#after_sign_in", as: :save_progress_after_sign_in post "/save-your-progress" => "forms/save_progress#create", as: :create_save_progress diff --git a/spec/requests/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb index f6d273455..9318c11c0 100644 --- a/spec/requests/forms/resume_options_controller_spec.rb +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -61,7 +61,7 @@ let(:resume_options) { "continue_saved" } it "redirects to resume their saved progress" do - expect(response).to redirect_to(resume_progress_path(form.form_id, form.form_slug, mode:)) + expect(response).to redirect_to(continue_saved_form_path(form.form_id, form.form_slug, mode:)) end end diff --git a/spec/routing/continue_saved_form_routing_spec.rb b/spec/routing/continue_saved_form_routing_spec.rb new file mode 100644 index 000000000..0ad55bbf1 --- /dev/null +++ b/spec/routing/continue_saved_form_routing_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe Forms::ContinueSavedFormController, type: :routing do + describe "routing" do + describe "#continue_saved_form" do + it "routes to #new" do + expect(get: continue_saved_form_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/continue_saved_form#new", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + + describe "#continue_saved_form_signed_in" do + it "routes to #signed_in" do + expect(get: continue_saved_form_signed_in_path(mode: "form", form_id: 999, form_slug: "valid-slug")) + .to route_to("forms/continue_saved_form#signed_in", mode: "form", form_id: "999", form_slug: "valid-slug") + end + end + end +end diff --git a/spec/routing/resume_progress_routing_spec.rb b/spec/routing/resume_progress_routing_spec.rb deleted file mode 100644 index 5af465eb2..000000000 --- a/spec/routing/resume_progress_routing_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "rails_helper" - -RSpec.describe Forms::ResumeProgressController, type: :routing do - describe "routing" do - describe "#resume_progress" do - it "routes to #new" do - expect(get: resume_progress_path(mode: "form", form_id: 999, form_slug: "valid-slug")) - .to route_to("forms/resume_progress#new", mode: "form", form_id: "999", form_slug: "valid-slug") - end - end - - describe "#resume_progress_signed_in" do - it "routes to #signed_in" do - expect(get: resume_progress_signed_in_path(mode: "form", form_id: 999, form_slug: "valid-slug")) - .to route_to("forms/resume_progress#signed_in", mode: "form", form_id: "999", form_slug: "valid-slug") - end - end - end -end From f5d673359ce9266d32611c1817acedd5f1fd0916 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 14:54:18 +0000 Subject: [PATCH 15/24] Update ContinueSavedFormController and views Logic to redirect correctly after logged in or display a new screen telling the user that an IncompleteSubmission could not be found. --- .../forms/continue_saved_form_controller.rb | 27 ++++++- .../forms/continue_saved_form/new.html.erb | 0 .../continue_saved_form/not_found.html.erb | 11 +++ config/locales/cy.yml | 6 +- config/locales/en.yml | 6 +- .../continue_saved_form_controller_spec.rb | 78 +++++++++++++++++++ 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 app/views/forms/continue_saved_form/new.html.erb create mode 100644 app/views/forms/continue_saved_form/not_found.html.erb create mode 100644 spec/requests/forms/continue_saved_form_controller_spec.rb diff --git a/app/controllers/forms/continue_saved_form_controller.rb b/app/controllers/forms/continue_saved_form_controller.rb index e6dc49df3..e38b22741 100644 --- a/app/controllers/forms/continue_saved_form_controller.rb +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -1,2 +1,27 @@ -class Forms::ContinueSavedFormController < ApplicationController +module Forms + class ContinueSavedFormController < BaseController + before_action :redirect_if_save_and_return_disabled + + def new + load_saved_progress_and_redirect + end + + private + + def load_saved_progress_and_redirect + incomplete_submission = LoadFormProgressService.new(current_context:, user_id: auth_service.sub).load! + return redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) if incomplete_submission + + current_context.clear + current_context.clear_submission_details + + render :not_found, status: :not_found + end + + def redirect_if_save_and_return_disabled + return if current_context.form.save_and_return_enabled? + + redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.next_step_slug) + end + end end diff --git a/app/views/forms/continue_saved_form/new.html.erb b/app/views/forms/continue_saved_form/new.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/forms/continue_saved_form/not_found.html.erb b/app/views/forms/continue_saved_form/not_found.html.erb new file mode 100644 index 000000000..0a9a21297 --- /dev/null +++ b/app/views/forms/continue_saved_form/not_found.html.erb @@ -0,0 +1,11 @@ +<% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> + +
+
+

<%= t('.title') %>

+ + <%= t('.body_html') %> + + <%= govuk_button_link_to t('.start_new_form_button'), form_step_path(@form.id, @form.form_slug, @form.start_page) %> +
+
diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 7eb013219..b04fd372b 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -403,6 +403,11 @@ cy: view_english_form: 'Gallwch lenwi’r ffurflen yn Saesneg o hyd:' welsh_version_archived: Mae’r fersiwn Gymraeg o’r ffurflen hon wedi’i harchifo ac nid yw ar gael mwyach. back: Yn ôl + continue_saved_form: + not_found: + body_html:

This could be because:

  • you did not save your progress for this form
  • you used a different GOV.UK One Login to save the form
  • it has been more than 30 days since you saved the form (we delete saved forms after 30 days)
+ start_new_form_button: Start a new form + title: We could not find your saved form continue_to_one_login: show: body_html: "

Bydd yr e-bost gyda’ch atebion yn cael ei anfon i’r cyfeiriad e-bost sy’n gysylltiedig â’ch GOV.UK One Login.

Os ydych angen i’r atebion gael eu hanfon i gyfeiriad e-bost gwahanol - er enghraifft, eich e-bost gwaith - gallwch greu GOV.UK One Login newydd.

" @@ -431,7 +436,6 @@ cy: title: Tynnu’ch ffeil resume_options: show: - heading: What do you want to do? title: What do you want to do? review_file: show: diff --git a/config/locales/en.yml b/config/locales/en.yml index a60bb3dc8..9046dd076 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -403,6 +403,11 @@ en: view_english_form: 'You can still complete the form in English:' welsh_version_archived: The Welsh version of this form has been archived and is no longer available. back: Back + continue_saved_form: + not_found: + body_html:

This could be because:

  • you did not save your progress for this form
  • you used a different GOV.UK One Login to save the form
  • it has been more than 30 days since you saved the form (we delete saved forms after 30 days)
+ start_new_form_button: Start a new form + title: We could not find your saved form continue_to_one_login: show: body_html: "

The email with your answers will be sent to the email address linked to your GOV.UK One Login.

If you need the answers sent to a different email address - for example, your work email - you can create a new GOV.UK One Login.

" @@ -431,7 +436,6 @@ en: title: Remove your file resume_options: show: - heading: What do you want to do? title: What do you want to do? review_file: show: diff --git a/spec/requests/forms/continue_saved_form_controller_spec.rb b/spec/requests/forms/continue_saved_form_controller_spec.rb new file mode 100644 index 000000000..e11cb3704 --- /dev/null +++ b/spec/requests/forms/continue_saved_form_controller_spec.rb @@ -0,0 +1,78 @@ +require "rails_helper" + +RSpec.describe Forms::ContinueSavedFormController, type: :request do + let(:form) do + build(:form_document, :with_support, form_id: 2, start_page: 1, steps:, save_and_return: "enabled") + end + + let(:steps) do + [ + build(:question_step, :with_text_settings, id: 1, next_step_id: 2), + build(:question_step, :with_text_settings, id: 2), + ] + end + + let(:req_headers) { { "Accept" => "application/json" } } + let(:api_url_suffix) { "/draft" } + let(:mode) { "preview-draft" } + + let(:auth_session) { {} } + + let(:store) { auth_session.with_indifferent_access } + + before do + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/api/v2/forms/#{form.form_id}#{api_url_suffix}", req_headers, form.to_json, 200 + end + + allow(Flow::Context).to receive(:new).and_wrap_original do |original_method, *args| + original_method.call(form: args[0][:form], form_document: args[0][:form_document], store:) + end + allow(AuthService).to receive(:new).and_wrap_original do |original_method, *_args| + original_method.call(store) + end + end + + describe "GET #new" do + context "when the user is signed in to GOV.UK One Login" do + let(:auth_session) do + { + auth: { + sub: "user-sub-123", + email: "test@example.com", + token: "a-token", + authenticated_at: Time.current.to_i, + }, + } + end + + context "and they have saved progress for the form" do + before do + IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }) + + get continue_saved_form_path(mode:, form_id: form.form_id, form_slug: form.form_slug) + end + + it "redirects to the first question" do + expect(response).to redirect_to(form_step_path(mode:, form_id: form.form_id, form_slug: form.form_slug, step_slug: 1)) + end + end + + context "and they have no saved progress for the form" do + before do + get continue_saved_form_path(mode:, form_id: form.form_id, form_slug: form.form_slug) + end + + it "shows the page saying their saved form could not be found" do + expect(response).to have_http_status(:not_found) + expect(response).to render_template(:not_found) + expect(response.body).to include(I18n.t("forms.continue_saved_form.not_found.title")) + end + + it "links to start a new form" do + expect(response.body).to include(form_step_path(mode:, form_id: form.form_id, form_slug: form.form_slug, step_slug: 1)) + end + end + end + end +end From 34c14cdd3b57e8fa28bca7ab5e6b621a0c88cc97 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 15:05:18 +0000 Subject: [PATCH 16/24] Update to handle continuing a saved form submission when user is not logged in --- .../forms/continue_saved_form_controller.rb | 9 +++++++- app/lib/store/return_from_one_login_store.rb | 1 + .../forms/continue_saved_form/new.html.erb | 16 ++++++++++++++ config/locales/cy.yml | 4 ++++ config/locales/en.yml | 4 ++++ .../continue_saved_form_controller_spec.rb | 22 +++++++++++++++++++ 6 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/controllers/forms/continue_saved_form_controller.rb b/app/controllers/forms/continue_saved_form_controller.rb index e38b22741..aef99d0f5 100644 --- a/app/controllers/forms/continue_saved_form_controller.rb +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -3,7 +3,14 @@ class ContinueSavedFormController < BaseController before_action :redirect_if_save_and_return_disabled def new - load_saved_progress_and_redirect + return load_saved_progress_and_redirect if auth_service.logged_in? + + auth_service.store_return_params( + form: current_context.form, + mode:, + locale: locale_param, + return_to: Store::ReturnFromOneLoginStore::RETURN_TO_VALUES[:continue_saved_form], + ) end private diff --git a/app/lib/store/return_from_one_login_store.rb b/app/lib/store/return_from_one_login_store.rb index f3fb0cb46..4b22d2f4e 100644 --- a/app/lib/store/return_from_one_login_store.rb +++ b/app/lib/store/return_from_one_login_store.rb @@ -16,6 +16,7 @@ def initialize(message = "Return from One Login parameters are missing from the RETURN_TO_VALUES = { copy_of_answers: "copy_of_answers", save_and_return: "save_and_return", + continue_saved_form: "continue_saved_form", }.freeze def initialize(store) diff --git a/app/views/forms/continue_saved_form/new.html.erb b/app/views/forms/continue_saved_form/new.html.erb index e69de29bb..68d6d78a8 100644 --- a/app/views/forms/continue_saved_form/new.html.erb +++ b/app/views/forms/continue_saved_form/new.html.erb @@ -0,0 +1,16 @@ +<% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> + +
+
+

<%= t('.title') %>

+ + <%= t('.body_html') %> + + <%= govuk_button_to( + t('.continue_to_one_login_button'), + "/auth/govuk_one_login", + method: :post, + form: { data: { turbo: false } }, + ) %> +
+
diff --git a/config/locales/cy.yml b/config/locales/cy.yml index b04fd372b..e466c8606 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -404,6 +404,10 @@ cy: welsh_version_archived: Mae’r fersiwn Gymraeg o’r ffurflen hon wedi’i harchifo ac nid yw ar gael mwyach. back: Yn ôl continue_saved_form: + new: + body_html: "

We use GOV.UK One Login to keep your information secure.

Sign in to your GOV.UK One Login to continue filling in your saved form.

" + continue_to_one_login_button: Continue to GOV.UK One Login + title: Sign in to your GOV.UK One Login not_found: body_html:

This could be because:

  • you did not save your progress for this form
  • you used a different GOV.UK One Login to save the form
  • it has been more than 30 days since you saved the form (we delete saved forms after 30 days)
start_new_form_button: Start a new form diff --git a/config/locales/en.yml b/config/locales/en.yml index 9046dd076..2165766c1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -404,6 +404,10 @@ en: welsh_version_archived: The Welsh version of this form has been archived and is no longer available. back: Back continue_saved_form: + new: + body_html: "

We use GOV.UK One Login to keep your information secure.

Sign in to your GOV.UK One Login to continue filling in your saved form.

" + continue_to_one_login_button: Continue to GOV.UK One Login + title: Sign in to your GOV.UK One Login not_found: body_html:

This could be because:

  • you did not save your progress for this form
  • you used a different GOV.UK One Login to save the form
  • it has been more than 30 days since you saved the form (we delete saved forms after 30 days)
start_new_form_button: Start a new form diff --git a/spec/requests/forms/continue_saved_form_controller_spec.rb b/spec/requests/forms/continue_saved_form_controller_spec.rb index e11cb3704..11535b027 100644 --- a/spec/requests/forms/continue_saved_form_controller_spec.rb +++ b/spec/requests/forms/continue_saved_form_controller_spec.rb @@ -34,6 +34,28 @@ end describe "GET #new" do + context "when the user is not signed in to GOV.UK One Login" do + before do + get continue_saved_form_path(mode:, form_id: form.form_id, form_slug: form.form_slug) + end + + it "renders the One Login sign-in interstitial" do + expect(response).to have_http_status(:ok) + expect(response).to render_template(:new) + expect(response.body).to include(I18n.t("forms.continue_saved_form.new.title")) + end + + it "stores the continue saved form return params for returning from One Login on the session" do + expect(store["return_from_one_login"]).to eq({ + "last_form_id" => form.form_id, + "last_form_slug" => form.form_slug, + "last_mode" => mode.to_s, + "last_locale" => nil, + "return_to" => "continue_saved_form", + }) + end + end + context "when the user is signed in to GOV.UK One Login" do let(:auth_session) do { From bcb97ce2ac12414b2685f2132317d41f2ed6c132 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 15:09:12 +0000 Subject: [PATCH 17/24] Update resuming journey after user logs in --- .../forms/continue_saved_form_controller.rb | 9 +++ app/controllers/users/omniauth_controller.rb | 2 + .../continue_saved_form_controller_spec.rb | 68 +++++++++++++++++++ .../users/omniauth_controller_spec.rb | 16 +++++ 4 files changed, 95 insertions(+) diff --git a/app/controllers/forms/continue_saved_form_controller.rb b/app/controllers/forms/continue_saved_form_controller.rb index aef99d0f5..c5c67244a 100644 --- a/app/controllers/forms/continue_saved_form_controller.rb +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -13,6 +13,15 @@ def new ) end + def signed_in + unless auth_service.logged_in? && auth_service.return_to == Store::ReturnFromOneLoginStore::RETURN_TO_VALUES[:continue_saved_form] + return redirect_to(continue_saved_form_path(current_context.form.id, current_context.form.form_slug)) + end + + auth_service.clear_return_from_one_login + load_saved_progress_and_redirect + end + private def load_saved_progress_and_redirect diff --git a/app/controllers/users/omniauth_controller.rb b/app/controllers/users/omniauth_controller.rb index 03fe8cd65..2e595f208 100644 --- a/app/controllers/users/omniauth_controller.rb +++ b/app/controllers/users/omniauth_controller.rb @@ -54,6 +54,8 @@ def logged_out def redirect_after_sign_in if auth_service.return_to == Store::ReturnFromOneLoginStore::RETURN_TO_VALUES[:save_and_return] redirect_to save_progress_after_sign_in_path(**auth_service.form_path_params) + elsif auth_service.return_to == Store::ReturnFromOneLoginStore::RETURN_TO_VALUES[:continue_saved_form] + redirect_to continue_saved_form_signed_in_path(**auth_service.form_path_params) else # "copy_of_answers" as the default to help avoid regression issues when deploying for current users redirect_to check_your_answers_path(**auth_service.form_path_params) end diff --git a/spec/requests/forms/continue_saved_form_controller_spec.rb b/spec/requests/forms/continue_saved_form_controller_spec.rb index 11535b027..543ba325f 100644 --- a/spec/requests/forms/continue_saved_form_controller_spec.rb +++ b/spec/requests/forms/continue_saved_form_controller_spec.rb @@ -97,4 +97,72 @@ end end end + + describe "GET #signed_in" do + def get_signed_in + get continue_saved_form_signed_in_path(mode:, form_id: form.form_id, form_slug: form.form_slug) + end + + let(:signed_in_session) do + { + auth: { + sub: "user-sub-123", + email: "test@example.com", + token: "a-token", + authenticated_at: Time.current.to_i, + }, + } + end + + context "when the user is not signed in to GOV.UK One Login" do + before { get_signed_in } + + it "redirects to the sign-in interstitial" do + expect(response).to redirect_to(continue_saved_form_path(form.form_id, form.form_slug, mode:)) + end + end + + context "when the user is signed in but did not start the continue saved form journey" do + let(:auth_session) { signed_in_session } + + before { get_signed_in } + + it "redirects to the sign-in interstitial" do + expect(response).to redirect_to(continue_saved_form_path(form.form_id, form.form_slug, mode:)) + end + end + + context "when the user is signed in after starting the continue saved form journey" do + let(:auth_session) { signed_in_session.merge(return_from_one_login: { return_to: "continue_saved_form" }) } + + context "and they have saved progress for the form" do + before do + IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }) + + get_signed_in + end + + it "redirects to the first question" do + expect(response).to redirect_to(form_step_path(mode:, form_id: form.form_id, form_slug: form.form_slug, step_slug: 1)) + end + + it "loads the saved answers into the session" do + expect(store.dig(:answers, form.form_id.to_s)).to eq({ "1" => { "text" => "a saved answer" } }) + end + + it "clears the return from One Login params" do + expect(store).not_to have_key(:return_from_one_login) + end + end + + context "and they have no saved progress for the form" do + before { get_signed_in } + + it "shows the page saying their saved form could not be found" do + expect(response).to have_http_status(:not_found) + expect(response).to render_template(:not_found) + end + end + end + end end diff --git a/spec/requests/users/omniauth_controller_spec.rb b/spec/requests/users/omniauth_controller_spec.rb index 7d32d649c..a7a4ba859 100644 --- a/spec/requests/users/omniauth_controller_spec.rb +++ b/spec/requests/users/omniauth_controller_spec.rb @@ -135,6 +135,22 @@ end end + context "when the session says to return to continue a saved form" do + let(:return_from_one_login_session) do + { + "last_form_id" => form_id, + "last_form_slug" => form_slug, + "last_mode" => mode, + "last_locale" => locale, + "return_to" => "continue_saved_form", + } + end + + it "redirects to the continue saved form signed in action" do + expect(response).to redirect_to(continue_saved_form_signed_in_path(form_id:, form_slug:, mode:, locale:)) + end + end + context "when the session says to return to copy of answers" do let(:return_from_one_login_session) do { From 4e8dbf738870b6903f13adf7afc818d406a5d828 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 15:51:37 +0000 Subject: [PATCH 18/24] Update content for parts of Save and return --- app/mailers/progress_saved_mailer.rb | 2 +- .../continue_saved_form/not_found.html.erb | 4 +++ app/views/forms/save_progress/show.html.erb | 5 ++- config/locales/cy.yml | 3 +- config/locales/en.yml | 3 +- spec/features/save_form_progress_spec.rb | 2 +- spec/mailers/progress_saved_mailer_spec.rb | 6 ++-- .../not_found.html.erb_spec.rb | 34 +++++++++++++++++++ 8 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 spec/views/forms/continue_saved_form/not_found.html.erb_spec.rb diff --git a/app/mailers/progress_saved_mailer.rb b/app/mailers/progress_saved_mailer.rb index e395c9297..c6fb6047f 100644 --- a/app/mailers/progress_saved_mailer.rb +++ b/app/mailers/progress_saved_mailer.rb @@ -23,7 +23,7 @@ def progress_saved_email( end def resume_form_link(form, mode, locale: nil) - path = Rails.application.routes.url_helpers.form_path(mode:, form_id: form.id, form_slug: form.form_slug, locale:) + path = Rails.application.routes.url_helpers.continue_saved_form_path(mode:, form_id: form.id, form_slug: form.form_slug, locale:) "#{Settings.forms_runner.base_url}#{path}" end diff --git a/app/views/forms/continue_saved_form/not_found.html.erb b/app/views/forms/continue_saved_form/not_found.html.erb index 0a9a21297..19da5aad4 100644 --- a/app/views/forms/continue_saved_form/not_found.html.erb +++ b/app/views/forms/continue_saved_form/not_found.html.erb @@ -1,5 +1,9 @@ <% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> +<% content_for :back_link do %> + <%= link_to t("forms.back"), resume_options_path(@form.id, @form.form_slug), class: "govuk-back-link" %> +<% end %> +

<%= t('.title') %>

diff --git a/app/views/forms/save_progress/show.html.erb b/app/views/forms/save_progress/show.html.erb index 9fc582065..7413c6589 100644 --- a/app/views/forms/save_progress/show.html.erb +++ b/app/views/forms/save_progress/show.html.erb @@ -6,7 +6,10 @@ <% banner.with_heading(text: t('.title')) %> <% end %> - <%= t('.body_html', email: @one_login_email, deletion_date: @deletion_date, form_url: form_url(@current_context.form.id, @current_context.form.form_slug)) %> + <%= t('.body_html', email: @one_login_email, deletion_date: @deletion_date, form_url: continue_saved_form_url(@current_context.form.id, @current_context.form.form_slug)) %> + + <%= govuk_button_link_to t('.continue_button'), form_step_path(@current_context.form.id, @current_context.form.form_slug, @current_context.next_step_slug) %> +

<%= link_to t('.sign_out_link'), "#" %>

diff --git a/config/locales/cy.yml b/config/locales/cy.yml index e466c8606..47fb599de 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -456,8 +456,9 @@ cy: title: Use GOV.UK One Login to save your progress show: body_html: '

We’ve sent a link to %{email} so you can continue filling in the form when you are ready. This is the email address you use to sign in with GOV.UK One Login.

Use this link to continue filling in the form: %{form_url}

We’ll delete your saved form on %{deletion_date}.

' + continue_button: Continue filling the form sign_out_link: Sign out of GOV.UK One Login - title: Your form has been saved + title: Your progress has been saved gov_uk_forms: GOV.UK Forms govuk_components: govuk_summary_list: diff --git a/config/locales/en.yml b/config/locales/en.yml index 2165766c1..f3b2b413e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -456,8 +456,9 @@ en: title: Use GOV.UK One Login to save your progress show: body_html: '

We’ve sent a link to %{email} so you can continue filling in the form when you are ready. This is the email address you use to sign in with GOV.UK One Login.

Use this link to continue filling in the form: %{form_url}

We’ll delete your saved form on %{deletion_date}.

' + continue_button: Continue filling the form sign_out_link: Sign out of GOV.UK One Login - title: Your form has been saved + title: Your progress has been saved gov_uk_forms: GOV.UK Forms govuk_components: govuk_summary_list: diff --git a/spec/features/save_form_progress_spec.rb b/spec/features/save_form_progress_spec.rb index 318937892..7c5ecabc9 100644 --- a/spec/features/save_form_progress_spec.rb +++ b/spec/features/save_form_progress_spec.rb @@ -86,7 +86,7 @@ def when_i_continue_to_one_login end def then_i_see_the_confirmation_page - expect(page).to have_css(".govuk-notification-banner--success", text: "Your form has been saved") + expect(page).to have_css(".govuk-notification-banner--success", text: "Your progress has been saved") expect(page).to have_link "Sign out of GOV.UK One Login" end diff --git a/spec/mailers/progress_saved_mailer_spec.rb b/spec/mailers/progress_saved_mailer_spec.rb index 21278be21..b05a15ea3 100644 --- a/spec/mailers/progress_saved_mailer_spec.rb +++ b/spec/mailers/progress_saved_mailer_spec.rb @@ -20,7 +20,7 @@ updated_at: Time.zone.local(2026, 10, 12, 12, 0, 0), ) end - let(:resume_form_link) { "#{Settings.forms_runner.base_url}/form/#{form_document.form_id}/my-form" } + let(:resume_form_link) { "#{Settings.forms_runner.base_url}/form/#{form_document.form_id}/my-form/continue-saved-form" } let(:form_document) do build(:form_document, name: "My form", @@ -91,7 +91,7 @@ end context "when the incomplete submission is for a draft preview" do - let(:preview_draft_resume_form_link) { "#{Settings.forms_runner.base_url}/preview-draft/#{form_document.form_id}/my-form" } + let(:preview_draft_resume_form_link) { "#{Settings.forms_runner.base_url}/preview-draft/#{form_document.form_id}/my-form/continue-saved-form" } before do incomplete_submission.mode = "preview-draft" @@ -138,7 +138,7 @@ context "when the saved answers are in Welsh" do let(:locale) { "cy" } - let(:welsh_resume_form_link) { "#{Settings.forms_runner.base_url}/form/#{form_document.form_id}/welsh-form.cy" } + let(:welsh_resume_form_link) { "#{Settings.forms_runner.base_url}/form/#{form_document.form_id}/welsh-form.cy/continue-saved-form" } let(:welsh_form_document) do build(:form_document, form_id: form_document.form_id, diff --git a/spec/views/forms/continue_saved_form/not_found.html.erb_spec.rb b/spec/views/forms/continue_saved_form/not_found.html.erb_spec.rb new file mode 100644 index 000000000..021940ae5 --- /dev/null +++ b/spec/views/forms/continue_saved_form/not_found.html.erb_spec.rb @@ -0,0 +1,34 @@ +require "rails_helper" + +describe "forms/continue_saved_form/not_found.html.erb" do + let(:form) { build :form, save_and_return: "enabled", start_page: 1 } + let(:mode) { OpenStruct.new(preview_draft?: false, preview_archived?: false, preview_live?: false) } + let(:first_question_path) { "/form/#{form.id}/#{form.form_slug}/#{form.start_page}" } + let(:resume_options_path) { "/form/#{form.id}/#{form.form_slug}/what-do-you-want-to-do" } + + before do + assign(:current_context, OpenStruct.new(form:)) + assign(:form, form) + assign(:mode, mode) + + allow(view).to receive_messages(form_step_path: first_question_path, resume_options_path:) + + render + end + + it "has the correct page title" do + expect(view.content_for(:title)).to eq "#{I18n.t('forms.continue_saved_form.not_found.title')} - #{form.name}" + end + + it "has the correct heading" do + expect(rendered).to have_css("h1", text: I18n.t("forms.continue_saved_form.not_found.title")) + end + + it "has a back link to the page asking whether to start a new form or continue a saved one" do + expect(view.content_for(:back_link)).to have_link(I18n.t("forms.back"), href: resume_options_path) + end + + it "has a button to start a new form" do + expect(rendered).to have_link(I18n.t("forms.continue_saved_form.not_found.start_new_form_button"), href: first_question_path, class: "govuk-button") + end +end From 4279d09876d66b22e78dd75b3908fdd1554f081b Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Fri, 30 Oct 2026 15:52:23 +0000 Subject: [PATCH 19/24] Update view spec to test for a button to continue filling in a form --- .../views/forms/save_progress/show.html.erb_spec.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/views/forms/save_progress/show.html.erb_spec.rb b/spec/views/forms/save_progress/show.html.erb_spec.rb index 1d0650103..4c3aa35e0 100644 --- a/spec/views/forms/save_progress/show.html.erb_spec.rb +++ b/spec/views/forms/save_progress/show.html.erb_spec.rb @@ -4,17 +4,20 @@ let(:form) { build :form, save_and_return: "enabled" } let(:one_login_email) { "test@example.com" } let(:deletion_date) { "13/11/2026" } - let(:form_url) { "http://www.example.com/form/#{form.id}/#{form.form_slug}" } + let(:form_url) { "http://www.example.com/form/#{form.id}/#{form.form_slug}/continue-saved-form" } let(:mode) { OpenStruct.new(preview_draft?: false, preview_archived?: false, preview_live?: false) } + let(:next_step_slug) { "2" } + let(:next_step_path) { "/form/#{form.id}/#{form.form_slug}/#{next_step_slug}" } before do - assign(:current_context, OpenStruct.new(form:)) + assign(:current_context, OpenStruct.new(form:, next_step_slug:)) assign(:form, form) assign(:mode, mode) assign(:one_login_email, one_login_email) assign(:deletion_date, deletion_date) - allow(view).to receive(:form_url).with(form.id, form.form_slug).and_return(form_url) + allow(view).to receive(:continue_saved_form_url).with(form.id, form.form_slug).and_return(form_url) + allow(view).to receive(:form_step_path).with(form.id, form.form_slug, next_step_slug).and_return(next_step_path) render end @@ -39,6 +42,10 @@ expect(rendered).to have_link(form_url, href: form_url) end + it "has a button to continue filling the form from the next incomplete step" do + expect(rendered).to have_link(I18n.t("forms.save_progress.show.continue_button"), href: next_step_path, class: "govuk-button") + end + it "has a link to sign out of GOV.UK One Login" do expect(rendered).to have_link(I18n.t("forms.save_progress.show.sign_out_link")) end From 3a97b49a02a8b791a1dd295bc6835fc9ed960449 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Mon, 2 Nov 2026 12:07:24 +0000 Subject: [PATCH 20/24] Add feature spec for continuing submitting a form --- spec/features/continue_saved_form_spec.rb | 139 ++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 spec/features/continue_saved_form_spec.rb diff --git a/spec/features/continue_saved_form_spec.rb b/spec/features/continue_saved_form_spec.rb new file mode 100644 index 000000000..113ac1e6e --- /dev/null +++ b/spec/features/continue_saved_form_spec.rb @@ -0,0 +1,139 @@ +require "rails_helper" + +feature "Continue a saved form", type: :feature do + let(:first_question_text) { "What is your name?" } + let(:second_question_text) { "What is your quest?" } + + let(:steps) do + [ + build(:question_step, :with_text_settings, id: 1, next_step_id: 2, question_text: first_question_text), + build(:question_step, :with_text_settings, id: 2, question_text: second_question_text), + ] + end + + let(:form) do + build(:form_document, :live, form_id: 1, name: "Apply for a juggling licence", steps:, start_page: 1, save_and_return: "enabled") + end + + let(:req_headers) { { "Accept" => "application/json" } } + + let(:one_login_sub) { "user-sub-abc" } + let(:one_login_email) { "filler@example.com" } + let(:id_token) { JWT.encode({ iat: Time.current.to_i }, nil, "none") } + let(:auth_hash) do + { + provider: :govuk_one_login, + uid: one_login_sub, + info: { email: one_login_email }, + credentials: { id_token: }, + } + end + + before do + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/api/v2/forms/1/live", req_headers, form.to_json, 200 + end + + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:default] = OmniAuth::AuthHash.new(auth_hash) + end + + after do + OmniAuth.config.test_mode = false + OmniAuth.config.mock_auth[:default] = nil + end + + scenario "As a form filler, I sign in with GOV.UK One Login to continue filling in my saved form" do + given_i_have_saved_progress_for_the_form + + when_i_visit_the_form_start_page + and_i_choose_to_continue_filling_in_a_saved_form + then_i_see_the_sign_in_interstitial + + when_i_continue_to_one_login + then_i_see_the_first_question_with_my_saved_answer + + when_i_continue_to_the_next_question + then_i_see_the_second_question + end + + scenario "As a form filler with no saved form, I can start a new form instead" do + when_i_visit_the_form_start_page + and_i_choose_to_continue_filling_in_a_saved_form + and_i_continue_to_one_login + then_i_see_that_my_saved_form_could_not_be_found + + when_i_choose_to_start_a_new_form + then_i_see_the_first_question_with_no_answer + end + + scenario "As a form filler, I choose to start a new form" do + when_i_visit_the_form_start_page + and_i_choose_to_start_a_new_form + then_i_see_the_first_question_with_no_answer + end + + def given_i_have_saved_progress_for_the_form + IncompleteSubmission.create!( + user_id: one_login_sub, + form_id: 1, + form_version: form.version, + answers: { "1" => { "text" => "Sir Lancelot" } }, + locale: "en", + mode: "form", + submission_reference: "SAVEDREF", + ) + end + + def when_i_visit_the_form_start_page + visit form_path(mode: "form", form_id: 1, form_slug: form.form_slug) + expect(page.find("h1")).to have_text "What do you want to do?" + end + + def and_i_choose_to_continue_filling_in_a_saved_form + choose "Continue filling in a saved form" + click_button "Continue" + end + + def and_i_choose_to_start_a_new_form + choose "Start a new form" + click_button "Continue" + end + + def then_i_see_the_sign_in_interstitial + expect(page.find("h1")).to have_text "Sign in to your GOV.UK One Login" + expect(page).to have_button "Continue to GOV.UK One Login" + end + + def when_i_continue_to_one_login + click_button "Continue to GOV.UK One Login" + end + + alias_method :and_i_continue_to_one_login, :when_i_continue_to_one_login + + def then_i_see_the_first_question_with_my_saved_answer + expect(page.find("h1")).to have_text first_question_text + expect(page).to have_field(first_question_text, with: "Sir Lancelot") + end + + def when_i_continue_to_the_next_question + click_button "Continue" + end + + def then_i_see_the_second_question + expect(page.find("h1")).to have_text second_question_text + end + + def then_i_see_that_my_saved_form_could_not_be_found + expect(page.find("h1")).to have_text "We could not find your saved form" + end + + def when_i_choose_to_start_a_new_form + click_link "Start a new form" + end + + def then_i_see_the_first_question_with_no_answer + expect(page.find("h1")).to have_text first_question_text + expect(page).to have_field(first_question_text, with: "") + end +end From 3392ec5cc9c9832cfae90185a715d85c571bc8d1 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Tue, 3 Nov 2026 16:34:13 +0000 Subject: [PATCH 21/24] Fix clearing session data when choosing Start a new form --- .../forms/resume_options_controller.rb | 9 ++++++++- spec/features/continue_saved_form_spec.rb | 15 ++++++++++++++ .../forms/resume_options_controller_spec.rb | 20 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb index 0bb932388..f233f29c3 100644 --- a/app/controllers/forms/resume_options_controller.rb +++ b/app/controllers/forms/resume_options_controller.rb @@ -12,13 +12,20 @@ def continue return render :show, status: :unprocessable_entity end - return redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) unless @resume_options_input.continue_saved? + return start_new_form unless @resume_options_input.continue_saved? redirect_to continue_saved_form_path(form_id: @current_context.form.id, form_slug: @current_context.form.form_slug) end private + def start_new_form + current_context.clear + current_context.clear_submission_details + + redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) + end + def back_to_start unless @current_context.form.save_and_return_enabled? redirect_to form_step_path(@current_context.form.id, diff --git a/spec/features/continue_saved_form_spec.rb b/spec/features/continue_saved_form_spec.rb index 113ac1e6e..51a47141a 100644 --- a/spec/features/continue_saved_form_spec.rb +++ b/spec/features/continue_saved_form_spec.rb @@ -73,6 +73,14 @@ then_i_see_the_first_question_with_no_answer end + scenario "As a form filler who has already answered some questions, I choose to start a new form" do + given_i_have_answered_the_first_question + + when_i_visit_the_form_start_page + and_i_choose_to_start_a_new_form + then_i_see_the_first_question_with_no_answer + end + def given_i_have_saved_progress_for_the_form IncompleteSubmission.create!( user_id: one_login_sub, @@ -85,6 +93,13 @@ def given_i_have_saved_progress_for_the_form ) end + def given_i_have_answered_the_first_question + visit form_step_path(mode: "form", form_id: 1, form_slug: form.form_slug, step_slug: 1) + fill_in first_question_text, with: "Sir Robin" + click_button "Continue" + expect(page.find("h1")).to have_text second_question_text + end + def when_i_visit_the_form_start_page visit form_path(mode: "form", form_id: 1, form_slug: form.form_slug) expect(page.find("h1")).to have_text "What do you want to do?" diff --git a/spec/requests/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb index 9318c11c0..77e24f199 100644 --- a/spec/requests/forms/resume_options_controller_spec.rb +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -16,10 +16,22 @@ let(:api_url_suffix) { "/draft" } let(:mode) { "preview-draft" } + let(:store) do + { + answers: { form.form_id.to_s => { "1" => { "text" => "an answer from an earlier visit" } } }, + locales: { form.form_id.to_s => %w[en] }, + confirmation_details: { form.form_id.to_s => { "submission_reference" => "OLDREF", "wants_copy_of_answers" => true } }, + }.with_indifferent_access + end + before do ActiveResource::HttpMock.respond_to do |mock| mock.get "/api/v2/forms/#{form.form_id}#{api_url_suffix}", req_headers, form.to_json, 200 end + + allow(Flow::Context).to receive(:new).and_wrap_original do |original_method, *args| + original_method.call(form: args[0][:form], form_document: args[0][:form_document], store:) + end end describe "GET #show" do @@ -55,6 +67,14 @@ it "redirects to the first question" do expect(response).to redirect_to(form_step_path(form.form_id, form.form_slug, 1, mode:)) end + + it "clears the answers for the form from the session" do + expect(store.dig(:answers, form.form_id.to_s)).to be_nil + end + + it "clears the submission details for the form from the session" do + expect(store.dig(:confirmation_details, form.form_id.to_s)).to be_nil + end end context "when the user chooses to continue filling in a saved form" do From ea752f690742b9185397244d09d76c221288f819 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 5 Nov 2026 11:44:57 +0000 Subject: [PATCH 22/24] Update Submission, Load and Save services to handle different modes of IncompleteSubmissions Because these are distinct submissions. --- .../forms/continue_saved_form_controller.rb | 2 +- .../forms/save_progress_controller.rb | 2 +- app/models/incomplete_submission.rb | 3 +- app/services/form_submission_service.rb | 5 +++ app/services/load_form_progress_service.rb | 5 ++- app/services/save_form_progress_service.rb | 3 +- ..._to_incomplete_submissions_unique_index.rb | 6 +++ db/schema.rb | 4 +- spec/models/incomplete_submission_spec.rb | 36 +++++++++++++++--- .../continue_saved_form_controller_spec.rb | 4 +- .../forms/save_progress_controller_spec.rb | 3 +- spec/services/form_submission_service_spec.rb | 37 +++++++++++++++++++ .../load_form_progress_service_spec.rb | 21 ++++++++++- .../save_form_progress_service_spec.rb | 20 +++++++++- 14 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 db/migrate/20261105100000_add_mode_to_incomplete_submissions_unique_index.rb diff --git a/app/controllers/forms/continue_saved_form_controller.rb b/app/controllers/forms/continue_saved_form_controller.rb index c5c67244a..51bdf7016 100644 --- a/app/controllers/forms/continue_saved_form_controller.rb +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -25,7 +25,7 @@ def signed_in private def load_saved_progress_and_redirect - incomplete_submission = LoadFormProgressService.new(current_context:, user_id: auth_service.sub).load! + incomplete_submission = LoadFormProgressService.new(current_context:, user_id: auth_service.sub, mode:).load! return redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) if incomplete_submission current_context.clear diff --git a/app/controllers/forms/save_progress_controller.rb b/app/controllers/forms/save_progress_controller.rb index c7f71d31f..45fca4d60 100644 --- a/app/controllers/forms/save_progress_controller.rb +++ b/app/controllers/forms/save_progress_controller.rb @@ -34,7 +34,7 @@ def create end def show - saved = IncompleteSubmission.find_by(user_id: auth_service.sub, form_id: current_context.form.id) + saved = IncompleteSubmission.find_by(user_id: auth_service.sub, form_id: current_context.form.id, mode: mode.to_s) return redirect_to(save_progress_path(current_context.form.id, current_context.form.form_slug)) unless saved @one_login_email = auth_service.email diff --git a/app/models/incomplete_submission.rb b/app/models/incomplete_submission.rb index aedeff547..d396c0b46 100644 --- a/app/models/incomplete_submission.rb +++ b/app/models/incomplete_submission.rb @@ -1,7 +1,8 @@ class IncompleteSubmission < ApplicationRecord encrypts :answers validates :user_id, presence: true - validates :form_id, presence: true, uniqueness: { scope: :user_id } + validates :form_id, presence: true, uniqueness: { scope: %i[user_id mode] } + validates :mode, presence: true def mode_object Mode.new(mode) diff --git a/app/services/form_submission_service.rb b/app/services/form_submission_service.rb index 52978d924..5ccbfc503 100644 --- a/app/services/form_submission_service.rb +++ b/app/services/form_submission_service.rb @@ -28,6 +28,7 @@ def submit submission = create_submission_record enqueue_deliveries(submission) + delete_saved_progress Metrics.record_submission(form_id: form.id, form_name: form.name, mode:) @@ -104,6 +105,10 @@ def resolve_submission_job_class(delivery_configuration) end end + def delete_saved_progress + IncompleteSubmission.where(form_id: form.id, mode: mode.to_s, submission_reference:).delete_all + end + def create_submission_record Submission.create!( reference: submission_reference, diff --git a/app/services/load_form_progress_service.rb b/app/services/load_form_progress_service.rb index a1c2b587f..c9a689901 100644 --- a/app/services/load_form_progress_service.rb +++ b/app/services/load_form_progress_service.rb @@ -1,11 +1,12 @@ class LoadFormProgressService - def initialize(current_context:, user_id:) + def initialize(current_context:, user_id:, mode:) @current_context = current_context @user_id = user_id + @mode = mode end def load! - incomplete_submission = IncompleteSubmission.find_by(user_id: @user_id, form_id: @current_context.form.id) + incomplete_submission = IncompleteSubmission.find_by(user_id: @user_id, form_id: @current_context.form.id, mode: @mode.to_s) return nil unless incomplete_submission @current_context.clear diff --git a/app/services/save_form_progress_service.rb b/app/services/save_form_progress_service.rb index c9b090cb5..5f1c56c01 100644 --- a/app/services/save_form_progress_service.rb +++ b/app/services/save_form_progress_service.rb @@ -7,11 +7,10 @@ def initialize(current_context:, user_id:, mode:, email:) end def save! - incomplete_submission = IncompleteSubmission.find_or_initialize_by(user_id: @user_id, form_id: @current_context.form.id) + incomplete_submission = IncompleteSubmission.find_or_initialize_by(user_id: @user_id, form_id: @current_context.form.id, mode: @mode.to_s) incomplete_submission.form_version = @current_context.form.version incomplete_submission.answers = @current_context.answers incomplete_submission.locale = @current_context.submission_locale - incomplete_submission.mode = @mode.to_s incomplete_submission.submission_reference = @current_context.get_submission_reference incomplete_submission.save! diff --git a/db/migrate/20261105100000_add_mode_to_incomplete_submissions_unique_index.rb b/db/migrate/20261105100000_add_mode_to_incomplete_submissions_unique_index.rb new file mode 100644 index 000000000..6f105f990 --- /dev/null +++ b/db/migrate/20261105100000_add_mode_to_incomplete_submissions_unique_index.rb @@ -0,0 +1,6 @@ +class AddModeToIncompleteSubmissionsUniqueIndex < ActiveRecord::Migration[8.1] + def change + remove_index :incomplete_submissions, %i[user_id form_id], unique: true + add_index :incomplete_submissions, %i[user_id form_id mode], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 92d60d94c..0dcc6d4a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_10_15_163042) do +ActiveRecord::Schema[8.1].define(version: 2026_11_05_100000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -40,7 +40,7 @@ t.string "locale", default: "en", null: false, comment: "The language used to complete the form in ISO 2 letter format. Normally either 'en' or 'cy'" t.string "mode" t.string "submission_reference" - t.index ["user_id", "form_id"], name: "index_incomplete_submissions_on_user_id_and_form_id", unique: true + t.index ["user_id", "form_id", "mode"], name: "index_incomplete_submissions_on_user_id_and_form_id_and_mode", unique: true end create_table "submission_deliveries", force: :cascade do |t| diff --git a/spec/models/incomplete_submission_spec.rb b/spec/models/incomplete_submission_spec.rb index 78c18f377..97c14d7fe 100644 --- a/spec/models/incomplete_submission_spec.rb +++ b/spec/models/incomplete_submission_spec.rb @@ -2,7 +2,7 @@ RSpec.describe IncompleteSubmission, type: :model do subject(:incomplete_submission) do - described_class.new(user_id: "one-login-sub", form_id: 1, form_version: 1, answers: {}) + described_class.new(user_id: "one-login-sub", form_id: 1, form_version: 1, answers: {}, mode: "form") end describe "validations" do @@ -28,23 +28,47 @@ end end - context "when an incomplete submission already exists for the same user and form" do - before { described_class.create!(user_id: incomplete_submission.user_id, form_id: incomplete_submission.form_id) } + context "without a mode" do + before { incomplete_submission.mode = nil } + + it "is invalid" do + expect(incomplete_submission).not_to be_valid + expect(incomplete_submission.errors[:mode]).to include("can't be blank") + end + end + + context "when an incomplete submission already exists for the same user, form and mode" do + before { described_class.create!(user_id: incomplete_submission.user_id, form_id: incomplete_submission.form_id, mode: "form") } it "is invalid" do expect(incomplete_submission).not_to be_valid expect(incomplete_submission.errors[:form_id]).to include("has already been taken") end end + + context "when an incomplete submission already exists for the same user and form in a different mode" do + before { described_class.create!(user_id: incomplete_submission.user_id, form_id: incomplete_submission.form_id, mode: "preview-draft") } + + it "is valid" do + expect(incomplete_submission).to be_valid + end + end end describe "database constraints" do - it "rejects a duplicate (user_id, form_id) that bypasses validation" do - described_class.create!(user_id: "one-login-sub", form_id: 1) - duplicate = described_class.new(user_id: "one-login-sub", form_id: 1) + it "rejects a duplicate (user_id, form_id, mode) that bypasses validation" do + described_class.create!(user_id: "one-login-sub", form_id: 1, mode: "form") + duplicate = described_class.new(user_id: "one-login-sub", form_id: 1, mode: "form") expect { duplicate.save!(validate: false) }.to raise_error(ActiveRecord::RecordNotUnique) end + + it "allows the same user and form in a different mode" do + described_class.create!(user_id: "one-login-sub", form_id: 1, mode: "form") + other_mode = described_class.new(user_id: "one-login-sub", form_id: 1, mode: "preview-draft") + + expect { other_mode.save!(validate: false) }.not_to raise_error + end end describe "#form" do diff --git a/spec/requests/forms/continue_saved_form_controller_spec.rb b/spec/requests/forms/continue_saved_form_controller_spec.rb index 543ba325f..f57c91c2f 100644 --- a/spec/requests/forms/continue_saved_form_controller_spec.rb +++ b/spec/requests/forms/continue_saved_form_controller_spec.rb @@ -70,7 +70,7 @@ context "and they have saved progress for the form" do before do - IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }) + IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }, mode:) get continue_saved_form_path(mode:, form_id: form.form_id, form_slug: form.form_slug) end @@ -137,7 +137,7 @@ def get_signed_in context "and they have saved progress for the form" do before do - IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }) + IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: { "1" => { "text" => "a saved answer" } }, mode:) get_signed_in end diff --git a/spec/requests/forms/save_progress_controller_spec.rb b/spec/requests/forms/save_progress_controller_spec.rb index c100f7ff0..c063e7a38 100644 --- a/spec/requests/forms/save_progress_controller_spec.rb +++ b/spec/requests/forms/save_progress_controller_spec.rb @@ -207,7 +207,7 @@ def get_after_sign_in let(:previously_saved_answers) { { "1" => { "text" => "previous answer" } } } before do - IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: previously_saved_answers) + IncompleteSubmission.create!(user_id: "user-sub-123", form_id: form.form_id, answers: previously_saved_answers, mode:) get_after_sign_in end @@ -334,6 +334,7 @@ def get_after_sign_in user_id: "user-sub-123", form_id: form.form_id, answers:, + mode:, updated_at: Time.zone.local(2026, 10, 14, 12, 0, 0), ) diff --git a/spec/services/form_submission_service_spec.rb b/spec/services/form_submission_service_spec.rb index 322c7ecdb..6813d5762 100644 --- a/spec/services/form_submission_service_spec.rb +++ b/spec/services/form_submission_service_spec.rb @@ -119,6 +119,43 @@ end end + context "when the user saved their progress before submitting" do + let(:stored_submission_reference) { "ABC23456" } + + let!(:saved_progress) do + IncompleteSubmission.create!(user_id: "one-login-sub", form_id: 1, answers:, submission_reference: "ABC23456", mode: "form") + end + let!(:saved_progress_in_other_mode) do + IncompleteSubmission.create!(user_id: "one-login-sub", form_id: 1, answers:, submission_reference: "DRAFTREF", mode: "preview-draft") + end + + it "deletes the saved progress so it cannot be continued again" do + service.submit + + expect(IncompleteSubmission.exists?(saved_progress.id)).to be false + end + + it "keeps saved progress for the form in other modes" do + service.submit + + expect(IncompleteSubmission.exists?(saved_progress_in_other_mode.id)).to be true + end + + context "and the submission fails" do + before do + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/api/v3/forms/1/delivery-configurations/current", {}, "Error", 500 + end + end + + it "keeps the saved progress" do + expect { service.submit }.to raise_error(ActiveResource::ServerError) + + expect(IncompleteSubmission.exists?(saved_progress.id)).to be true + end + end + end + it "includes the submission reference in the logging context" do service.submit expect(log_line["submission_reference"]).to eq(reference) diff --git a/spec/services/load_form_progress_service_spec.rb b/spec/services/load_form_progress_service_spec.rb index a3e191551..a68346327 100644 --- a/spec/services/load_form_progress_service_spec.rb +++ b/spec/services/load_form_progress_service_spec.rb @@ -1,9 +1,10 @@ require "rails_helper" RSpec.describe LoadFormProgressService do - subject(:service) { described_class.new(current_context:, user_id:) } + subject(:service) { described_class.new(current_context:, user_id:, mode:) } let(:user_id) { "one-login-sub" } + let(:mode) { Mode.new("form") } let(:form_document) { build(:form_document, form_id: 1, start_page: 1, steps: [build(:question_step, :with_text_settings, id: 1)]) } let(:form) { Form.new(form_document) } let(:current_context) { Flow::Context.new(form:, form_document:, store: {}) } @@ -13,7 +14,7 @@ let(:saved_answers) { { "1" => { "text" => "a saved answer" } } } let!(:incomplete_submission) do - IncompleteSubmission.create!(user_id:, form_id: form.id, answers: saved_answers, submission_reference: "SAVEDREF", locale: "cy") + IncompleteSubmission.create!(user_id:, form_id: form.id, answers: saved_answers, submission_reference: "SAVEDREF", locale: "cy", mode: "form") end it "returns the saved progress" do @@ -44,5 +45,21 @@ expect(service.load!).to be_nil end end + + context "when the user only has saved progress for the form in a different mode" do + before do + IncompleteSubmission.create!(user_id:, form_id: form.id, answers: { "1" => { "text" => "a draft answer" } }, mode: "preview-draft") + end + + it "returns nil" do + expect(service.load!).to be_nil + end + + it "does not load the answers into the session" do + service.load! + + expect(current_context.answers).to be_nil + end + end end end diff --git a/spec/services/save_form_progress_service_spec.rb b/spec/services/save_form_progress_service_spec.rb index 5172b5641..965d370ee 100644 --- a/spec/services/save_form_progress_service_spec.rb +++ b/spec/services/save_form_progress_service_spec.rb @@ -61,7 +61,7 @@ context "when a saved answer already exists for the user and form" do let!(:existing) do - IncompleteSubmission.create!(user_id:, form_id: form.id, form_version: 1, answers: { "old" => "answer" }) + IncompleteSubmission.create!(user_id:, form_id: form.id, form_version: 1, answers: { "old" => "answer" }, mode: "preview-draft") end it "updates the existing row in place rather than creating a duplicate" do @@ -70,5 +70,23 @@ expect(existing.reload).to have_attributes(form_version: form.version, answers:) end end + + context "when a saved answer exists for the user and form in a different mode" do + let!(:other_mode) do + IncompleteSubmission.create!(user_id:, form_id: form.id, form_version: 1, answers: { "old" => "answer" }, mode: "form") + end + + it "creates a separate row for this mode" do + expect { service.save! }.to change(IncompleteSubmission, :count).by(1) + + expect(IncompleteSubmission.find_by(user_id:, form_id: form.id, mode: "preview-draft").answers).to eq answers + end + + it "leaves the other mode's saved answers unchanged" do + service.save! + + expect(other_mode.reload.answers).to eq({ "old" => "answer" }) + end + end end end From d138a78983d0a57327f00b5462646bb0daf927d8 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 5 Nov 2026 17:02:06 +0000 Subject: [PATCH 23/24] Refactor continue button path logic in show.html.erb This broke from returning from the CheckYourAnswers screen when a user clicks the Change link. It always skipped back to the CheckYourAnswers screen instead of continuing the rest of the form! Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/views/forms/save_progress/show.html.erb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/forms/save_progress/show.html.erb b/app/views/forms/save_progress/show.html.erb index 7413c6589..81486e9e4 100644 --- a/app/views/forms/save_progress/show.html.erb +++ b/app/views/forms/save_progress/show.html.erb @@ -8,7 +8,12 @@ <%= t('.body_html', email: @one_login_email, deletion_date: @deletion_date, form_url: continue_saved_form_url(@current_context.form.id, @current_context.form.form_slug)) %> - <%= govuk_button_link_to t('.continue_button'), form_step_path(@current_context.form.id, @current_context.form.form_slug, @current_context.next_step_slug) %> + <% continue_path = if @current_context.next_step_slug == CheckYourAnswersStep::CHECK_YOUR_ANSWERS_STEP_SLUG && @current_context.form.copy_of_answers_enabled? + copy_of_answers_path(form_id: @current_context.form.id, form_slug: @current_context.form.form_slug) + else + form_step_path(@current_context.form.id, @current_context.form.form_slug, @current_context.next_step_slug) + end %> + <%= govuk_button_link_to t('.continue_button'), continue_path %>

<%= link_to t('.sign_out_link'), "#" %>

From 2596e3659c1a987d8c85e68cb5a28b94d639dbd0 Mon Sep 17 00:00:00 2001 From: Kelvin Gan Date: Thu, 5 Nov 2026 17:23:46 +0000 Subject: [PATCH 24/24] Rename test from store_submission_reference to set_submission_reference To be consistent with method name Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- spec/lib/store/submission_details_store_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/store/submission_details_store_spec.rb b/spec/lib/store/submission_details_store_spec.rb index 222704673..dfcf1d8c0 100644 --- a/spec/lib/store/submission_details_store_spec.rb +++ b/spec/lib/store/submission_details_store_spec.rb @@ -33,7 +33,7 @@ end end - describe "#store_submission_reference" do + describe "#set_submission_reference" do it "stores the given submission reference" do submission_details_store.set_submission_reference("SAVEDREF")