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..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 @@ -73,6 +73,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist setAutoSubmit(true); setImmediate(() => { // continue with navigation + 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/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/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..853bd294c36 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -147,6 +147,17 @@ 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 + 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 it 'treats it as a weak password' do email = 'test@test.com' @@ -169,11 +180,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..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 = (ENV['CAPYBARA_WAIT_TIME_SECONDS'] || '0.5').to_f +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 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