diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb index 3e34430f7e2..849e113b869 100644 --- a/app/controllers/frontend_log_controller.rb +++ b/app/controllers/frontend_log_controller.rb @@ -47,6 +47,11 @@ class FrontendLogController < ApplicationController # rubocop:enable Layout/LineLength ALLOWED_EVENTS = %i[ + idv_sdk_selfie_image_added + idv_sdk_selfie_image_capture_closed_without_photo + idv_sdk_selfie_image_capture_failed + idv_sdk_selfie_image_capture_opened + idv_selfie_image_file_uploaded phone_input_country_changed ].freeze diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 6bc249fb945..a26b4d6baea 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -409,7 +409,11 @@ function AcuantCapture( size: nextValue.size, failedImageResubmission: hasFailed, }); - trackEvent(`IdV: ${name} image added`, analyticsPayload); + + trackEvent( + name === 'selfie' ? 'idv_selfie_image_file_uploaded' : `IdV: ${name} image added`, + analyticsPayload, + ); } onChangeAndResetError(nextValue, analyticsPayload); @@ -498,13 +502,32 @@ function AcuantCapture( } } + function onSelfieCaptureOpen() { + trackEvent('idv_sdk_selfie_image_capture_opened'); + + setIsCapturingEnvironment(true); + } + + function onSelfieCaptureClosed() { + trackEvent('idv_sdk_selfie_image_capture_closed_without_photo'); + + setIsCapturingEnvironment(false); + } + function onSelfieCaptureSuccess({ image }: { image: string }) { + trackEvent('idv_sdk_selfie_image_added', { attempt }); + onChangeAndResetError(image); onResetFailedCaptureAttempts(); setIsCapturingEnvironment(false); } - function onSelfieCaptureFailure() { + function onSelfieCaptureFailure(error) { + trackEvent('idv_sdk_selfie_image_capture_failed', { + sdk_error_code: error.code, + sdk_error_message: error.message, + }); + // Internally, Acuant sets a cookie to bail on guided capture if initialization had // previously failed for any reason, including declined permission. Since the cookie // never expires, and since we want to re-prompt even if the user had previously @@ -653,8 +676,8 @@ function AcuantCapture( setIsCapturingEnvironment(true)} - onImageCaptureClose={() => setIsCapturingEnvironment(false)} + onImageCaptureOpen={onSelfieCaptureOpen} + onImageCaptureClose={onSelfieCaptureClosed} > void; /** * Capture open callback, tells the rest of the page * when the fullscreen selfie capture page is open @@ -100,7 +100,7 @@ function AcuantSelfieCamera({ onError: (error) => { // Error occurred. Camera permission not granted will // manifest here with 1 as error code. Unexpected errors will have 2 as error code. - onImageCaptureFailure({ error }); + onImageCaptureFailure(error); }, onPhotoTaken: () => { // The photo has been taken and it's showing a preview with a button to accept or retake the image. diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 3559dd34b24..45b85cd8cf6 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -2729,6 +2729,74 @@ def idv_request_letter_visited( ) end + # @param [Integer] attempt number of attempts + # User captured and approved of their selfie + def idv_sdk_selfie_image_added(attempt:, **extra) + track_event(:idv_sdk_selfie_image_added, attempt: attempt, **extra) + end + + # User closed the SDK for taking a selfie without submitting a photo + def idv_sdk_selfie_image_capture_closed_without_photo(**extra) + track_event(:idv_sdk_selfie_image_capture_closed_without_photo, **extra) + end + + # @param [Integer] sdk_error_code SDK code for the error encountered + # @param [String] sdk_error_message SDK message for the error encountered + # User encountered an error with the SDK selfie process + # Error code 1: camera permission not granted + # Error code 2: unexpected errors + def idv_sdk_selfie_image_capture_failed(sdk_error_code:, sdk_error_message:, **extra) + track_event( + :idv_sdk_selfie_image_capture_failed, + sdk_error_code: sdk_error_code, + sdk_error_message: sdk_error_message, + **extra, + ) + end + + # User opened the SDK to take a selfie + def idv_sdk_selfie_image_capture_opened(**extra) + track_event(:idv_sdk_selfie_image_capture_opened, **extra) + end + + # @param [Integer] attempt number of attempts + # @param [Integer] failedImageResubmission + # @param [String] fingerprint fingerprint of the image added + # @param [String] flow_path whether the user is in the hybrid or standard flow + # @param [Integer] height height of image added in pixels + # @param [String] mimeType MIME type of image added + # @param [Integer] size size of image added in bytes + # @param [String] source + # @param [Integer] width width of image added in pixels + # User uploaded a selfie using the file picker + # rubocop:disable Naming/VariableName,Naming/MethodParameterName + def idv_selfie_image_file_uploaded( + attempt:, + failedImageResubmission:, + fingerprint:, + flow_path:, + height:, + mimeType:, + size:, + source:, + width:, + **_extra + ) + track_event( + :idv_selfie_image_file_uploaded, + attempt: attempt, + failedImageResubmission: failedImageResubmission, + fingerprint: fingerprint, + flow_path: flow_path, + height: height, + mimeType: mimeType, + size: size, + source: source, + width: width, + ) + end + # rubocop:enable Naming/VariableName,Naming/MethodParameterName + # Tracks when the user visits one of the the session error pages. # @param [String] type # @param [Integer,nil] attempts_remaining diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 81d9e6b4e81..29d5079034f 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -465,6 +465,117 @@ 'IdV: user clicked sp link on ready to verify page' => {}, } end + + let(:happy_selfie_path_events) do + { + 'IdV: intro visited' => {}, + 'IdV: doc auth welcome visited' => { + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: nil, lexisnexis_instant_verify_workflow_ab_test_bucket: :default + }, + 'IdV: doc auth welcome submitted' => { + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: nil, lexisnexis_instant_verify_workflow_ab_test_bucket: :default + }, + 'IdV: doc auth agreement visited' => { + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default + }, + 'IdV: consent checkbox toggled' => { + checked: true, + }, + 'IdV: doc auth agreement submitted' => { + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default + }, + 'IdV: doc auth hybrid handoff visited' => { + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + }, + 'IdV: doc auth hybrid handoff submitted' => { + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + }, + 'IdV: doc auth document_capture visited' => { + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, skip_hybrid_handoff: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false + }, + 'Frontend: IdV: front image added' => { + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + }, + 'Frontend: IdV: back image added' => { + width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil + }, + 'IdV: doc auth image upload form submitted' => { + success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) + }, + 'IdV: doc auth image upload vendor pii validation' => { + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), classification_info: {} + }, + 'IdV: doc auth document_capture submitted' => { + success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, skip_hybrid_handoff: nil, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false + }, + :idv_selfie_image_file_uploaded => { + attempt: 1, failedImageResubmission: nil, fingerprint: 'aIzxkX_iMtoxFOURZr55qkshs53emQKUOr7VfTf6G1Q', flow_path: 'standard', height: 38, mimeType: 'image/png', size: 3694, source: 'upload', width: 284 + }, + 'IdV: doc auth ssn visited' => { + flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + }, + 'IdV: doc auth ssn submitted' => { + success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + }, + 'IdV: doc auth verify visited' => { + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + }, + 'IdV: doc auth verify submitted' => { + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + }, + 'IdV: doc auth verify proofing results' => { + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, irs_reproofing: false, skip_hybrid_handoff: nil, + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + }, + 'IdV: phone of record visited' => { + acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + }, + 'IdV: phone confirmation form' => { + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + }, + 'IdV: phone confirmation vendor' => { + success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + 'IdV: phone confirmation otp sent' => { + success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + 'IdV: phone confirmation otp visited' => { + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + }, + 'IdV: phone confirmation otp submitted' => { + success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + :idv_enter_password_visited => { + address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + :idv_enter_password_submitted => { + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + 'IdV: final resolution' => { + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + 'IdV: personal key visited' => { + address_verification_method: 'phone', in_person_verification_pending: false, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + 'IdV: personal key acknowledgment toggled' => { + checked: true, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + }, + 'IdV: personal key submitted' => { + address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + }, + } + end # rubocop:enable Layout/LineLength # rubocop:enable Layout/MultilineHashKeyLineBreaks @@ -730,4 +841,61 @@ def wait_for_event(event, wait) end end end + + context 'Happy selfie path' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true) + + mobile_device = Browser.new(mobile_user_agent) + allow(BrowserCache).to receive(:parse).and_return(mobile_device) + + perform_in_browser(:mobile) do + sign_in_and_2fa_user(user) + visit_idp_from_sp_with_ial2(:oidc) + complete_doc_auth_steps_before_document_capture_step + + attach_images + attach_selfie + submit_images + + click_idv_continue + visit idv_ssn_url + complete_ssn_step + complete_verify_step + fill_out_phone_form_ok('202-555-1212') + verify_phone_otp + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key + end + end + + it 'records all of the events' do + happy_selfie_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + + context 'proofing_device_profiling disabled' do + let(:proofing_device_profiling) { :disabled } + let(:threatmetrix) { false } + let(:threatmetrix_response) do + { client: 'tmx_disabled', + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: nil, + review_status: 'pass', + response_body: { error: 'TMx response body was empty' } } + end + + it 'records all of the events' do + aggregate_failures 'analytics events' do + happy_selfie_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + end + end + end end diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx index a542fd4c00f..a9b18500dae 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx @@ -9,7 +9,7 @@ import { AnalyticsContext, FailedCaptureAttemptsContextProvider, } from '@18f/identity-document-capture'; -import { createEvent, waitFor } from '@testing-library/dom'; +import { createEvent, waitFor, screen } from '@testing-library/dom'; import DeviceContext from '@18f/identity-document-capture/context/device'; import { I18n } from '@18f/identity-i18n'; @@ -1114,21 +1114,116 @@ describe('document-capture/components/acuant-capture', () => { }); context('mobile selfie', () => { - it('renders the selfie capture loading div in acuant-capture', async () => { + const trackEvent = sinon.stub(); + + beforeEach(async () => { + // Set up the components so that everything is as it would actually be -except- the AcuantSDK + // The AcuantSDK isn't possible to run in test, so the initialize({...}) call below mocks it. + render( + + + + + + + , + ); + + // Simulate the user clicking on the box that usually opens full screen selfie capture. + // This isn't strictly necessary for the logging tests, but doing this makes the calls to + // trackEvent appear in the actual order we'd expect when using the Acuant SDK. + await userEvent.click(screen.getByLabelText('Image')); + }); + + it('renders the selfie capture loading div in acuant-capture', () => { // What we want to test is that the selfie version of the FileInput appears // when the name="selfie". The only difference between the selfie and document // versions is what happens when you click the FileInput, so this test clicks // the file input, then checks that the full screen div opened - const { getByRole, getByLabelText } = render( - - - - - , + expect(screen.getByRole('dialog')).to.be.ok(); + }); + + it('calls trackEvent from onSelfieCaptureOpen', () => { + // In real use the `start` method opens the Acuant SDK full screen selfie capture window. + // Because we can't do that in test (AcuantSDK does not allow), this doesn't attempt to load + // the SDK. Instead, it simply calls the callback that happens when a photo is captured. + // This allows us to test everything about that callback -except- the Acuant SDK parts. + initialize({ + selfieStart: sinon.stub().callsFake((callbacks) => { + callbacks.onOpened(); + }), + }); + + expect(trackEvent).to.be.calledWith('IdV: selfie image clicked'); + expect(trackEvent).to.be.calledWith('IdV: Acuant SDK loaded'); + + expect(trackEvent).to.have.been.calledWith('idv_sdk_selfie_image_capture_opened'); + }); + + it('calls trackEvent from onSelfieCaptureClosed', () => { + // In real use the `start` method opens the Acuant SDK full screen selfie capture window. + // Because we can't do that in test (AcuantSDK does not allow), this doesn't attempt to load + // the SDK. Instead, it simply calls the callback that happens when a photo is captured. + // This allows us to test everything about that callback -except- the Acuant SDK parts. + initialize({ + selfieStart: sinon.stub().callsFake((callbacks) => { + callbacks.onClosed(); + }), + }); + + expect(trackEvent).to.be.calledWith('IdV: selfie image clicked'); + expect(trackEvent).to.be.calledWith('IdV: Acuant SDK loaded'); + + expect(trackEvent).to.have.been.calledWith( + 'idv_sdk_selfie_image_capture_closed_without_photo', ); + }); - await userEvent.click(getByLabelText('Image')); - expect(getByRole('dialog')).to.be.ok(); + it('calls trackEvent from onSelfieCaptureSuccess', () => { + // In real use the `start` method opens the Acuant SDK full screen selfie capture window. + // Because we can't do that in test (AcuantSDK does not allow), this doesn't attempt to load + // the SDK. Instead, it simply calls the callback that happens when a photo is captured. + // This allows us to test everything about that callback -except- the Acuant SDK parts. + initialize({ + selfieStart: sinon.stub().callsFake((callbacks) => { + callbacks.onCaptured(); + }), + }); + + expect(trackEvent).to.be.calledWith('IdV: selfie image clicked'); + expect(trackEvent).to.be.calledWith('IdV: Acuant SDK loaded'); + + expect(trackEvent).to.have.been.calledWith( + 'idv_sdk_selfie_image_added', + sinon.match({ + attempt: sinon.match.number, + }), + ); + }); + + it('calls trackEvent from onSelfieCaptureFailure', () => { + const errorHash = { code: 1, message: 'Camera permission not granted' }; + + // In real use the `start` method opens the Acuant SDK full screen selfie capture window. + // Because we can't do that in test (AcuantSDK does not allow), this doesn't attempt to load + // the SDK. Instead, it simply calls the callback that happens when a photo is captured. + // This allows us to test everything about that callback -except- the Acuant SDK parts. + initialize({ + selfieStart: sinon.stub().callsFake((callbacks) => { + callbacks.onError(errorHash); + }), + }); + + expect(trackEvent).to.be.calledWith('IdV: selfie image clicked'); + expect(trackEvent).to.be.calledWith('IdV: Acuant SDK loaded'); + + expect(trackEvent).to.have.been.calledWith( + 'idv_sdk_selfie_image_capture_failed', + sinon.match({ + sdk_error_code: sinon.match.number, + sdk_error_message: sinon.match.string, + }), + ); }); }); diff --git a/spec/javascript/support/document-capture.jsx b/spec/javascript/support/document-capture.jsx index 24dea01116a..5259b26eded 100644 --- a/spec/javascript/support/document-capture.jsx +++ b/spec/javascript/support/document-capture.jsx @@ -70,6 +70,8 @@ export function useAcuant() { isCameraSupported = true, start = sinon.stub(), end = sinon.stub(), + selfieStart = sinon.stub(), + selfieEnd = sinon.stub(), triggerCapture = sinon.stub(), } = {}) { window.AcuantJavascriptWebSdk = { @@ -92,7 +94,7 @@ export function useAcuant() { }), end, }; - window.AcuantPassiveLiveness = { start: sinon.stub(), end: sinon.stub() }; + window.AcuantPassiveLiveness = { start: selfieStart, end: selfieEnd }; window.loadAcuantSdk = () => {}; const sdkScript = document.querySelector('[data-acuant-sdk]'); sdkScript.onload();