diff --git a/app/controllers/forms/base_controller.rb b/app/controllers/forms/base_controller.rb index cd2a5abe5..eb8bc16f9 100644 --- a/app/controllers/forms/base_controller.rb +++ b/app/controllers/forms/base_controller.rb @@ -4,8 +4,11 @@ class BaseController < ApplicationController around_action :set_locale def redirect_to_friendly_url_start - redirect_to form_step_path(params.require(:form_id), @form.form_slug, @form.start_page) 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) end rescue_from ActiveResource::ResourceNotFound, RepeatableStep::AnswerIndexError do 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..aead1300d --- /dev/null +++ b/app/controllers/forms/continue_saved_form_controller.rb @@ -0,0 +1,43 @@ +module Forms + class ContinueSavedFormController < BaseController + before_action :redirect_if_save_and_return_disabled + + def new + unless 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], + ) + return + end + + load_saved_progress_and_redirect + 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 + incomplete_submission = LoadFormProgressService.new(current_context:, user_id: auth_service.sub).load! + return render :not_found, status: :not_found unless incomplete_submission + + redirect_to form_step_path(current_context.form.id, current_context.form.form_slug, current_context.form.start_page) + 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/controllers/forms/resume_options_controller.rb b/app/controllers/forms/resume_options_controller.rb new file mode 100644 index 000000000..f233f29c3 --- /dev/null +++ b/app/controllers/forms/resume_options_controller.rb @@ -0,0 +1,41 @@ +module Forms + class ResumeOptionsController < BaseController + before_action :back_to_start + 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 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, + @current_context.form.form_slug, + @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/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/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/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/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/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/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/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/services/load_form_progress_service.rb b/app/services/load_form_progress_service.rb new file mode 100644 index 000000000..c725c99d0 --- /dev/null +++ b/app/services/load_form_progress_service.rb @@ -0,0 +1,17 @@ +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.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/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..68d6d78a8 --- /dev/null +++ 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/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..157057cef --- /dev/null +++ b/app/views/forms/continue_saved_form/not_found.html.erb @@ -0,0 +1,19 @@ +<% 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') %>

+ + <%= t('.body_html') %> + + <%= govuk_button_link_to t('.start_new_form_button'), form_step_path(@form.id, @form.form_slug, @form.start_page) %> + +

+ <%= link_to t('.sign_in_different_link'), "#", class: "govuk-link" %> +

+
+
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..d421a6370 --- /dev/null +++ b/app/views/forms/resume_options/show.html.erb @@ -0,0 +1,23 @@ +<% set_page_title(form_title(form_name: @current_context.form.name, page_name: t('.title'), mode: @mode)) %> + + +<%= 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/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 ab1c6a3a1..ca7ed35ad 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 @@ -399,6 +403,16 @@ 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: + 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:

+ sign_in_different_link: Sign in with a different GOV.UK One Login + 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.

" @@ -425,6 +439,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 @@ -440,8 +457,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: @@ -459,6 +477,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: @@ -477,6 +498,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 e7a276e19..c98411e5c 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 @@ -399,6 +403,16 @@ 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: + 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:

+ sign_in_different_link: Sign in with a different GOV.UK One Login + 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.

" @@ -425,6 +439,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 @@ -440,8 +457,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: @@ -459,6 +477,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: @@ -477,6 +498,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/config/routes.rb b/config/routes.rb index 3fdb69c06..c01b73dc4 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/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/features/continue_saved_form_spec.rb b/spec/features/continue_saved_form_spec.rb new file mode 100644 index 000000000..51a47141a --- /dev/null +++ b/spec/features/continue_saved_form_spec.rb @@ -0,0 +1,154 @@ +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 + + 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, + form_id: 1, + form_version: form.version, + answers: { "1" => { "text" => "Sir Lancelot" } }, + locale: "en", + mode: "form", + submission_reference: "SAVEDREF", + ) + 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?" + 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 diff --git a/spec/features/save_form_progress_spec.rb b/spec/features/save_form_progress_spec.rb index 480ab08bc..7c5ecabc9 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 @@ -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/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 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 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) 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/requests/forms/base_controller_spec.rb b/spec/requests/forms/base_controller_spec.rb index 17c38c7e0..d23ad9efd 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,18 @@ 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 + + it "Logs the form_visit event" do + expect(LogEventService).to have_received(:log_form_start) + end + end end context "when the form has no start page" do 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..543ba325f --- /dev/null +++ b/spec/requests/forms/continue_saved_form_controller_spec.rb @@ -0,0 +1,168 @@ +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 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 + { + 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 + + 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/forms/resume_options_controller_spec.rb b/spec/requests/forms/resume_options_controller_spec.rb new file mode 100644 index 000000000..77e24f199 --- /dev/null +++ b/spec/requests/forms/resume_options_controller_spec.rb @@ -0,0 +1,98 @@ +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" } + + 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 + 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 + + 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 + + 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 + + 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 + let(:resume_options) { "continue_saved" } + + it "redirects to resume their saved progress" do + expect(response).to redirect_to(continue_saved_form_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 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 { 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_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 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 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..96720fa62 --- /dev/null +++ b/spec/views/forms/continue_saved_form/not_found.html.erb_spec.rb @@ -0,0 +1,38 @@ +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 + + it "has a link to sign in with a different GOV.UK One Login" do + expect(rendered).to have_link(I18n.t("forms.continue_saved_form.not_found.sign_in_different_link")) + end +end 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..cbd6b9321 --- /dev/null +++ b/spec/views/forms/resume_options/show.html.erb_spec.rb @@ -0,0 +1,43 @@ +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(:resume_options_continue_path).and_return("/resume_options_continue") + 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 + + 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 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