diff --git a/app/assets/javascripts/i18n-strings.js.erb b/app/assets/javascripts/i18n-strings.js.erb index 6f4328d58c2..148f6c2d349 100644 --- a/app/assets/javascripts/i18n-strings.js.erb +++ b/app/assets/javascripts/i18n-strings.js.erb @@ -50,6 +50,7 @@ window.LoginGov = window.LoginGov || {}; 'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases', 'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns', 'doc_auth.buttons.take_picture', + 'doc_auth.buttons.take_picture_retry', 'doc_auth.forms.selected_file', 'doc_auth.forms.change_file', 'doc_auth.forms.choose_file_html', @@ -57,13 +58,20 @@ window.LoginGov = window.LoginGov || {}; 'doc_auth.headings.document_capture', 'doc_auth.headings.document_capture_front', 'doc_auth.headings.document_capture_back', + 'doc_auth.headings.document_capture_selfie', 'doc_auth.headings.front', + 'doc_auth.headings.photo', + 'doc_auth.headings.selfie', + 'doc_auth.instructions.document_capture_selfie_instructions', 'doc_auth.tips.document_capture_header_text', 'doc_auth.tips.document_capture_hint', 'doc_auth.tips.document_capture_id_text1', 'doc_auth.tips.document_capture_id_text2', 'doc_auth.tips.document_capture_id_text3', 'doc_auth.tips.document_capture_id_text4', + 'doc_auth.tips.document_capture_selfie_text1', + 'doc_auth.tips.document_capture_selfie_text2', + 'doc_auth.tips.document_capture_selfie_text3', 'users.personal_key.close' ] %> diff --git a/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx index 429afa5244e..fbecee013ee 100644 --- a/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx @@ -1,6 +1,36 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; +/** + * @typedef AcuantImage + * + * @prop {string} data Base64-encoded image data. + * @prop {number} width Image width. + * @prop {number} height Image height. + */ + +/** + * @typedef AcuantSuccessResponse + * + * @prop {AcuantImage} image Image object. + * @prop {boolean} isPassport Whether document is passport. + * @prop {number} glare Detected image glare. + * @prop {number} sharpness Detected image sharpness. + * @prop {number} dpi Detected image resolution. + * + * @see https://github.com/Acuant/JavascriptWebSDKV11/tree/11.3.3/SimpleHTMLApp#acuantcamera + */ + +/** + * @typedef AcuantCaptureCanvasProps + * + * @prop {(response:AcuantSuccessResponse)=>void} onImageCaptureSuccess Success callback. + * @prop {(error:Error)=>void} onImageCaptureFailure Failure callback. + */ + +/** + * @param {AcuantCaptureCanvasProps} props Component props. + */ function AcuantCaptureCanvas({ onImageCaptureSuccess, onImageCaptureFailure }) { useEffect(() => { window.AcuantCameraUI.start(onImageCaptureSuccess, onImageCaptureFailure); diff --git a/app/javascript/app/document-capture/components/acuant-capture.jsx b/app/javascript/app/document-capture/components/acuant-capture.jsx index e7f04d35458..ca3fb87c290 100644 --- a/app/javascript/app/document-capture/components/acuant-capture.jsx +++ b/app/javascript/app/document-capture/components/acuant-capture.jsx @@ -1,46 +1,80 @@ import React, { useContext, useState } from 'react'; +import PropTypes from 'prop-types'; import AcuantContext from '../context/acuant'; import AcuantCaptureCanvas from './acuant-capture-canvas'; +import FileInput from './file-input'; import FullScreen from './full-screen'; +import Button from './button'; import useI18n from '../hooks/use-i18n'; +import DeviceContext from '../context/device'; +import DataURLFile from '../models/data-url-file'; -function AcuantCapture() { - const { isReady, isError } = useContext(AcuantContext); +function AcuantCapture({ label, hint, bannerText, value, onChange, className }) { + const { isReady, isError, isCameraSupported } = useContext(AcuantContext); const [isCapturing, setIsCapturing] = useState(false); - const [capture, setCapture] = useState(null); + const { isMobile } = useContext(DeviceContext); const { t } = useI18n(); + const hasCapture = !isError && (isReady ? isCameraSupported : isMobile); - if (isError) { - return 'Error!'; - } - - if (!isReady) { - return 'Loading…'; - } - - if (capture) { - const { data, width, height } = capture.image; - return Captured result; + let startCaptureIfSupported; + if (hasCapture) { + startCaptureIfSupported = (event) => { + event.preventDefault(); + setIsCapturing(true); + }; } return ( - <> +
{isCapturing && ( setIsCapturing(false)}> { - setCapture(nextCapture); + onChange(nextCapture.image.data); setIsCapturing(false); }} onImageCaptureFailure={() => setIsCapturing(false)} /> )} - - + + {hasCapture && ( + + )} +
); } +AcuantCapture.propTypes = { + label: PropTypes.string.isRequired, + hint: PropTypes.string, + bannerText: PropTypes.string, + value: PropTypes.instanceOf(DataURLFile), + onChange: PropTypes.func, + className: PropTypes.string, +}; + +AcuantCapture.defaultProps = { + hint: null, + value: null, + bannerText: null, + onChange: () => {}, + className: null, +}; + export default AcuantCapture; diff --git a/app/javascript/app/document-capture/components/button.jsx b/app/javascript/app/document-capture/components/button.jsx index d9ce9060252..d762ecf10f9 100644 --- a/app/javascript/app/document-capture/components/button.jsx +++ b/app/javascript/app/document-capture/components/button.jsx @@ -1,8 +1,25 @@ import React from 'react'; import PropTypes from 'prop-types'; -function Button({ type, onClick, children, isPrimary, isDisabled, className }) { - const classes = ['btn', isPrimary && 'btn-primary btn-wide', className].filter(Boolean).join(' '); +function Button({ + type, + onClick, + children, + isPrimary, + isSecondary, + isDisabled, + isUnstyled, + className, +}) { + const classes = [ + 'btn', + isPrimary && 'btn-primary btn-wide', + isSecondary && 'btn-secondary', + isUnstyled && 'btn-link', + className, + ] + .filter(Boolean) + .join(' '); return ( // Disable reason: We can assume `type` is provided as valid, or the default `button`. @@ -18,7 +35,9 @@ Button.propTypes = { onClick: PropTypes.func, children: PropTypes.node, isPrimary: PropTypes.bool, + isSecondary: PropTypes.bool, isDisabled: PropTypes.bool, + isUnstyled: PropTypes.bool, className: PropTypes.string, }; @@ -27,7 +46,9 @@ Button.defaultProps = { onClick: () => {}, children: null, isPrimary: false, + isSecondary: false, isDisabled: false, + isUnstyled: false, className: undefined, }; diff --git a/app/javascript/app/document-capture/components/document-capture.jsx b/app/javascript/app/document-capture/components/document-capture.jsx index 9b0616d569b..be80a842d3d 100644 --- a/app/javascript/app/document-capture/components/document-capture.jsx +++ b/app/javascript/app/document-capture/components/document-capture.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import AcuantCapture from './acuant-capture'; import FormSteps from './form-steps'; import DocumentsStep, { isValid as isDocumentsStepValid } from './documents-step'; +import SelfieStep, { isValid as isSelfieStepValid } from './selfie-step'; import Submission from './submission'; function DocumentCapture() { @@ -19,9 +19,9 @@ function DocumentCapture() { }, { name: 'selfie', - component: AcuantCapture, + component: SelfieStep, + isValid: isSelfieStepValid, }, - { name: 'confirm', component: () => 'Confirm?' }, ]} onComplete={setFormValues} /> diff --git a/app/javascript/app/document-capture/components/documents-step.jsx b/app/javascript/app/document-capture/components/documents-step.jsx index 4e64138ea69..d20581008ab 100644 --- a/app/javascript/app/document-capture/components/documents-step.jsx +++ b/app/javascript/app/document-capture/components/documents-step.jsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import FileInput from './file-input'; +import AcuantCapture from './acuant-capture'; import PageHeading from './page-heading'; import useI18n from '../hooks/use-i18n'; import DeviceContext from '../context/device'; @@ -33,7 +33,7 @@ function DocumentsStep({ value, onChange }) { const inputKey = `${side}_image`; return ( - onChange({ [inputKey]: nextValue })} className="id-card-file-input" diff --git a/app/javascript/app/document-capture/components/file-input.jsx b/app/javascript/app/document-capture/components/file-input.jsx index 466b68a8f02..60e9b7f9188 100644 --- a/app/javascript/app/document-capture/components/file-input.jsx +++ b/app/javascript/app/document-capture/components/file-input.jsx @@ -89,7 +89,7 @@ export function toDataURL(file) { }); } -function FileInput({ label, hint, bannerText, accept, value, errors, onChange, className }) { +function FileInput({ label, hint, bannerText, accept, value, errors, onClick, onChange }) { const { t, formatHTML } = useI18n(); const ifStillMounted = useIfStillMounted(); const instanceId = useInstanceId(); @@ -123,7 +123,7 @@ function FileInput({ label, hint, bannerText, accept, value, errors, onChange, c return (
@@ -206,6 +206,7 @@ function FileInput({ label, hint, bannerText, accept, value, errors, onChange, c className="usa-file-input__input" type="file" onChange={onChangeAsDataURL} + onClick={onClick} accept={accept ? accept.join() : undefined} aria-describedby={hint ? hintId : null} /> @@ -222,8 +223,8 @@ FileInput.propTypes = { accept: PropTypes.arrayOf(PropTypes.string), value: PropTypes.instanceOf(DataURLFile), errors: PropTypes.arrayOf(PropTypes.string), + onClick: PropTypes.func, onChange: PropTypes.func, - className: PropTypes.string, }; FileInput.defaultProps = { @@ -232,8 +233,8 @@ FileInput.defaultProps = { accept: null, value: undefined, errors: [], + onClick: () => {}, onChange: () => {}, - className: null, }; export default FileInput; diff --git a/app/javascript/app/document-capture/components/selfie-step.jsx b/app/javascript/app/document-capture/components/selfie-step.jsx new file mode 100644 index 00000000000..1fe9709a958 --- /dev/null +++ b/app/javascript/app/document-capture/components/selfie-step.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import PageHeading from './page-heading'; +import useI18n from '../hooks/use-i18n'; +import AcuantCapture from './acuant-capture'; +import DataURLFile from '../models/data-url-file'; + +function SelfieStep({ value, onChange }) { + const { t } = useI18n(); + + return ( + <> + {t('doc_auth.headings.selfie')} +

+ {t('doc_auth.instructions.document_capture_selfie_instructions')} +

+

{t('doc_auth.tips.document_capture_header_text')}

+ + onChange({ selfie: nextSelfie })} + /> + + ); +} + +SelfieStep.propTypes = { + value: PropTypes.shape({ + selfie: PropTypes.instanceOf(DataURLFile), + }), + onChange: PropTypes.func, +}; + +SelfieStep.defaultProps = { + value: {}, + onChange: () => {}, +}; + +/** + * Returns true if the step is valid for the given values, or false otherwise. + * + * @param {Record} value Current form values. + * + * @return {boolean} Whether step is valid. + */ +export const isValid = (value) => Boolean(value.selfie); + +export default SelfieStep; diff --git a/app/javascript/app/document-capture/context/acuant.jsx b/app/javascript/app/document-capture/context/acuant.jsx index b7c4e266d31..7fd02041a5b 100644 --- a/app/javascript/app/document-capture/context/acuant.jsx +++ b/app/javascript/app/document-capture/context/acuant.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; const AcuantContext = createContext({ isReady: false, isError: false, + isCameraSupported: null, credentials: null, endpoint: null, }); @@ -11,9 +12,11 @@ const AcuantContext = createContext({ function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { const [isReady, setIsReady] = useState(false); const [isError, setIsError] = useState(false); - const value = useMemo(() => ({ isReady, isError, endpoint, credentials }), [ + const [isCameraSupported, setIsCameraSupported] = useState(/** @type {?boolean} */ (null)); + const value = useMemo(() => ({ isReady, isError, isCameraSupported, endpoint, credentials }), [ isReady, isError, + isCameraSupported, endpoint, credentials, ]); @@ -24,7 +27,10 @@ function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { const originalOnAcuantSdkLoaded = window.onAcuantSdkLoaded; window.onAcuantSdkLoaded = () => { window.AcuantJavascriptWebSdk.initialize(credentials, endpoint, { - onSuccess: () => setIsReady(true), + onSuccess: () => { + setIsReady(true); + setIsCameraSupported(window.AcuantCamera.isCameraSupported); + }, onFail: () => setIsError(true), }); }; diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index e2016e4f217..884fb66253e 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -34,7 +34,8 @@ en: and selfie document_capture_selfie: Your photo front: Front - selfie: Take a selfie. + photo: Photo + selfie: Take a selfie ssn: Please enter your social security number. take_pic_back: Take a photo of the back of your ID take_pic_front: Take a photo of the front of your ID diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 1c8cc75b10a..0c94cf0e716 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -35,7 +35,8 @@ es: por el estado y una foto suya document_capture_selfie: Tu foto front: Frente - selfie: Toma una selfie. + photo: Foto + selfie: Toma una selfie ssn: Por favor ingrese su número de seguro social. take_pic_back: Toma una foto de la parte posterior de tu identificación take_pic_front: Toma una foto del frente de tu identificación diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 7497f89fd8e..89564146c2e 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -37,7 +37,8 @@ fr: officielle et une photo de vous document_capture_selfie: Ta photo front: De face - selfie: Prendre un selfie. + photo: Photo + selfie: Prendre un selfie ssn: S'il vous plaît entrez votre numéro de sécurité sociale. take_pic_back: Prenez une photo au verso de votre identifiant take_pic_front: Prenez une photo du recto de votre identifiant diff --git a/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx b/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx index 49afedf44ae..921a7b8d71c 100644 --- a/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx @@ -1,9 +1,11 @@ import React from 'react'; import { fireEvent, cleanup } from '@testing-library/react'; +import { waitForElementToBeRemoved } from '@testing-library/dom'; import sinon from 'sinon'; import render from '../../../support/render'; import AcuantCapture from '../../../../../app/javascript/app/document-capture/components/acuant-capture'; import { Provider as AcuantContextProvider } from '../../../../../app/javascript/app/document-capture/context/acuant'; +import DeviceContext from '../../../../../app/javascript/app/document-capture/context/device'; describe('document-capture/components/acuant-capture', () => { afterEach(() => { @@ -12,34 +14,51 @@ describe('document-capture/components/acuant-capture', () => { // unsubscribe will attempt to reference globals that no longer exist. cleanup(); delete window.AcuantJavascriptWebSdk; + delete window.AcuantCamera; delete window.AcuantCameraUI; }); - it('renders a loading indicator while acuant is not ready', () => { - const { container } = render( - - - , + it('renders without capture button while acuant is not ready and on desktop', () => { + const { getByText } = render( + + + + + , ); - expect(container.textContent).to.equal('Loading…'); + expect(() => getByText('doc_auth.buttons.take_picture')).to.throw(); }); - it('renders an error indicator if acuant script fails to load', async () => { - const { findByText } = render( - - - , + it('renders with assumed capture button support while acuant is not ready and on mobile', () => { + const { getByText } = render( + + + + + , + ); + + expect(getByText('doc_auth.buttons.take_picture')).to.be.ok(); + }); + + it('renders without capture button indicator if acuant script fails to load', async () => { + const { getByText } = render( + + + + + , ); - expect(await findByText('Error!')).to.be.ok(); + await waitForElementToBeRemoved(getByText('doc_auth.buttons.take_picture')); expect(console).to.have.loggedError(/^Error: Could not load script:/); }); - it('renders an error indicator if acuant fails to initialize', () => { - const { container } = render( + it('renders without capture button if acuant fails to initialize', () => { + const { getByText } = render( - + , ); @@ -48,19 +67,20 @@ describe('document-capture/components/acuant-capture', () => { }; window.onAcuantSdkLoaded(); - expect(container.textContent).to.equal('Error!'); + expect(() => getByText('doc_auth.buttons.take_picture')).to.throw(); }); it('renders a button when successfully loaded', () => { const { getByText } = render( - + , ); window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); const button = getByText('doc_auth.buttons.take_picture'); @@ -71,13 +91,14 @@ describe('document-capture/components/acuant-capture', () => { it('renders a canvas when capturing', () => { const { getByText } = render( - + , ); window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); window.AcuantCameraUI = { start: sinon.spy(), end: sinon.spy() }; @@ -88,24 +109,45 @@ describe('document-capture/components/acuant-capture', () => { expect(window.AcuantCameraUI.end.called).to.be.false(); }); - it('renders the captured image on successful capture', () => { - const { getByText, getByAltText } = render( + it('starts capturing when clicking input on supported device', () => { + const { getByLabelText } = render( + + + , + ); + + window.AcuantJavascriptWebSdk = { + initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), + }; + window.AcuantCamera = { isCameraSupported: true }; + window.onAcuantSdkLoaded(); + window.AcuantCameraUI = { start: sinon.spy(), end: sinon.spy() }; + + const button = getByLabelText('Image'); + fireEvent.click(button); + + expect(window.AcuantCameraUI.start.calledOnce).to.be.true(); + expect(window.AcuantCameraUI.end.called).to.be.false(); + }); + + it('calls onChange with the captured image on successful capture', () => { + const onChange = sinon.spy(); + const { getByText } = render( - + , ); window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); window.AcuantCameraUI = { start(onImageCaptureSuccess) { const capture = { image: { data: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"/%3E', - width: 10, - height: 20, }, }; onImageCaptureSuccess(capture); @@ -116,25 +158,23 @@ describe('document-capture/components/acuant-capture', () => { const button = getByText('doc_auth.buttons.take_picture'); fireEvent.click(button); - const image = getByAltText('Captured result'); - - expect(image).to.be.ok(); - expect(image.src).to.equal('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"/%3E'); - expect(image.width).to.equal(10); - expect(image.height).to.equal(20); + expect(onChange.getCall(0).args).to.deep.equal([ + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"/%3E', + ]); expect(window.AcuantCameraUI.end.calledOnce).to.be.true(); }); it('renders the button when the capture failed', () => { const { getByText } = render( - + , ); window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); window.AcuantCameraUI = { start(_onImageCaptureSuccess, onImageCaptureFailure) { @@ -154,13 +194,14 @@ describe('document-capture/components/acuant-capture', () => { it('ends the capture when the component unmounts', () => { const { getByText, unmount } = render( - + , ); window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); window.AcuantCameraUI = { start: sinon.spy(), @@ -174,4 +215,10 @@ describe('document-capture/components/acuant-capture', () => { expect(window.AcuantCameraUI.end.calledOnce).to.be.true(); }); + + it('renders with custom className', () => { + const { container } = render(); + + expect(container.firstChild.classList.contains('my-custom-class')).to.be.true(); + }); }); diff --git a/spec/javascripts/app/document-capture/components/button-spec.jsx b/spec/javascripts/app/document-capture/components/button-spec.jsx index 65813ed28cf..f1ea4f1b550 100644 --- a/spec/javascripts/app/document-capture/components/button-spec.jsx +++ b/spec/javascripts/app/document-capture/components/button-spec.jsx @@ -15,7 +15,9 @@ describe('document-capture/components/button', () => { expect(button.type).to.equal('button'); expect(button.classList.contains('btn')).to.be.true(); expect(button.classList.contains('btn-primary')).to.be.false(); + expect(button.classList.contains('btn-secondary')).to.be.false(); expect(button.classList.contains('btn-wide')).to.be.false(); + expect(button.classList.contains('btn-link')).to.be.false(); }); it('calls click callback with no arguments', () => { @@ -35,7 +37,31 @@ describe('document-capture/components/button', () => { const button = getByText('Click me'); expect(button.classList.contains('btn-primary')).to.be.true(); + expect(button.classList.contains('btn-secondary')).to.be.false(); expect(button.classList.contains('btn-wide')).to.be.true(); + expect(button.classList.contains('btn-link')).to.be.false(); + }); + + it('renders as secondary', () => { + const { getByText } = render(); + + const button = getByText('Click me'); + + expect(button.classList.contains('btn-primary')).to.be.false(); + expect(button.classList.contains('btn-secondary')).to.be.true(); + expect(button.classList.contains('btn-wide')).to.be.false(); + expect(button.classList.contains('btn-link')).to.be.false(); + }); + + it('renders as unstyled', () => { + const { getByText } = render(); + + const button = getByText('Click me'); + + expect(button.classList.contains('btn-primary')).to.be.false(); + expect(button.classList.contains('btn-secondary')).to.be.false(); + expect(button.classList.contains('btn-wide')).to.be.false(); + expect(button.classList.contains('btn-link')).to.be.true(); }); it('renders as disabled', () => { diff --git a/spec/javascripts/app/document-capture/components/document-capture-spec.jsx b/spec/javascripts/app/document-capture/components/document-capture-spec.jsx index ee87f949455..69cf26e0cce 100644 --- a/spec/javascripts/app/document-capture/components/document-capture-spec.jsx +++ b/spec/javascripts/app/document-capture/components/document-capture-spec.jsx @@ -5,6 +5,16 @@ import render from '../../../support/render'; import DocumentCapture from '../../../../../app/javascript/app/document-capture/components/document-capture'; describe('document-capture/components/document-capture', () => { + let originalHash; + + beforeEach(() => { + originalHash = window.location.hash; + }); + + afterEach(() => { + window.location.hash = originalHash; + }); + it('renders the form steps', () => { const { getByText } = render(); @@ -27,11 +37,16 @@ describe('document-capture/components/document-capture', () => { const continueButton = getByText('forms.buttons.continue'); await waitFor(() => expect(continueButton.disabled).to.be.false()); userEvent.click(continueButton); - userEvent.click(getByText('forms.buttons.continue')); - userEvent.click(getByText('forms.buttons.submit.default')); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_selfie'), + new window.File([''], 'selfie.png', { type: 'image/png' }), + ); + const submitButton = getByText('forms.buttons.submit.default'); + await waitFor(() => expect(submitButton.disabled).to.be.false()); + userEvent.click(submitButton); const confirmation = await findByText( - 'Finished sending: {"front_image":"data:image/png;base64,","back_image":"data:image/png;base64,"}', + 'Finished sending: {"front_image":"data:image/png;base64,","back_image":"data:image/png;base64,","selfie":"data:image/png;base64,"}', ); expect(confirmation).to.be.ok(); diff --git a/spec/javascripts/app/document-capture/components/file-input-spec.jsx b/spec/javascripts/app/document-capture/components/file-input-spec.jsx index f9b2dacb1d7..b79842bd3fb 100644 --- a/spec/javascripts/app/document-capture/components/file-input-spec.jsx +++ b/spec/javascripts/app/document-capture/components/file-input-spec.jsx @@ -103,7 +103,7 @@ describe('document-capture/components/file-input', () => { expect(isImage('data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==')).to.be.false(); }); - it('returns false if given file is not an image (data url string)', () => { + it('returns true if given file is an image', () => { expect( isImage('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'), ).to.be.true(); @@ -140,12 +140,6 @@ describe('document-capture/components/file-input', () => { }); }); - it('renders with custom className', () => { - const { container } = render(); - - expect(container.firstChild.classList.contains('my-custom-class')).to.be.true(); - }); - it('renders file input with label', () => { const { getByLabelText } = render(); diff --git a/spec/javascripts/app/document-capture/context/acuant.jsx b/spec/javascripts/app/document-capture/context/acuant-spec.jsx similarity index 93% rename from spec/javascripts/app/document-capture/context/acuant.jsx rename to spec/javascripts/app/document-capture/context/acuant-spec.jsx index e9708829ba0..784755597af 100644 --- a/spec/javascripts/app/document-capture/context/acuant.jsx +++ b/spec/javascripts/app/document-capture/context/acuant-spec.jsx @@ -7,6 +7,7 @@ import AcuantContext, { describe('document-capture/context/acuant', () => { afterEach(() => { delete window.AcuantJavascriptWebSdk; + delete window.AcuantCamera; }); function ContextReader() { @@ -20,6 +21,7 @@ describe('document-capture/context/acuant', () => { expect(JSON.parse(container.textContent)).to.eql({ isReady: false, isError: false, + isCameraSupported: null, credentials: null, endpoint: null, }); @@ -47,6 +49,7 @@ describe('document-capture/context/acuant', () => { expect(JSON.parse(container.textContent)).to.eql({ isReady: false, isError: false, + isCameraSupported: null, credentials: 'a', endpoint: 'b', }); @@ -62,11 +65,13 @@ describe('document-capture/context/acuant', () => { window.AcuantJavascriptWebSdk = { initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), }; + window.AcuantCamera = { isCameraSupported: true }; window.onAcuantSdkLoaded(); expect(JSON.parse(container.textContent)).to.eql({ isReady: true, isError: false, + isCameraSupported: true, credentials: null, endpoint: null, }); @@ -87,6 +92,7 @@ describe('document-capture/context/acuant', () => { expect(JSON.parse(container.textContent)).to.eql({ isReady: false, isError: true, + isCameraSupported: null, credentials: null, endpoint: null, });