From 0c3bbfb61195604937bcca952ea1ef933af5ae61 Mon Sep 17 00:00:00 2001 From: Eric Gade Date: Thu, 4 Aug 2022 16:58:17 -0400 Subject: [PATCH 01/10] Initial commit of LG-6777 work -- What We are saving progress, but still trying to figure out where to inject the faile attempts check in order to trigger manual camera/upload flow. --- .../document-capture/components/acuant-capture.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 7e9aa5aaba5..3bc65802187 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -282,9 +282,8 @@ function AcuantCapture( const [attempt, incrementAttempt] = useCounter(1); const [acuantFailureCookie, setAcuantFailureCookie, refreshAcuantFailureCookie] = useCookie('AcuantCameraHasFailed'); - const { onFailedCaptureAttempt, onResetFailedCaptureAttempts } = useContext( - FailedCaptureAttemptsContext, - ); + const { failedCaptureAttempts, onFailedCaptureAttempt, onResetFailedCaptureAttempts } = + useContext(FailedCaptureAttemptsContext); const hasCapture = !isError && (isReady ? isCameraSupported : isMobile); useEffect(() => { // If capture had started before Acuant was ready, stop capture if readiness reveals that no @@ -390,6 +389,9 @@ function AcuantCapture( */ function startCaptureOrTriggerUpload(event) { if (event.target === inputRef.current) { + if (failedCaptureAttempts >= 2) { + return forceUpload(); + } const isAcuantCaptureCapable = hasCapture && !acuantFailureCookie; const shouldStartAcuantCapture = isAcuantCaptureCapable && capture !== 'user' && !isForceUploading.current; From 3e6ae99e3b29baf395de444baf9b690cdbaeabdf Mon Sep 17 00:00:00 2001 From: Parissa Date: Fri, 5 Aug 2022 12:00:56 -0400 Subject: [PATCH 02/10] LG-6777 - add maxAttemptsBeforeNativeCamera variable --- .../document-capture/components/acuant-capture.jsx | 8 ++++++-- .../document-capture/context/failed-capture-attempts.jsx | 4 +++- app/javascript/packs/document-capture.jsx | 2 ++ app/views/idv/shared/_document_capture.html.erb | 1 + config/application.yml.default | 1 + lib/identity_config.rb | 1 + 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 3bc65802187..aba66a897fb 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -282,7 +282,7 @@ function AcuantCapture( const [attempt, incrementAttempt] = useCounter(1); const [acuantFailureCookie, setAcuantFailureCookie, refreshAcuantFailureCookie] = useCookie('AcuantCameraHasFailed'); - const { failedCaptureAttempts, onFailedCaptureAttempt, onResetFailedCaptureAttempts } = + const { failedCaptureAttempts, maxAttemptsBeforeNativeCamera, onFailedCaptureAttempt, onResetFailedCaptureAttempts } = useContext(FailedCaptureAttemptsContext); const hasCapture = !isError && (isReady ? isCameraSupported : isMobile); useEffect(() => { @@ -389,7 +389,11 @@ function AcuantCapture( */ function startCaptureOrTriggerUpload(event) { if (event.target === inputRef.current) { - if (failedCaptureAttempts >= 2) { + + if (failedCaptureAttempts >= maxAttemptsBeforeNativeCamera) { + addPageAction(`IdV: Force native camera. Failed attempts: ${failedCaptureAttempts}`, { + field: name, + }); return forceUpload(); } const isAcuantCaptureCapable = hasCapture && !acuantFailureCookie; diff --git a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx index 18bd4734bbf..0f6309f10af 100644 --- a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx +++ b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx @@ -33,6 +33,7 @@ const FailedCaptureAttemptsContext = createContext( onFailedCaptureAttempt: () => {}, onResetFailedCaptureAttempts: () => {}, maxFailedAttemptsBeforeTips: Infinity, + maxAttemptsBeforeNativeCamera: Infinity, lastAttemptMetadata: DEFAULT_LAST_ATTEMPT_METADATA, }), ); @@ -49,7 +50,7 @@ FailedCaptureAttemptsContext.displayName = 'FailedCaptureAttemptsContext'; /** * @param {FailedCaptureAttemptsContextProviderProps} props */ -function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBeforeTips }) { +function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBeforeTips, maxAttemptsBeforeNativeCamera }) { const [lastAttemptMetadata, setLastAttemptMetadata] = useState( /** @type {CaptureAttemptMetadata} */ (DEFAULT_LAST_ATTEMPT_METADATA), ); @@ -71,6 +72,7 @@ function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBefor onFailedCaptureAttempt, onResetFailedCaptureAttempts, maxFailedAttemptsBeforeTips, + maxAttemptsBeforeNativeCamera, lastAttemptMetadata, }} > diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index 2a8f1a8e6d0..a268e82d097 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -131,6 +131,7 @@ function addPageAction(event, payload) { const { helpCenterRedirectUrl: helpCenterRedirectURL, maxCaptureAttemptsBeforeTips, + maxAttemptsBeforeNativeCamera, appName, flowPath, cancelUrl: cancelURL, @@ -180,6 +181,7 @@ function addPageAction(event, payload) { FailedCaptureAttemptsContextProvider, { maxFailedAttemptsBeforeTips: Number(maxCaptureAttemptsBeforeTips), + maxAttemptsBeforeNativeCamera: Number(maxAttemptsBeforeNativeCamera), }, ], [DocumentCapture, { isAsyncForm, onStepChange: keepAlive }], diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 7333af9c835..4316d38eb20 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -29,6 +29,7 @@ sharpness_threshold: IdentityConfig.store.doc_auth_client_sharpness_threshold, status_poll_interval_ms: IdentityConfig.store.poll_rate_for_verify_in_seconds * 1000, max_capture_attempts_before_tips: IdentityConfig.store.doc_auth_max_capture_attempts_before_tips, + max_attempts_before_native_camera: IdentityConfig.store.doc_auth_max_attempts_before_native_camera, sp_name: sp_name, flow_path: flow_path, cancel_url: idv_cancel_path, diff --git a/config/application.yml.default b/config/application.yml.default index c8f835fddda..fe1ae6bb8ac 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -78,6 +78,7 @@ doc_auth_error_glare_threshold: 40 doc_auth_error_sharpness_threshold: 40 doc_auth_max_attempts: 20 doc_auth_max_capture_attempts_before_tips: 3 +doc_auth_max_attempts_before_native_camera: 2 doc_capture_request_valid_for_minutes: 15 email_from: no-reply@login.gov email_from_display_name: Login.gov diff --git a/lib/identity_config.rb b/lib/identity_config.rb index bbeeed761d2..a8a146bec30 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -142,6 +142,7 @@ def self.build_store(config_map) config.add(:doc_auth_error_sharpness_threshold, type: :integer) config.add(:doc_auth_extend_timeout_by_minutes, type: :integer) config.add(:doc_auth_max_attempts, type: :integer) + config.add(:doc_auth_max_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_capture_attempts_before_tips, type: :integer) config.add(:doc_auth_s3_request_timeout, type: :integer) config.add(:doc_auth_vendor, type: :string) From 4d3a8f903f9b97e129e128b7ae02821a7a53d7d4 Mon Sep 17 00:00:00 2001 From: Parissa Date: Fri, 5 Aug 2022 13:42:48 -0400 Subject: [PATCH 03/10] fix test --- .../document-capture/context/failed-capture-attempts.jsx | 4 ++-- .../document-capture/context/failed-capture-attempts-spec.jsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx index 0f6309f10af..7a50164f98c 100644 --- a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx +++ b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx @@ -32,8 +32,8 @@ const FailedCaptureAttemptsContext = createContext( failedCaptureAttempts: 0, onFailedCaptureAttempt: () => {}, onResetFailedCaptureAttempts: () => {}, - maxFailedAttemptsBeforeTips: Infinity, maxAttemptsBeforeNativeCamera: Infinity, + maxFailedAttemptsBeforeTips: Infinity, lastAttemptMetadata: DEFAULT_LAST_ATTEMPT_METADATA, }), ); @@ -71,8 +71,8 @@ function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBefor failedCaptureAttempts, onFailedCaptureAttempt, onResetFailedCaptureAttempts, - maxFailedAttemptsBeforeTips, maxAttemptsBeforeNativeCamera, + maxFailedAttemptsBeforeTips, lastAttemptMetadata, }} > diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index a32f59b10d3..924bdebebed 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -13,6 +13,7 @@ describe('document-capture/context/failed-capture-attempts', () => { 'onFailedCaptureAttempt', 'onResetFailedCaptureAttempts', 'maxFailedAttemptsBeforeTips', + 'maxAttemptsBeforeNativeCamera', 'lastAttemptMetadata', ]); expect(result.current.failedCaptureAttempts).to.equal(0); From 1d51da5f9e8d18a4f3aaa7d2001e096bf71cf7e1 Mon Sep 17 00:00:00 2001 From: Parissa Date: Fri, 5 Aug 2022 14:21:53 -0400 Subject: [PATCH 04/10] add test for analytics --- .../context/failed-capture-attempts-spec.jsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index 924bdebebed..eebbf072cfe 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -1,8 +1,17 @@ import { useContext } from 'react'; import { renderHook } from '@testing-library/react-hooks'; +import { DeviceContext, AnalyticsContext } from '@18f/identity-document-capture'; +import AcuantContext, { + dirname, + Provider as AcuantContextProvider, + DEFAULT_ACCEPTABLE_GLARE_SCORE, + DEFAULT_ACCEPTABLE_SHARPNESS_SCORE, +} from '@18f/identity-document-capture/context/acuant'; + import FailedCaptureAttemptsContext, { Provider, } from '@18f/identity-document-capture/context/failed-capture-attempts'; +import sinon from "sinon"; describe('document-capture/context/failed-capture-attempts', () => { it('has expected default properties', () => { @@ -45,5 +54,31 @@ describe('document-capture/context/failed-capture-attempts', () => { expect(result.current.lastAttemptMetadata).to.deep.equal(metadata); }); + context('failed acuant camera attempts', () => { + let result; + let addPageAction; + + + addPageAction = sinon.spy(); + ({ result } = renderHook(() => useContext(AcuantContext), { + wrapper: ({ children }) => ( + + + + {children} + + + + ), + })); + + + it('calls analytics with native camera message when failed attempts is greater than 2', () => { + expect(addPageAction).to.have.been.calledWith( + 'IdV: Force native camera. Failed attempts: 3', + {success: true}, + ); + }) + }); }); }); From 5a4cfe681537f650bea0439d384f774e0ad5b49d Mon Sep 17 00:00:00 2001 From: Eric Gade Date: Tue, 9 Aug 2022 11:21:06 -0400 Subject: [PATCH 05/10] Updaing test file to remove node error --- .../context/failed-capture-attempts-spec.jsx | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index eebbf072cfe..74a96646e4f 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -11,7 +11,7 @@ import AcuantContext, { import FailedCaptureAttemptsContext, { Provider, } from '@18f/identity-document-capture/context/failed-capture-attempts'; -import sinon from "sinon"; +import sinon from 'sinon'; describe('document-capture/context/failed-capture-attempts', () => { it('has expected default properties', () => { @@ -29,6 +29,7 @@ describe('document-capture/context/failed-capture-attempts', () => { expect(result.current.onFailedCaptureAttempt).to.be.a('function'); expect(result.current.onResetFailedCaptureAttempts).to.be.a('function'); expect(result.current.maxFailedAttemptsBeforeTips).to.be.a('number'); + expect(result.current.maxAttemptsBeforeNativeCamera).to.be.a('number'); expect(result.current.lastAttemptMetadata).to.be.an('object'); }); @@ -54,31 +55,38 @@ describe('document-capture/context/failed-capture-attempts', () => { expect(result.current.lastAttemptMetadata).to.deep.equal(metadata); }); - context('failed acuant camera attempts', () => { - let result; - let addPageAction; + context('failed acuant camera attempts', function () { + //let result; + //let addPageAction; - - addPageAction = sinon.spy(); - ({ result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( + let addPageAction = sinon.spy(); + const { result } = renderHook(() => useContext(FailedCaptureAttemptsContext), { + wrapper: ({ children }) => ( + - - - {children} - - + {children} - ), - })); - + + ), + }); + // const { result } = renderHook(() => useContext(FailedCaptureAttemptsContext), { + // wrapper: ({ children }) => ( + // + // {children} + // + // ), + // }); + result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); + result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); + result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); - it('calls analytics with native camera message when failed attempts is greater than 2', () => { + it('calls analytics with native camera message when failed attempts is greater than 2', function () { + //expect(addPageAction).to.have.been.called(); expect(addPageAction).to.have.been.calledWith( 'IdV: Force native camera. Failed attempts: 3', - {success: true}, + `Failed attempts: ${result.current.failedCaptureAttempts}`, ); - }) + }); }); }); }); From a06e5d2e0f35233201398a1bffffd7330a4fc209 Mon Sep 17 00:00:00 2001 From: Eric Gade Date: Tue, 9 Aug 2022 17:30:18 -0400 Subject: [PATCH 06/10] Updating FailedCaptureContext with new dynamic forceNativeCamera -- What For clarity in the FailedCaptureContext interface, we've added a property called `forceNativeCamera` which is a boolean value that will update in the FailedCaptureContextProvider whenever the maxAttemptsBeforeNativeCamera is equal to or exceeds the current failedCaptures value. Additionally, we finally have some working tests for this. For the moment, we only get results by forcing maxAttemptsBeforeNativeCamera to be 0, then clicking the file input. This seems to do the trick. But we need to figure out how to test a couple of other scenarios, and how to force failures from that point in a test case. --- .../components/acuant-capture.jsx | 14 ++- .../context/failed-capture-attempts-spec.jsx | 118 +++++++++++++----- 2 files changed, 96 insertions(+), 36 deletions(-) diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index aba66a897fb..3b0d6b3f14c 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -282,8 +282,15 @@ function AcuantCapture( const [attempt, incrementAttempt] = useCounter(1); const [acuantFailureCookie, setAcuantFailureCookie, refreshAcuantFailureCookie] = useCookie('AcuantCameraHasFailed'); - const { failedCaptureAttempts, maxAttemptsBeforeNativeCamera, onFailedCaptureAttempt, onResetFailedCaptureAttempts } = - useContext(FailedCaptureAttemptsContext); + + const { + failedCaptureAttempts, + maxAttemptsBeforeNativeCamera, + onFailedCaptureAttempt, + onResetFailedCaptureAttempts, + forceNativeCamera, + } = useContext(FailedCaptureAttemptsContext); + const hasCapture = !isError && (isReady ? isCameraSupported : isMobile); useEffect(() => { // If capture had started before Acuant was ready, stop capture if readiness reveals that no @@ -389,8 +396,7 @@ function AcuantCapture( */ function startCaptureOrTriggerUpload(event) { if (event.target === inputRef.current) { - - if (failedCaptureAttempts >= maxAttemptsBeforeNativeCamera) { + if (forceNativeCamera) { addPageAction(`IdV: Force native camera. Failed attempts: ${failedCaptureAttempts}`, { field: name, }); diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index 74a96646e4f..f7051d35136 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -1,13 +1,15 @@ import { useContext } from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { DeviceContext, AnalyticsContext } from '@18f/identity-document-capture'; +import userEvent from '@testing-library/user-event'; +import { DeviceContext, AnalyticsContext, UploadContext } from '@18f/identity-document-capture'; import AcuantContext, { dirname, Provider as AcuantContextProvider, DEFAULT_ACCEPTABLE_GLARE_SCORE, DEFAULT_ACCEPTABLE_SHARPNESS_SCORE, } from '@18f/identity-document-capture/context/acuant'; - +import AcuantCapture from '@18f/identity-document-capture/components/acuant-capture'; +import { useAcuant, render } from '../../../support/document-capture'; import FailedCaptureAttemptsContext, { Provider, } from '@18f/identity-document-capture/context/failed-capture-attempts'; @@ -19,6 +21,7 @@ describe('document-capture/context/failed-capture-attempts', () => { expect(result.current).to.have.keys([ 'failedCaptureAttempts', + 'forceNativeCamera', 'onFailedCaptureAttempt', 'onResetFailedCaptureAttempts', 'maxFailedAttemptsBeforeTips', @@ -36,7 +39,11 @@ describe('document-capture/context/failed-capture-attempts', () => { describe('Provider', () => { it('sets increments on onFailedCaptureAttempt', () => { const { result } = renderHook(() => useContext(FailedCaptureAttemptsContext), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => ( + + {children} + + ), }); result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); @@ -55,38 +62,85 @@ describe('document-capture/context/failed-capture-attempts', () => { expect(result.current.lastAttemptMetadata).to.deep.equal(metadata); }); - context('failed acuant camera attempts', function () { - //let result; - //let addPageAction; + }); +}); - let addPageAction = sinon.spy(); - const { result } = renderHook(() => useContext(FailedCaptureAttemptsContext), { - wrapper: ({ children }) => ( - - - {children} - - - ), - }); - // const { result } = renderHook(() => useContext(FailedCaptureAttemptsContext), { - // wrapper: ({ children }) => ( - // - // {children} - // - // ), - // }); - result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); - result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); - result.current.onFailedCaptureAttempt({ isAssessedAsGlare: true, isAssessedAsBlurry: false }); +describe('FailedCaptureAttemptsContext testing of forceNativeCamera logic', () => { + it('Updating to a number of failed captures less than maxAttemptsBeforeNativeCamera will keep forceNativeCamera as false', () => { + const { result, rerender } = renderHook(() => useContext(FailedCaptureAttemptsContext), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + result.current.onFailedCaptureAttempt({ + isAssessedAsGlare: true, + isAssessedAsBlurry: false, + }); + rerender(true); + expect(result.current.failedCaptureAttempts).to.equal(1); + expect(result.current.forceNativeCamera).to.equal(false); + }); + it('Updating failed captures to a number gte the maxAttemptsBeforeNativeCamera will set forceNativeCamera to true', () => { + const { result, rerender } = renderHook(() => useContext(FailedCaptureAttemptsContext), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + result.current.onFailedCaptureAttempt({ + isAssessedAsGlare: true, + isAssessedAsBlurry: false, + }); + rerender(true); + expect(result.current.forceNativeCamera).to.equal(false); + result.current.onFailedCaptureAttempt({ + isAssessedAsGlare: true, + isAssessedAsBlurry: false, + }); + rerender(true); + expect(result.current.forceNativeCamera).to.equal(true); + result.current.onFailedCaptureAttempt({ + isAssessedAsGlare: true, + isAssessedAsBlurry: false, + }); + rerender({}); + expect(result.current.failedCaptureAttempts).to.equal(3); + expect(result.current.forceNativeCamera).to.equal(true); + }); +}); - it('calls analytics with native camera message when failed attempts is greater than 2', function () { - //expect(addPageAction).to.have.been.called(); - expect(addPageAction).to.have.been.calledWith( - 'IdV: Force native camera. Failed attempts: 3', - `Failed attempts: ${result.current.failedCaptureAttempts}`, +describe('maxAttemptsBeforeNativeCamera loggin tests', () => { + context('failed acuant camera attempts', function () { + const { initialize } = useAcuant(); + it('calls analytics with native camera message when failed attempts is greater than or equal to 2', async function () { + let addPageAction = sinon.spy(); + const acuantCaptureComponent = ; + const TestComponent = ({ children }) => { + return ( + + + + + {acuantCaptureComponent} + {children} + + + + ); - }); + + initialize(); + }; + const result = render(); + const user = userEvent.setup(); + const fileInput = result.container.querySelector('input[type="file"]'); + expect(fileInput).to.exist(); + await user.click(fileInput); + expect(addPageAction).to.have.been.called(); + expect(addPageAction).to.have.been.calledWith('IdV: Force native camera. Failed attempts: 0'); }); }); }); From 88a6a1ea8b12f12c00354c6e87384c0f1f71d1ff Mon Sep 17 00:00:00 2001 From: Eric Gade Date: Wed, 10 Aug 2022 15:09:55 -0400 Subject: [PATCH 07/10] Updating frontend tests for maxAttemptsBeforeNativeCamera changelog: Improvements, Acuant, updating max attempts before login --- .../context/failed-capture-attempts-spec.jsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index f7051d35136..4dd8e6ac144 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -112,10 +112,18 @@ describe('FailedCaptureAttemptsContext testing of forceNativeCamera logic', () = }); }); -describe('maxAttemptsBeforeNativeCamera loggin tests', () => { +describe('maxAttemptsBeforeNativeCamera logging tests', () => { context('failed acuant camera attempts', function () { const { initialize } = useAcuant(); - it('calls analytics with native camera message when failed attempts is greater than or equal to 2', async function () { + /** + * NOTE: We have to force maxAttemptsBeforeLogin to be 0 here + * in order to test this interactively. This is because the react + * testing library does not provide consistent ways to test using both + * a component's elements (for triggering clicks) and a component's + * subscribed context changes. You can use either render or renderHook, + * but not both. + */ + it('calls analytics with native camera message when failed attempts is greater than or equal to 0', async function () { let addPageAction = sinon.spy(); const acuantCaptureComponent = ; const TestComponent = ({ children }) => { @@ -131,10 +139,9 @@ describe('maxAttemptsBeforeNativeCamera loggin tests', () => { ); - initialize(); }; - const result = render(); + let result = render(); const user = userEvent.setup(); const fileInput = result.container.querySelector('input[type="file"]'); expect(fileInput).to.exist(); From bfe3064b767d7e3e01d93fbf9a4f027d4c9aea06 Mon Sep 17 00:00:00 2001 From: eric-gade Date: Thu, 11 Aug 2022 13:54:29 -0400 Subject: [PATCH 08/10] Updating type annotations for new FailedCaptureContext structure -- What This commit updates various JSDoc type annotations so that they accurately describe the new structure of the FailedCaptureAttempts context. Specifically, we added maxAttemptsBeforeNativeCamera and forceNativeCamera to this context in previous commits. Now the types and provider components accurately reflect those changes. --- .../context/failed-capture-attempts.jsx | 13 ++++++++++++- app/javascript/packs/document-capture.jsx | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx index 7a50164f98c..0ce5887c00e 100644 --- a/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx +++ b/app/javascript/packages/document-capture/context/failed-capture-attempts.jsx @@ -18,7 +18,9 @@ import useCounter from '../hooks/use-counter'; * attempt, to increment attempts. * @prop {() => void} onResetFailedCaptureAttempts Callback to trigger a reset of attempts. * @prop {number} maxFailedAttemptsBeforeTips Number of failed attempts before showing tips. + * @prop {number} maxAttemptsBeforeNativeCamera Number of attempts before forcing the use of the native camera (if available) * @prop {CaptureAttemptMetadata} lastAttemptMetadata Metadata about the last attempt. + * @prop {boolean} forceNativeCamera Whether or not to force use of the native camera. Is set to true if the number of failedCaptureAttempts is equal to or greater than maxAttemptsBeforeNativeCamera */ /** @type {CaptureAttemptMetadata} */ @@ -35,6 +37,7 @@ const FailedCaptureAttemptsContext = createContext( maxAttemptsBeforeNativeCamera: Infinity, maxFailedAttemptsBeforeTips: Infinity, lastAttemptMetadata: DEFAULT_LAST_ATTEMPT_METADATA, + forceNativeCamera: false, }), ); @@ -45,18 +48,25 @@ FailedCaptureAttemptsContext.displayName = 'FailedCaptureAttemptsContext'; * * @prop {ReactNode} children * @prop {number} maxFailedAttemptsBeforeTips + * @prop {number} maxAttemptsBeforeNativeCamera */ /** * @param {FailedCaptureAttemptsContextProviderProps} props */ -function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBeforeTips, maxAttemptsBeforeNativeCamera }) { +function FailedCaptureAttemptsContextProvider({ + children, + maxFailedAttemptsBeforeTips, + maxAttemptsBeforeNativeCamera, +}) { const [lastAttemptMetadata, setLastAttemptMetadata] = useState( /** @type {CaptureAttemptMetadata} */ (DEFAULT_LAST_ATTEMPT_METADATA), ); const [failedCaptureAttempts, incrementFailedCaptureAttempts, onResetFailedCaptureAttempts] = useCounter(); + const forceNativeCamera = failedCaptureAttempts >= maxAttemptsBeforeNativeCamera; + /** * @param {CaptureAttemptMetadata} metadata */ @@ -74,6 +84,7 @@ function FailedCaptureAttemptsContextProvider({ children, maxFailedAttemptsBefor maxAttemptsBeforeNativeCamera, maxFailedAttemptsBeforeTips, lastAttemptMetadata, + forceNativeCamera, }} > {children} diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index a268e82d097..f2ea46db5a4 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -40,6 +40,7 @@ import { trackEvent } from '@18f/identity-analytics'; * @prop {string} helpCenterRedirectUrl * @prop {string} appName * @prop {string} maxCaptureAttemptsBeforeTips + * @prop {string} maxAttemptsBeforeNativeCamera * @prop {FlowPath} flowPath * @prop {string} cancelUrl * @prop {string=} idvInPersonUrl From 143086e7c95b606c4781c4b631f869e102f76fcd Mon Sep 17 00:00:00 2001 From: eric-gade Date: Thu, 11 Aug 2022 14:56:56 -0400 Subject: [PATCH 09/10] Adding passing test for negative logging case -- What Adding a test to ensure that logging is *not* called in the case where failed attempts is not equal or greater to the max attempts before native camera is forced. changelog: Improvements, Acuant Camera settings, updating max failures before native camera is triggered --- .../context/failed-capture-attempts-spec.jsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index 4dd8e6ac144..2320eaa7d5b 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -149,5 +149,34 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => { expect(addPageAction).to.have.been.called(); expect(addPageAction).to.have.been.calledWith('IdV: Force native camera. Failed attempts: 0'); }); + + it('Does not call analytics with native camera message when failed attempts less than 2', async function () { + let addPageAction = sinon.spy(); + const acuantCaptureComponent = ; + const TestComponent = ({ children }) => { + return ( + + + + + {acuantCaptureComponent} + {children} + + + + + ); + initialize(); + }; + let result = render(); + const user = userEvent.setup(); + const fileInput = result.container.querySelector('input[type="file"]'); + expect(fileInput).to.exist(); + await user.click(fileInput); + //expect(addPageAction).to.have.been.called(); + expect(addPageAction).to.not.have.been.calledWith( + 'IdV: Force native camera. Failed attempts: 2', + ); + }); }); }); From 42616f0c392cd77669cd2dcb7d5379663425d675 Mon Sep 17 00:00:00 2001 From: eric-gade Date: Fri, 12 Aug 2022 12:26:57 -0400 Subject: [PATCH 10/10] Fixing linter errors in JS files changelog: Improvements, Acuant native camera, linting fixes --- .../components/acuant-capture.jsx | 51 +++++++++---------- .../context/failed-capture-attempts-spec.jsx | 35 +++++-------- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 3b0d6b3f14c..7568f3d044e 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -285,7 +285,6 @@ function AcuantCapture( const { failedCaptureAttempts, - maxAttemptsBeforeNativeCamera, onFailedCaptureAttempt, onResetFailedCaptureAttempts, forceNativeCamera, @@ -387,6 +386,31 @@ function AcuantCapture( isSuppressingClickLogging.current = false; } + /** + * Triggers upload to occur, regardless of support for direct capture. This is necessary since the + * default behavior for interacting with the file input is intercepted when capture is supported. + * Calling `forceUpload` will flag the click handling to skip intercepting the event as capture. + */ + function forceUpload() { + if (!inputRef.current) { + return; + } + + isForceUploading.current = true; + + const originalCapture = inputRef.current.getAttribute('capture'); + + if (originalCapture !== null) { + inputRef.current.removeAttribute('capture'); + } + + withoutClickLogging(() => inputRef.current?.click()); + + if (originalCapture !== null) { + inputRef.current.setAttribute('capture', originalCapture); + } + } + /** * Responds to a click by starting capture if supported in the environment, or triggering the * default file picker prompt. The click event may originate from the file input itself, or @@ -429,31 +453,6 @@ function AcuantCapture( } } - /** - * Triggers upload to occur, regardless of support for direct capture. This is necessary since the - * default behavior for interacting with the file input is intercepted when capture is supported. - * Calling `forceUpload` will flag the click handling to skip intercepting the event as capture. - */ - function forceUpload() { - if (!inputRef.current) { - return; - } - - isForceUploading.current = true; - - const originalCapture = inputRef.current.getAttribute('capture'); - - if (originalCapture !== null) { - inputRef.current.removeAttribute('capture'); - } - - withoutClickLogging(() => inputRef.current?.click()); - - if (originalCapture !== null) { - inputRef.current.setAttribute('capture', originalCapture); - } - } - /** * @param {AcuantSuccessResponse} nextCapture */ diff --git a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx index 2320eaa7d5b..4ec3510cf61 100644 --- a/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/failed-capture-attempts-spec.jsx @@ -1,19 +1,14 @@ import { useContext } from 'react'; import { renderHook } from '@testing-library/react-hooks'; import userEvent from '@testing-library/user-event'; -import { DeviceContext, AnalyticsContext, UploadContext } from '@18f/identity-document-capture'; -import AcuantContext, { - dirname, - Provider as AcuantContextProvider, - DEFAULT_ACCEPTABLE_GLARE_SCORE, - DEFAULT_ACCEPTABLE_SHARPNESS_SCORE, -} from '@18f/identity-document-capture/context/acuant'; +import { DeviceContext, AnalyticsContext } from '@18f/identity-document-capture'; +import { Provider as AcuantContextProvider } from '@18f/identity-document-capture/context/acuant'; import AcuantCapture from '@18f/identity-document-capture/components/acuant-capture'; -import { useAcuant, render } from '../../../support/document-capture'; import FailedCaptureAttemptsContext, { Provider, } from '@18f/identity-document-capture/context/failed-capture-attempts'; import sinon from 'sinon'; +import { useAcuant, render } from '../../../support/document-capture'; describe('document-capture/context/failed-capture-attempts', () => { it('has expected default properties', () => { @@ -115,6 +110,7 @@ describe('FailedCaptureAttemptsContext testing of forceNativeCamera logic', () = describe('maxAttemptsBeforeNativeCamera logging tests', () => { context('failed acuant camera attempts', function () { const { initialize } = useAcuant(); + initialize(); /** * NOTE: We have to force maxAttemptsBeforeLogin to be 0 here * in order to test this interactively. This is because the react @@ -124,9 +120,9 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => { * but not both. */ it('calls analytics with native camera message when failed attempts is greater than or equal to 0', async function () { - let addPageAction = sinon.spy(); - const acuantCaptureComponent = ; - const TestComponent = ({ children }) => { + const addPageAction = sinon.spy(); + const acuantCaptureComponent = ; + function TestComponent({ children }) { return ( @@ -139,9 +135,8 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => { ); - initialize(); - }; - let result = render(); + } + const result = render(); const user = userEvent.setup(); const fileInput = result.container.querySelector('input[type="file"]'); expect(fileInput).to.exist(); @@ -151,9 +146,9 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => { }); it('Does not call analytics with native camera message when failed attempts less than 2', async function () { - let addPageAction = sinon.spy(); - const acuantCaptureComponent = ; - const TestComponent = ({ children }) => { + const addPageAction = sinon.spy(); + const acuantCaptureComponent = ; + function TestComponent({ children }) { return ( @@ -166,14 +161,12 @@ describe('maxAttemptsBeforeNativeCamera logging tests', () => { ); - initialize(); - }; - let result = render(); + } + const result = render(); const user = userEvent.setup(); const fileInput = result.container.querySelector('input[type="file"]'); expect(fileInput).to.exist(); await user.click(fileInput); - //expect(addPageAction).to.have.been.called(); expect(addPageAction).to.not.have.been.calledWith( 'IdV: Force native camera. Failed attempts: 2', );