From cc17f5186f2d8a6f949037017c530b9cc79a4a7d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 16 Mar 2023 17:09:51 -0400 Subject: [PATCH 1/4] Configure Capybara wait time to zero changelog: Internal, Automated Testing, Configure automated tests to more aggressively identify unresponsive user interactions --- ...-location-post-office-search-step.spec.tsx | 1 + ...erson-location-post-office-search-step.tsx | 9 +++++-- .../components/location-collection-item.tsx | 14 +++++------ .../document-capture/context/analytics.tsx | 13 +++++----- .../packages/form-steps/form-steps.spec.tsx | 12 ++++++--- .../packages/form-steps/form-steps.tsx | 19 ++++++++++---- spec/features/idv/in_person_spec.rb | 25 +++++++++++-------- spec/features/users/sign_up_spec.rb | 16 ++++++++++-- spec/support/capybara.rb | 2 +- spec/support/features/doc_auth_helper.rb | 11 ++++---- spec/support/features/idv_helper.rb | 4 --- spec/support/features/in_person_helper.rb | 11 +++----- spec/support/features/interaction_helper.rb | 13 +++++----- 13 files changed, 89 insertions(+), 61 deletions(-) diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx index 4df8ac8d242..7fbe21d3e94 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx @@ -53,6 +53,7 @@ const MULTI_LOCATION_RESPONSE = [ const DEFAULT_PROPS = { toPreviousStep() {}, + toNextStep() {}, onChange() {}, value: {}, registerField() {}, diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx index bbb685efc59..07540a94fbf 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx @@ -13,7 +13,12 @@ import AddressSearch, { import InPersonLocations, { FormattedLocation } from './in-person-locations'; import { InPersonContext } from '../context'; -function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, registerField }) { +function InPersonLocationPostOfficeSearchStep({ + onChange, + toPreviousStep, + toNextStep, + registerField, +}) { const { inPersonCtaVariantActive } = useContext(InPersonContext); const { t } = useI18n(); const [inProgress, setInProgress] = useState(false); @@ -73,7 +78,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist setAutoSubmit(true); setImmediate(() => { // continue with navigation - e.target.click(); + toNextStep(); // allow process to be re-triggered in case submission did not work as expected setAutoSubmit(false); }); diff --git a/app/javascript/packages/document-capture/components/location-collection-item.tsx b/app/javascript/packages/document-capture/components/location-collection-item.tsx index 266cfd2f0cf..3cfc1ac2fd6 100644 --- a/app/javascript/packages/document-capture/components/location-collection-item.tsx +++ b/app/javascript/packages/document-capture/components/location-collection-item.tsx @@ -1,10 +1,10 @@ -import { Button } from '@18f/identity-components'; +import { SpinnerButton } from '@18f/identity-spinner-button'; import { useI18n } from '@18f/identity-react-i18n'; interface LocationCollectionItemProps { distance?: string; formattedCityStateZip: string; - handleSelect: (event: React.FormEvent, selection: number) => void; + handleSelect: (event: React.MouseEvent, selection: number) => void; name?: string; saturdayHours: string; selectId: number; @@ -60,18 +60,16 @@ function LocationCollectionItem({ {`${t('in_person_proofing.body.location.retail_hours_sun')} ${sundayHours}`} )} - +
- +
diff --git a/app/javascript/packages/document-capture/context/analytics.tsx b/app/javascript/packages/document-capture/context/analytics.tsx index 3778cb4c5ef..3ed7b18d277 100644 --- a/app/javascript/packages/document-capture/context/analytics.tsx +++ b/app/javascript/packages/document-capture/context/analytics.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useRef } from 'react'; import type { ReactNode } from 'react'; import type { trackEvent } from '@18f/identity-analytics'; import InPersonContext from './in-person'; @@ -57,15 +57,16 @@ const AnalyticsContext = createContext({ AnalyticsContext.displayName = 'AnalyticsContext'; export function AnalyticsContextProvider({ children, trackEvent }: AnalyticsContextProviderProps) { - const [submitEventMetadata, setSubmitEventMetadataState] = useState(DEFAULT_EVENT_METADATA); - const setSubmitEventMetadata: SetSubmitEventMetadata = (metadata) => - setSubmitEventMetadataState((prevState) => ({ ...prevState, ...metadata })); + const submitEventMetadata = useRef(DEFAULT_EVENT_METADATA); + const setSubmitEventMetadata: SetSubmitEventMetadata = (metadata) => { + submitEventMetadata.current = { ...submitEventMetadata.current, ...metadata }; + }; const trackSubmitEvent: TrackSubmitEvent = (stepName) => { if (LOGGED_STEPS.includes(stepName)) { - trackEvent(`IdV: ${stepName} submitted`, submitEventMetadata); + trackEvent(`IdV: ${stepName} submitted`, submitEventMetadata.current); } - setSubmitEventMetadataState(DEFAULT_EVENT_METADATA); + submitEventMetadata.current = DEFAULT_EVENT_METADATA; }; const { inPersonCtaVariantActive } = useContext(InPersonContext); diff --git a/app/javascript/packages/form-steps/form-steps.spec.tsx b/app/javascript/packages/form-steps/form-steps.spec.tsx index dd96c348e90..ab2d8c3d61b 100644 --- a/app/javascript/packages/form-steps/form-steps.spec.tsx +++ b/app/javascript/packages/form-steps/form-steps.spec.tsx @@ -40,11 +40,14 @@ describe('FormSteps', () => { { name: 'first', title: 'First Title', - form: ({ errors }) => ( + form: ({ errors, toNextStep }) => ( <> First Title First + {JSON.stringify(useContext(FormStepsContext))} Errors: {errors.map(({ error }) => error.message).join(',')} @@ -679,12 +682,13 @@ describe('FormSteps', () => { expect(window.history.pushState).not.to.have.been.called(); }); - it('provides the step implementation the option to navigate to the previous step', async () => { + it('provides the step implementation the option to navigate to the previous or next step', async () => { const { getByText } = render(); - await userEvent.click(getByText('forms.buttons.continue')); - await userEvent.click(getByText('Back')); + await userEvent.click(getByText('Next')); + expect(getByText('Second Title')).to.be.ok(); + await userEvent.click(getByText('Back')); expect(getByText('First Title')).to.be.ok(); }); diff --git a/app/javascript/packages/form-steps/form-steps.tsx b/app/javascript/packages/form-steps/form-steps.tsx index c1ac8ac689f..6a0ea8b9011 100644 --- a/app/javascript/packages/form-steps/form-steps.tsx +++ b/app/javascript/packages/form-steps/form-steps.tsx @@ -72,6 +72,11 @@ export interface FormStepComponentProps { * Callback to navigate to the previous step. */ toPreviousStep: () => void; + + /** + * Callback to navigate to the next step. + */ + toNextStep: () => void; } export interface FormStep { @@ -359,9 +364,7 @@ function FormSteps({ * Increments state to the next step, or calls onComplete callback * if the current step is the last step. */ - const toNextStep: FormEventHandler = async (event) => { - event.preventDefault(); - + async function toNextStep() { // Don't proceed if field errors have yet to be resolved. if (hasUnresolvedFieldErrors) { setActiveErrors(Array.from(activeErrors)); @@ -395,7 +398,7 @@ function FormSteps({ } } - onStepSubmit(step?.name); + onStepSubmit(step!.name); const nextStepIndex = stepIndex + 1; const isComplete = @@ -408,6 +411,11 @@ function FormSteps({ } // unset stepCanComplete so the next step that needs to can set it setStepCanComplete(undefined); + } + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + toNextStep(); }; const toPreviousStep = () => { @@ -424,7 +432,7 @@ function FormSteps({ const isLastStep = stepIndex + 1 === steps.length; return ( -
+ {promptOnNavigate && Object.keys(values).length > 0 && } {stepErrors.map((error) => ( @@ -439,6 +447,7 @@ function FormSteps({ value={values} errors={activeErrors} unknownFieldErrors={unknownFieldErrors} + toNextStep={toNextStep} onChange={ifStillMounted((nextValues, { patch } = { patch: true }) => { setActiveErrors((prevActiveErrors) => prevActiveErrors.filter(({ field }) => !field || !(field in nextValues)), diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index decca7926fd..da552aecc64 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -26,8 +26,9 @@ search_for_post_office # location page - bethesda_location = page.find_all('.location-collection-item')[1] - bethesda_location.click_button(t('in_person_proofing.body.location.location_button')) + within page.first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end # prepare page complete_prepare_step(user) @@ -125,8 +126,9 @@ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.find_a_post_office')) expect(page).to have_content(t('in_person_proofing.headings.po_search.location')) search_for_post_office - bethesda_location = page.find_all('.location-collection-item')[1] - bethesda_location.click_button(t('in_person_proofing.body.location.location_button')) + within page.first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end # prepare page expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.find_a_post_office')) @@ -260,8 +262,9 @@ # location page expect(page).to have_content(t('in_person_proofing.headings.po_search.location')) search_for_post_office - bethesda_location = page.find_all('.location-collection-item')[1] - bethesda_location.click_button(t('in_person_proofing.body.location.location_button')) + within page.first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end # prepare page expect(page).to have_content(t('in_person_proofing.headings.prepare')) @@ -340,8 +343,9 @@ click_link t('in_person_proofing.body.cta.button') search_for_post_office - bethesda_location = page.find_all('.location-collection-item')[1] - bethesda_location.click_button(t('in_person_proofing.body.location.location_button')) + within page.first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end click_idv_continue @@ -500,8 +504,9 @@ search_for_post_office # location page - location = page.find_all('.location-collection-item')[1] - location.click_button(t('in_person_proofing.body.location.location_button')) + within page.first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end # prepare page complete_prepare_step(user) diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 504343b419c..c7345534c14 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -147,6 +147,19 @@ page.driver.browser.execute_cdp('Browser.resetPermissions') end + def clipboard_text + # `evaluate_async_script` is expected to be asynchronous, but internally it sets the browser + # script timeout based on Capybara's configured default wait time. Allow for delay in this + # asynchronous result while avoiding modifying the default otherwise. + # + # See: https://github.com/teamcapybara/capybara/blob/3.38.0/lib/capybara/selenium/driver.rb#L146 + original_default_max_wait_time = Capybara.default_max_wait_time + Capybara.default_max_wait_time = 5 + result = page.evaluate_async_script('navigator.clipboard.readText().then(arguments[0])') + Capybara.default_max_wait_time = original_default_max_wait_time + result + end + context 'user enters their email as their password', email: true do it 'treats it as a weak password' do email = 'test@test.com' @@ -169,11 +182,10 @@ did_validate_name = -> { name.evaluate_script('this.didValidate') } click_on t('components.clipboard_button.label') - copied_text = page.evaluate_async_script('navigator.clipboard.readText().then(arguments[0])') expect(did_validate_name.call).to_not eq true otp_input = page.find('.one-time-code-input__input') - otp_input.set(generate_totp_code(copied_text)) + otp_input.set(generate_totp_code(clipboard_text)) click_button 'Submit' expect(did_validate_name.call).to eq true diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index cd94be301f6..da2b310b368 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -41,7 +41,7 @@ Capybara.server = :puma, { Silent: true } -Capybara.default_max_wait_time = (ENV['CAPYBARA_WAIT_TIME_SECONDS'] || '0.5').to_f +Capybara.default_max_wait_time = 0 Capybara.asset_host = ENV['RAILS_ASSET_HOST'] || 'http://localhost:3000' Capybara.automatic_label_click = true # USWDS styles native checkbox/radio as offscreen Capybara.enable_aria_label = true diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 9b2f061cbfb..f88f6045e2f 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -1,6 +1,8 @@ require_relative 'document_capture_step_helper' +require_relative 'interaction_helper' module DocAuthHelper + include InteractionHelper include DocumentCaptureStepHelper GOOD_SSN = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] @@ -81,7 +83,7 @@ def complete_doc_auth_steps_before_welcome_step(expect_accessible: false) end def complete_welcome_step - click_on t('doc_auth.buttons.continue') + click_spinner_button_and_wait t('doc_auth.buttons.continue') end def complete_doc_auth_steps_before_agreement_step(expect_accessible: false) @@ -91,11 +93,7 @@ def complete_doc_auth_steps_before_agreement_step(expect_accessible: false) end def complete_agreement_step - find( - 'label', - text: t('doc_auth.instructions.consent', app_name: APP_NAME), - wait: 5, - ).click + find('label', text: t('doc_auth.instructions.consent', app_name: APP_NAME)).click click_on t('doc_auth.buttons.continue') end @@ -126,6 +124,7 @@ def complete_document_capture_step_with_yml(proofing_yml) attach_file I18n.t('doc_auth.headings.document_capture_front'), File.expand_path(proofing_yml) attach_file I18n.t('doc_auth.headings.document_capture_back'), File.expand_path(proofing_yml) click_on I18n.t('forms.buttons.submit.default') + expect(page).to have_current_path(idv_ssn_url, wait: 10) end def complete_doc_auth_steps_before_email_sent_step diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb index 2deaaf63487..d50571f37af 100644 --- a/spec/support/features/idv_helper.rb +++ b/spec/support/features/idv_helper.rb @@ -47,10 +47,6 @@ def click_idv_send_security_code click_spinner_button_and_wait t('forms.buttons.send_one_time_code') end - def click_idv_select - click_select_button_and_wait t('in_person_proofing.body.location.location_button') - end - def click_try_again page.find( 'a', diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 3bfa4a51281..76e980ab7a8 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -68,16 +68,13 @@ def search_for_post_office def complete_location_step(_user = nil) search_for_post_office - first('.location-collection-item'). - click_button(t('in_person_proofing.body.location.location_button')) + within first('.location-collection-item') do + click_spinner_button_and_wait t('in_person_proofing.body.location.location_button') + end end def complete_prepare_step(_user = nil) - # Wait for page to load before clicking continue - expect(page).to have_content( - t('in_person_proofing.headings.prepare'), - ) - click_link t('forms.buttons.continue') + click_spinner_button_and_wait t('forms.buttons.continue') end def complete_state_id_step(_user = nil, same_address_as_id: true, include_address: false) diff --git a/spec/support/features/interaction_helper.rb b/spec/support/features/interaction_helper.rb index a2d7da1f9fc..6984ba3fec5 100644 --- a/spec/support/features/interaction_helper.rb +++ b/spec/support/features/interaction_helper.rb @@ -1,11 +1,12 @@ module InteractionHelper def click_spinner_button_and_wait(...) click_on(...) - expect(page).to have_no_css('lg-spinner-button.spinner-button--spinner-active', wait: 10) - end - - def click_select_button_and_wait(...) - click_button(...) - expect(page).to have_no_css('button.usa-button', wait: 10) + begin + expect(page).to have_no_css('lg-spinner-button.spinner-button--spinner-active', wait: 10) + rescue Selenium::WebDriver::Error::StaleElementReferenceError + # A stale element error can occur when attempting to wait for the spinner to disappear if the + # context in which the button was clicked (e.g. a `within` block) itself disappears. This is + # fine, since if the ancestor disappears, it can be assumed that the button is gone too. + end end end From b9c00a2682eb1c0e98d5bbd33b5d73c2c8444e24 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 17 Mar 2023 10:00:17 -0400 Subject: [PATCH 2/4] Revert toNextStep for IPP step This should probably be available as an option for a step, but it doesn't work in IPP because IPP relies on frame ticks resulting from the click on the button, since otherwise the state change in setting submit metadata results in the subsequent submit event using a stale version of that metadata state. In earlier iterations here, we changed this to a mutative ref, but refs are not good for use in the public interface of the hook, since updating the ref would not trigger an update to consuming components. --- ...-location-post-office-search-step.spec.tsx | 1 - ...erson-location-post-office-search-step.tsx | 10 +++------- .../document-capture/context/analytics.tsx | 13 ++++++------- .../packages/form-steps/form-steps.spec.tsx | 12 ++++-------- .../packages/form-steps/form-steps.tsx | 19 +++++-------------- 5 files changed, 18 insertions(+), 37 deletions(-) diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx index 7fbe21d3e94..4df8ac8d242 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx @@ -53,7 +53,6 @@ const MULTI_LOCATION_RESPONSE = [ const DEFAULT_PROPS = { toPreviousStep() {}, - toNextStep() {}, onChange() {}, value: {}, registerField() {}, diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx index 07540a94fbf..95e73ff07e8 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.tsx @@ -13,12 +13,7 @@ import AddressSearch, { import InPersonLocations, { FormattedLocation } from './in-person-locations'; import { InPersonContext } from '../context'; -function InPersonLocationPostOfficeSearchStep({ - onChange, - toPreviousStep, - toNextStep, - registerField, -}) { +function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, registerField }) { const { inPersonCtaVariantActive } = useContext(InPersonContext); const { t } = useI18n(); const [inProgress, setInProgress] = useState(false); @@ -78,7 +73,8 @@ function InPersonLocationPostOfficeSearchStep({ setAutoSubmit(true); setImmediate(() => { // continue with navigation - toNextStep(); + e.target.disabled = false; + e.target.click(); // allow process to be re-triggered in case submission did not work as expected setAutoSubmit(false); }); diff --git a/app/javascript/packages/document-capture/context/analytics.tsx b/app/javascript/packages/document-capture/context/analytics.tsx index 3ed7b18d277..3778cb4c5ef 100644 --- a/app/javascript/packages/document-capture/context/analytics.tsx +++ b/app/javascript/packages/document-capture/context/analytics.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useRef } from 'react'; +import { createContext, useContext, useState } from 'react'; import type { ReactNode } from 'react'; import type { trackEvent } from '@18f/identity-analytics'; import InPersonContext from './in-person'; @@ -57,16 +57,15 @@ const AnalyticsContext = createContext({ AnalyticsContext.displayName = 'AnalyticsContext'; export function AnalyticsContextProvider({ children, trackEvent }: AnalyticsContextProviderProps) { - const submitEventMetadata = useRef(DEFAULT_EVENT_METADATA); - const setSubmitEventMetadata: SetSubmitEventMetadata = (metadata) => { - submitEventMetadata.current = { ...submitEventMetadata.current, ...metadata }; - }; + const [submitEventMetadata, setSubmitEventMetadataState] = useState(DEFAULT_EVENT_METADATA); + const setSubmitEventMetadata: SetSubmitEventMetadata = (metadata) => + setSubmitEventMetadataState((prevState) => ({ ...prevState, ...metadata })); const trackSubmitEvent: TrackSubmitEvent = (stepName) => { if (LOGGED_STEPS.includes(stepName)) { - trackEvent(`IdV: ${stepName} submitted`, submitEventMetadata.current); + trackEvent(`IdV: ${stepName} submitted`, submitEventMetadata); } - submitEventMetadata.current = DEFAULT_EVENT_METADATA; + setSubmitEventMetadataState(DEFAULT_EVENT_METADATA); }; const { inPersonCtaVariantActive } = useContext(InPersonContext); diff --git a/app/javascript/packages/form-steps/form-steps.spec.tsx b/app/javascript/packages/form-steps/form-steps.spec.tsx index ab2d8c3d61b..dd96c348e90 100644 --- a/app/javascript/packages/form-steps/form-steps.spec.tsx +++ b/app/javascript/packages/form-steps/form-steps.spec.tsx @@ -40,14 +40,11 @@ describe('FormSteps', () => { { name: 'first', title: 'First Title', - form: ({ errors, toNextStep }) => ( + form: ({ errors }) => ( <> First Title First - {JSON.stringify(useContext(FormStepsContext))} Errors: {errors.map(({ error }) => error.message).join(',')} @@ -682,13 +679,12 @@ describe('FormSteps', () => { expect(window.history.pushState).not.to.have.been.called(); }); - it('provides the step implementation the option to navigate to the previous or next step', async () => { + it('provides the step implementation the option to navigate to the previous step', async () => { const { getByText } = render(); - await userEvent.click(getByText('Next')); - expect(getByText('Second Title')).to.be.ok(); - + await userEvent.click(getByText('forms.buttons.continue')); await userEvent.click(getByText('Back')); + expect(getByText('First Title')).to.be.ok(); }); diff --git a/app/javascript/packages/form-steps/form-steps.tsx b/app/javascript/packages/form-steps/form-steps.tsx index 6a0ea8b9011..c1ac8ac689f 100644 --- a/app/javascript/packages/form-steps/form-steps.tsx +++ b/app/javascript/packages/form-steps/form-steps.tsx @@ -72,11 +72,6 @@ export interface FormStepComponentProps { * Callback to navigate to the previous step. */ toPreviousStep: () => void; - - /** - * Callback to navigate to the next step. - */ - toNextStep: () => void; } export interface FormStep { @@ -364,7 +359,9 @@ function FormSteps({ * Increments state to the next step, or calls onComplete callback * if the current step is the last step. */ - async function toNextStep() { + const toNextStep: FormEventHandler = async (event) => { + event.preventDefault(); + // Don't proceed if field errors have yet to be resolved. if (hasUnresolvedFieldErrors) { setActiveErrors(Array.from(activeErrors)); @@ -398,7 +395,7 @@ function FormSteps({ } } - onStepSubmit(step!.name); + onStepSubmit(step?.name); const nextStepIndex = stepIndex + 1; const isComplete = @@ -411,11 +408,6 @@ function FormSteps({ } // unset stepCanComplete so the next step that needs to can set it setStepCanComplete(undefined); - } - - const handleSubmit: FormEventHandler = (event) => { - event.preventDefault(); - toNextStep(); }; const toPreviousStep = () => { @@ -432,7 +424,7 @@ function FormSteps({ const isLastStep = stepIndex + 1 === steps.length; return ( - + {promptOnNavigate && Object.keys(values).length > 0 && } {stepErrors.map((error) => ( @@ -447,7 +439,6 @@ function FormSteps({ value={values} errors={activeErrors} unknownFieldErrors={unknownFieldErrors} - toNextStep={toNextStep} onChange={ifStillMounted((nextValues, { patch } = { patch: true }) => { setActiveErrors((prevActiveErrors) => prevActiveErrors.filter(({ field }) => !field || !(field in nextValues)), From 02b17f1a2f41afe78dad87365c89d12f98a5719f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 17 Mar 2023 13:49:39 -0400 Subject: [PATCH 3/4] Revise to use using_wait_time helper It's designed for this purpose, is more readable and succinct --- spec/features/users/sign_up_spec.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index c7345534c14..853bd294c36 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -153,11 +153,9 @@ def clipboard_text # asynchronous result while avoiding modifying the default otherwise. # # See: https://github.com/teamcapybara/capybara/blob/3.38.0/lib/capybara/selenium/driver.rb#L146 - original_default_max_wait_time = Capybara.default_max_wait_time - Capybara.default_max_wait_time = 5 - result = page.evaluate_async_script('navigator.clipboard.readText().then(arguments[0])') - Capybara.default_max_wait_time = original_default_max_wait_time - result + Capybara.using_wait_time(5) do + page.evaluate_async_script('navigator.clipboard.readText().then(arguments[0])') + end end context 'user enters their email as their password', email: true do From ebd4c5e814b38d8c291a5d6c09b741cbde12eabb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 20 Mar 2023 08:53:34 -0400 Subject: [PATCH 4/4] Reintroduce CAPYBARA_WAIT_TIME_SECONDS env variable See: https://github.com/18F/identity-idp/pull/8013#discussion_r1140463263 --- spec/support/capybara.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index da2b310b368..783c5c818b5 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -41,7 +41,7 @@ Capybara.server = :puma, { Silent: true } -Capybara.default_max_wait_time = 0 +Capybara.default_max_wait_time = (ENV['CAPYBARA_WAIT_TIME_SECONDS'] || 0).to_f Capybara.asset_host = ENV['RAILS_ASSET_HOST'] || 'http://localhost:3000' Capybara.automatic_label_click = true # USWDS styles native checkbox/radio as offscreen Capybara.enable_aria_label = true