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
;
+ 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')}
+
+ - {t('doc_auth.tips.document_capture_selfie_text1')}
+ - {t('doc_auth.tips.document_capture_selfie_text2')}
+ - {t('doc_auth.tips.document_capture_selfie_text3')}
+
+
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,
});