diff --git a/src/applications/simple-forms/20-10207/components/FileField.jsx b/src/applications/simple-forms/20-10207/components/FileField.jsx deleted file mode 100644 index a28978894c6b..000000000000 --- a/src/applications/simple-forms/20-10207/components/FileField.jsx +++ /dev/null @@ -1,731 +0,0 @@ -/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ -/* Customized copy of Platform's FileField component - * - Adds new optional prop `ariaLabelAdditionalText` to append additional - * text to the upload button's aria-label attribute. -*/ -import PropTypes from 'prop-types'; -import React, { useEffect, useState, useRef } from 'react'; -import { connect } from 'react-redux'; -import classNames from 'classnames'; -import { - VaCard, - VaModal, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - -import { toggleValues } from 'platform/site-wide/feature-toggles/selectors'; -import get from 'platform/utilities/data/get'; -import set from 'platform/utilities/data/set'; -import unset from 'platform/utilities/data/unset'; -import { - displayFileSize, - focusElement, - scrollTo, - scrollToFirstError, -} from 'platform/utilities/ui'; - -import { FILE_UPLOAD_NETWORK_ERROR_MESSAGE } from 'platform/forms-system/src/js/constants'; -import { $ } from 'platform/forms-system/src/js/utilities/ui'; -import { - ShowPdfPassword, - PasswordLabel, - PasswordSuccess, - readAndCheckFile, - checkTypeAndExtensionMatches, - checkIsEncryptedPdf, - FILE_TYPE_MISMATCH_ERROR, -} from 'platform/forms-system/src/js/utilities/file'; -import { usePreviousValue } from 'platform/forms-system/src/js/helpers'; - -/** - * Modal content callback - * @typedef ModalContent - * @type {function} - * @property {string} fileName - name of file to be removed - * @returns {JSX} - default='We’ll delete the uploaded file - * {fileName}' - */ -/** - * UI options used in FileField - * @typedef uiOptions - * @type {object} - * @property {string} buttonText='Upload' - upload button text - * @property {string} addAnotherLabel='Upload another' - upload another text, - * replaces upload button text when greater than one upload is showing - * @property {string} ariaLabelAdditionalText='' - additional screen-reader text to be appended to upload button's aria-label attribute. - * @property {string} tryAgain='Try again' - button in enableShortWorkflow - * @property {string} newFile='Upload a new file' - button in enableShortWorkflow - * @property {string} cancel='Cancel' - button visible while uploading & in enableShortWorkflow - * @property {string} delete='Delete file' - delete button text - * @property {string} modalTitle='Are you sure you want to delete this file?' - - * delete confirmation modal title - * @property {ModalContent} modalContent - delete confirmation modal content - * @property {string} yesButton='Yes, delete this' - modal Yes button text - * @property {string} noButton='No, keep this' - modal No button text - */ -/** - * FormData of supported files - * @typeof Files - * @type {object} - * @property {string} name - file name - * @property {boolean} uploading - flag indicating that an upload is in - * progress - * @property {string} confirmationCode - uuid of uploaded file - * @property {string} attachmentId - form ID set by user - * @property {string} errorMessage - error message string returned from API - * @property {boolean} isEncrypted - (Encrypted PDF only; pre-upload only) - * encrypted state of the file - * @property {DOMFileObject} file - (Encrypted PDF only) File object, used - * when user submits password - */ -const FileField = props => { - const { - enableShortWorkflow, - errorSchema, - formContext, - formData = [], - idSchema, - onBlur, - onChange, - registry, - schema, - uiSchema, - } = props; - - const files = formData || []; - const [progress, setProgress] = useState(0); - const [uploadRequest, setUploadRequest] = useState(null); - const [isUploading, setIsUploading] = useState( - files.some(file => file.uploading), - ); - const [showRemoveModal, setShowRemoveModal] = useState(false); - const [removeIndex, setRemoveIndex] = useState(null); - const [initialized, setInitialized] = useState(false); - - const previousValue = usePreviousValue(formData); - const fileInputRef = useRef(null); - const fileButtonRef = useRef(null); - - const uiOptions = uiSchema?.['ui:options']; - - const maxItems = schema.maxItems || Infinity; - const { SchemaField } = registry.fields; - const attachmentIdRequired = schema.additionalItems.required - ? schema.additionalItems.required.includes('attachmentId') - : false; - const uswds = true; - - const content = { - upload: uiOptions.buttonText || 'Upload', - uploadAnother: uiOptions.addAnotherLabel || 'Upload another', - ariaLabelAdditionalText: uiOptions.ariaLabelAdditionalText || '', - passwordLabel: fileName => `Add a password for ${fileName}`, - tryAgain: 'Try again', - tryAgainLabel: fileName => `Try uploading ${fileName} again`, - newFile: 'Upload a new file', - cancel: 'Cancel', - cancelLabel: fileName => `Cancel upload of ${fileName}`, - delete: 'Delete file', - deleteLabel: fileName => `Delete ${fileName}`, - modalTitle: - uiOptions.modalTitle || 'Are you sure you want to delete this file?', - modalContent: fileName => - uiOptions.modalContent?.(fileName || 'Unknown') || ( - - We’ll delete the uploaded file{' '} - {fileName || 'Unknown'} - - ), - yesButton: 'Yes, delete this file', - noButton: 'No, keep this', - error: 'Error', - }; - - const Tag = formContext.onReviewPage && formContext.reviewMode ? 'dl' : 'div'; - - // hide upload & delete buttons on review & submit page when reviewing - const showButtons = !formContext.reviewMode && !isUploading; - - const titleString = - typeof uiSchema['ui:title'] === 'string' - ? uiSchema['ui:title'] - : schema.title; - - const getFileListId = index => `${idSchema.$id}_file_${index}`; - - // This is always true if enableShortWorkflow is not enabled - // If enabled, do not allow upload if any error exist - const checkUploadVisibility = () => - !enableShortWorkflow || - (enableShortWorkflow && - !files.some((file, index) => { - const errors = - errorSchema?.[index]?.__errors || - [file.errorMessage].filter(error => error); - - return errors.length > 0; - })); - - const focusAddAnotherButton = () => { - // Add a timeout to allow for the upload button to reappear in the DOM - // before trying to focus on it - setTimeout(() => { - // focus on upload button, not the label - focusElement( - // including `#upload-button` because RTL can't access the shadowRoot - 'button, #upload-button', - {}, - $(`#upload-button`)?.shadowRoot, - ); - }, 100); - }; - - const updateProgress = percent => { - setProgress(percent); - }; - - useEffect( - () => { - const prevFiles = previousValue || []; - fileButtonRef?.current?.classList?.toggle( - 'vads-u-display--none', - !checkUploadVisibility(), - ); - if (initialized && files.length !== prevFiles.length) { - focusAddAnotherButton(); - } - - const hasUploading = files.some(file => file.uploading); - const wasUploading = prevFiles.some(file => file.uploading); - setIsUploading(hasUploading); - if (hasUploading && !wasUploading) { - setProgress(0); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [formData], - ); - - useEffect( - () => { - // The File object is not preserved in the save-in-progress data - // We need to remove these entries; an empty `file` is included in the - // entry, but if API File Object still exists (within the same session), we - // can't use Object.keys() on it because it returns an empty array - const newData = files.filter( - // keep - file may not exist (already uploaded) - // keep - file may contain File object; ensure name isn't empty - // remove - file may be an empty object - data => !data.file || (data.file?.name || '') !== '', - ); - if (newData.length !== files.length) { - onChange(newData); - } - setInitialized(true); - }, - [files, onChange], - ); - - /** - * Add file to list and upload - * @param {Event} event - DOM File upload event - * @param {number} index - uploaded file index, if already uploaded - * @param {string} password - encrypted PDF password, only defined by - * `onSubmitPassword` function - * @listens - */ - const onAddFile = async (event, index = null, password) => { - if (event.target?.files?.length) { - const currentFile = event.target.files[0]; - const allFiles = props.formData || []; - const addUiOptions = props.uiSchema['ui:options']; - // needed for FileField unit tests - const { mockReadAndCheckFile } = uiOptions; - - let idx = index; - if (idx === null) { - idx = allFiles.length === 0 ? 0 : allFiles.length; - } - - let checkResults; - const checks = { checkTypeAndExtensionMatches, checkIsEncryptedPdf }; - - if (currentFile.type === 'testing') { - // Skip read file for Cypress testing - checkResults = { - checkTypeAndExtensionMatches: true, - checkIsEncryptedPdf: false, - }; - } else { - // read file mock for unit testing - checkResults = - typeof mockReadAndCheckFile === 'function' - ? mockReadAndCheckFile() - : await readAndCheckFile(currentFile, checks); - } - - if (!checkResults.checkTypeAndExtensionMatches) { - allFiles[idx] = { - file: currentFile, - name: currentFile.name, - errorMessage: FILE_TYPE_MISMATCH_ERROR, - }; - props.onChange(allFiles); - return; - } - - // Check if the file is an encrypted PDF - if ( - currentFile.name?.endsWith('pdf') && - !password && - checkResults.checkIsEncryptedPdf - ) { - allFiles[idx] = { - file: currentFile, - name: currentFile.name, - isEncrypted: true, - }; - - props.onChange(allFiles); - // wait for user to enter a password before uploading - return; - } - - setUploadRequest( - props.formContext.uploadFile( - currentFile, - addUiOptions, - updateProgress, - file => { - // formData is undefined initially - const newData = props.formData || []; - newData[idx] = { ...file, isEncrypted: !!password }; - onChange(newData); - // Focus on the 'Cancel' button when a file is being uploaded - if (file.uploading) { - $('.schemaform-file-uploading .cancel-upload')?.focus(); - } - // Focus on the file card after the file has finished uploading - if (!file.uploading) { - $(getFileListId(idx))?.focus(); - } - setUploadRequest(null); - }, - () => { - setUploadRequest(null); - }, - formContext.trackingPrefix, - password, - props.enableShortWorkflow, - ), - ); - } - }; - - const onSubmitPassword = (file, index, password) => { - if (file && password) { - onAddFile({ target: { files: [file] } }, index, password); - } - }; - - const onAttachmentIdChange = (index, value) => { - if (!value) { - props.onChange(unset([index, 'attachmentId'], props.formData)); - } else { - props.onChange(set([index, 'attachmentId'], value, props.formData)); - } - }; - - const onAttachmentNameChange = (index, value) => { - if (!value) { - props.onChange(unset([index, 'name'], props.formData)); - } else { - props.onChange(set([index, 'name'], value, props.formData)); - } - }; - - const removeFile = (index, focusAddButton = true) => { - const newFileList = props.formData.filter((__, idx) => index !== idx); - if (!newFileList.length) { - props.onChange(); - } else { - props.onChange(newFileList); - } - - // clear file input value; without this, the user won't be able to open the - // upload file window - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - - // When other actions follow removeFile, we do not want to apply this focus - if (focusAddButton) { - focusAddAnotherButton(); - } - }; - - const openRemoveModal = index => { - setRemoveIndex(index); - setShowRemoveModal(true); - }; - - const closeRemoveModal = ({ remove = false } = {}) => { - const idx = removeIndex; - setRemoveIndex(null); - setShowRemoveModal(false); - if (remove) { - removeFile(idx); - } else { - setTimeout(() => { - focusElement( - 'button, .delete-upload', - {}, - $(`#${getFileListId(idx)} .delete-upload`)?.shadowRoot, - ); - }); - } - }; - - const cancelUpload = index => { - if (uploadRequest) { - uploadRequest.abort(); - } - removeFile(index); - }; - - const retryLastUpload = (index, file) => { - onAddFile({ target: { files: [file] } }, index); - }; - - const deleteThenAddFile = index => { - removeFile(index, false); - fileInputRef.current?.click(); - }; - - const getRetryFunction = (allowRetry, index, file) => { - return allowRetry - ? () => retryLastUpload(index, file) - : () => deleteThenAddFile(index); - }; - - const uploadText = content[files.length > 0 ? 'uploadAnother' : 'upload']; - - return ( -
- closeRemoveModal({ remove: true })} - onSecondaryButtonClick={closeRemoveModal} - visible={showRemoveModal} - uswds={uswds} - > -

- {removeIndex !== null - ? content.modalContent(files[removeIndex]?.name) - : null} -

-
- {files.length > 0 && ( - - )} - {// Don't render an upload button on review & submit page while in - // review mode - showButtons && ( - <> - {(maxItems === null || files.length < maxItems) && - // Prevent additional upload if any upload has error state - checkUploadVisibility() && ( - - )} - `.${item}`).join(',')} - className="vads-u-display--none" - id={idSchema.$id} - name={idSchema.$id} - onChange={onAddFile} - /> - - )} -
- ); -}; - -FileField.propTypes = { - schema: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - disabled: PropTypes.bool, - enableShortWorkflow: PropTypes.bool, - errorSchema: PropTypes.object, - formContext: PropTypes.shape({ - onReviewPage: PropTypes.bool, - reviewMode: PropTypes.bool, - trackingPrefix: PropTypes.string, - uploadFile: PropTypes.func, - }), - formData: PropTypes.array, - idSchema: PropTypes.object, - readonly: PropTypes.bool, - registry: PropTypes.shape({ - fields: PropTypes.shape({ - SchemaField: PropTypes.func, - }), - formContext: PropTypes.shape({}), - }), - requiredSchema: PropTypes.object, - uiSchema: PropTypes.object, - onBlur: PropTypes.func, -}; - -const mapStateToProps = state => ({ - enableShortWorkflow: toggleValues(state).file_upload_short_workflow_enabled, -}); - -export { FileField }; - -export default connect(mapStateToProps)(FileField); diff --git a/src/applications/simple-forms/20-10207/pages/evidenceALS.js b/src/applications/simple-forms/20-10207/pages/evidenceALS.js index 4f19c297cda4..b2707399966b 100644 --- a/src/applications/simple-forms/20-10207/pages/evidenceALS.js +++ b/src/applications/simple-forms/20-10207/pages/evidenceALS.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import AlsViewField from '../components/AlsViewField'; import { ALS_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/pages/evidenceFinancialHardship.js b/src/applications/simple-forms/20-10207/pages/evidenceFinancialHardship.js index b5db1a06328b..6ca59025a135 100644 --- a/src/applications/simple-forms/20-10207/pages/evidenceFinancialHardship.js +++ b/src/applications/simple-forms/20-10207/pages/evidenceFinancialHardship.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import FinancialHardshipViewField from '../components/FinancialHardshipViewField'; import { FINANCIAL_HARDSHIP_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/pages/evidenceMedalAward.js b/src/applications/simple-forms/20-10207/pages/evidenceMedalAward.js index a87c0187756c..0716d68760ee 100644 --- a/src/applications/simple-forms/20-10207/pages/evidenceMedalAward.js +++ b/src/applications/simple-forms/20-10207/pages/evidenceMedalAward.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import MedalAwardViewField from '../components/MedalAwardViewField'; import { MEDAL_AWARD_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/pages/evidencePowDocuments.js b/src/applications/simple-forms/20-10207/pages/evidencePowDocuments.js index 4e351601845a..6f7568fa2927 100644 --- a/src/applications/simple-forms/20-10207/pages/evidencePowDocuments.js +++ b/src/applications/simple-forms/20-10207/pages/evidencePowDocuments.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import PowViewField from '../components/PowViewField'; import { POW_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/pages/evidenceTerminalIllness.js b/src/applications/simple-forms/20-10207/pages/evidenceTerminalIllness.js index aaa5c73b32ef..c143dd8858de 100644 --- a/src/applications/simple-forms/20-10207/pages/evidenceTerminalIllness.js +++ b/src/applications/simple-forms/20-10207/pages/evidenceTerminalIllness.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import TerminalIllnessViewField from '../components/TerminalIllnessViewField'; import { TERMINAL_ILLNESS_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/pages/evidenceVSI.js b/src/applications/simple-forms/20-10207/pages/evidenceVSI.js index e831da3af135..9f2486a0c73b 100644 --- a/src/applications/simple-forms/20-10207/pages/evidenceVSI.js +++ b/src/applications/simple-forms/20-10207/pages/evidenceVSI.js @@ -1,7 +1,7 @@ import environment from 'platform/utilities/environment'; import { titleUI } from 'platform/forms-system/src/js/web-component-patterns/titlePattern'; -import { FileField } from '../components/FileField'; +import FileField from 'platform/forms-system/src/js/fields/FileField'; import VsiViewField from '../components/VsiViewField'; import { VSI_DESCRIPTION } from '../config/constants'; diff --git a/src/applications/simple-forms/20-10207/tests/e2e/fixtures/data/veteran.json b/src/applications/simple-forms/20-10207/tests/e2e/fixtures/data/veteran.json index 76cc03a71273..684734e18a83 100644 --- a/src/applications/simple-forms/20-10207/tests/e2e/fixtures/data/veteran.json +++ b/src/applications/simple-forms/20-10207/tests/e2e/fixtures/data/veteran.json @@ -22,7 +22,7 @@ }, "veteranPhone": "1234567890", "otherReasons": { - "OVER_85": true + "OVER_85": true }, "view:hasReceivedMedicalTreatment": true, "medicalTreatments": [ diff --git a/src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js b/src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js index 9bb9bb54413f..67e1d4a8177f 100644 --- a/src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js +++ b/src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js @@ -16,14 +16,7 @@ const responses = { 'GET /v0/feature_toggles': mockFeatureToggles, 'GET /v0/in_progress_forms/20-10207': mockSipGet, 'PUT /v0/in_progress_forms/20-10207': mockSipPut, - 'POST /simple_forms_api/v1/simple_forms/submit_financial_hardship_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_terminal_illness_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_als_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_vsi_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_pow_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_pow_documents2': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_medal_award_documents': mockUpload, - 'POST /simple_forms_api/v1/simple_forms/submit_medal_award_documents2': mockUpload, + 'POST /simple_forms_api/v1/simple_forms/submit_supporting_documents': mockUpload, 'POST /simple_forms_api/v1/simple_forms': mockSubmit, }; diff --git a/src/applications/simple-forms/20-10207/tests/unit/components/FileField.unit.spec.jsx b/src/applications/simple-forms/20-10207/tests/unit/components/FileField.unit.spec.jsx deleted file mode 100644 index 1f74ea199733..000000000000 --- a/src/applications/simple-forms/20-10207/tests/unit/components/FileField.unit.spec.jsx +++ /dev/null @@ -1,1505 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import { render, fireEvent, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; - -import { uploadStore } from 'platform/forms-system/test/config/helpers'; -import { - DefinitionTester, - getFormDOM, -} from 'platform/testing/unit/schemaform-utils'; - -import { FILE_UPLOAD_NETWORK_ERROR_MESSAGE } from 'platform/forms-system/src/js/constants'; -import { fileTypeSignatures } from 'platform/forms-system/src/js/utilities/file'; -import fileUploadUI, { - fileSchema, -} from 'platform/forms-system/src/js/definitions/file'; -import { $, $$ } from 'platform/forms-system/src/js/utilities/ui'; -import { MISSING_PASSWORD_ERROR } from 'platform/forms-system/src/js/validation'; -import { FileField } from '../../../components/FileField'; - -const formContext = { - setTouched: sinon.spy(), -}; -const requiredSchema = {}; - -describe('Schemaform ', () => { - it('should render', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - const uploadButton = $('#upload-button', container); - expect(uploadButton).to.have.attribute('text', 'Upload'); - const fileInput = $('input[type="file"]', container); - expect(fileInput).to.have.attribute('accept', '.pdf,.jpg,.jpeg,.png'); - }); - - it('should render files', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('li', container).textContent).to.contain('Test file name.pdf'); - expect($('strong.dd-privacy-hidden[data-dd-action-name]', container)).to - .exist; - }); - - it('should render uswds components', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files', { uswds: true }); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.delete-upload[uswds]', container)).to.exist; - expect($('#upload-button[uswds]', container)).to.exist; - }); - - it('should remove files with empty file object when initializing', async () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test1.png', - }, - { - file: {}, - name: 'Test2.pdf', - }, - { - file: { - name: 'fake', // should never happen - }, - name: 'Test3.txt', - }, - { - file: new File([1, 2, 3], 'Test4.jpg'), - name: 'Test4.jpg', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const onChange = sinon.spy(); - render( - , - ); - - await waitFor(() => { - expect(onChange.calledOnce).to.be.true; - expect(onChange.firstCall.args[0].length).to.equal(3); - // empty file object was removed; - expect(onChange.firstCall.args[0][1].name).to.equal('Test3.txt'); - }); - }); - - it('should call onChange once when deleting files', async () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const onChange = sinon.spy(); - - const { container } = render( - , - ); - - const modal = $('va-modal', container); - expect(modal.getAttribute('visible')).to.eq('false'); - - fireEvent.click($('.delete-upload', container)); - expect(modal.getAttribute('visible')).to.eq('true'); - - // click yes in modal - $('va-modal', container).__events.primaryButtonClick(); - - await waitFor(() => { - expect(onChange.calledOnce).to.be.true; - expect(onChange.firstCall.args.length).to.equal(0); - }); - }); - - it('should render uploading', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - name: 'Test.pdf', - uploading: true, - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('va-progress-bar', container)).to.exist; - const button = $('.cancel-upload', container); - expect(button).to.exist; - expect(button.getAttribute('text')).to.eq('Cancel'); - expect(button.getAttribute('label')).to.eq('Cancel upload of Test.pdf'); - }); - - it('should show progress', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - uploading: true, - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - const progressBar = $('va-progress-bar', container); - expect(progressBar.getAttribute('percent')).to.equal('0'); - - // How to call `updateProgress(20)`? This method doesn't work: - // https://github.com/testing-library/react-testing-library/issues/638#issuecomment-615937561 - // expect(progressBar.getAttribute('percent')).to.equal('20'); - }); - - it('should render error', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - errorMessage: 'some error', - }, - ]; - const errorSchema = { - 0: { - __errors: ['Bad error'], - }, - }; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // Prepend 'Error' for screenreader - expect($('.schemaform-file-error', container).textContent).to.contain( - 'Error Bad error', - ); - expect($('span[role="alert"]', container)).to.exist; - }); - - it('should not render upload button if over max items', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - maxItems: 1, - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.upload-button-label', container)).to.not.exist; - }); - - it('should not render upload or delete button on review & submit page while in review mode', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.upload-button-label', container)).to.not.exist; - expect($('.delete-upload', container)).to.not.exist; - }); - - it('should render upload or delete button on review & submit page while in edit mode', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - size: 12345678, - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.upload-button-label', container)).to.exist; - expect($('.delete-upload', container)).to.exist; - - const text = $('li', container).textContent; - expect(text).to.include('Test file name.pdf'); - expect(text).to.include('12MB'); - }); - - it('should delete file', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const { container } = render( - - - , - ); - - const uploadButton = $('#upload-button', container); - expect($$('li', container)).to.not.be.empty; - - const modal = $('va-modal', container); - expect(modal.getAttribute('visible')).to.eq('false'); - - fireEvent.click($('.delete-upload', container)); - expect(modal.getAttribute('visible')).to.eq('true'); - - // click yes in modal - $('va-modal', container).__events.primaryButtonClick(); - - await waitFor(() => { - expect($$('li', container)).to.be.empty; - expect(document.activeElement).to.eq(uploadButton); - }); - }); - - it('should not delete file when "No" is selected in modal', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const { container } = render( - - - , - ); - - expect($$('li', container)).to.not.be.empty; - - const modal = $('va-modal', container); - expect(modal.getAttribute('visible')).to.eq('false'); - - const deleteButton = $('.delete-upload', container); - fireEvent.click(deleteButton); - expect(modal.getAttribute('visible')).to.eq('true'); - - // click no in modal - $('va-modal', container).__events.secondaryButtonClick(); - - await waitFor(() => { - expect($$('li', container)).to.not.be.empty; - expect(document.activeElement).to.eq(deleteButton); - }); - }); - - it('should upload png file', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const mockFile = { - name: 'test.png', - type: fileTypeSignatures.png.mime, - }; - const uiOptions = { - ...uiSchema['ui:options'], - mockReadAndCheckFile: () => ({ - checkIsEncryptedPdf: false, - checkTypeAndExtensionMatches: true, - }), - }; - const uploadFile = sinon.spy(); - const form = render( - - - , - ); - const formDOM = getFormDOM(form); - - formDOM.files('input[type=file]', [mockFile]); - - await waitFor(() => { - expect(uploadFile.firstCall.args[0]).to.eql(mockFile); - expect(uploadFile.firstCall.args[1]).to.eql(uiOptions); - expect(uploadFile.firstCall.args[2]).to.be.a('function'); - expect(uploadFile.firstCall.args[3]).to.be.a('function'); - expect(uploadFile.firstCall.args[4]).to.be.a('function'); - }); - }); - - it('should upload unencrypted pdf file', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const uploadFile = sinon.spy(); - const mockPDFFile = { - name: 'test.PDF', - type: fileTypeSignatures.pdf.mime, - }; - const uiOptions = { - ...uiSchema['ui:options'], - mockReadAndCheckFile: () => ({ - checkIsEncryptedPdf: false, - checkTypeAndExtensionMatches: true, - }), - }; - const fileField = { - ...uiSchema, - 'ui:options': uiOptions, - }; - - const form = render( - - - , - ); - const formDOM = getFormDOM(form); - - formDOM.files('input[type=file]', [mockPDFFile]); - - await waitFor(() => { - expect(uploadFile.firstCall.args[0]).to.eql(mockPDFFile); - expect(uploadFile.firstCall.args[1]).to.eql(uiOptions); - expect(uploadFile.firstCall.args[2]).to.be.a('function'); - expect(uploadFile.firstCall.args[3]).to.be.a('function'); - expect(uploadFile.firstCall.args[4]).to.be.a('function'); - }); - }); - - it('should upload test file using "testing" file type to bypass checks', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const mockFile = { - name: 'test.pdf', - type: 'testing', - }; - const uploadFile = sinon.spy(); - const form = render( - - - , - ); - const formDOM = getFormDOM(form); - - formDOM.files('input[type=file]', [mockFile]); - - await waitFor(() => { - expect(uploadFile.firstCall.args[0]).to.eql(mockFile); - expect(uploadFile.firstCall.args[1]).to.eql(uiSchema['ui:options']); - expect(uploadFile.firstCall.args[2]).to.be.a('function'); - expect(uploadFile.firstCall.args[3]).to.be.a('function'); - expect(uploadFile.firstCall.args[4]).to.be.a('function'); - }); - }); - - it('should not call uploadFile when initially adding an encrypted PDF', async () => { - const uiSchema = fileUploadUI('Files'); - const schema = { - type: 'object', - properties: { - fileField: fileSchema, - }, - }; - const uploadFile = sinon.spy(); - const isFileEncrypted = () => Promise.resolve(true); - const fileField = { - ...uiSchema, - 'ui:options': { - ...uiSchema['ui:options'], - isFileEncrypted, - }, - }; - - const form = render( - - - , - ); - const formDOM = getFormDOM(form); - - formDOM.files('input[type=file]', [{ name: 'test-pw.pdf' }]); - - await waitFor(() => { - expect(uploadFile.notCalled).to.be.true; - }); - }); - - it('should render file with attachment type', () => { - let testProps = null; - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - items: [ - { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - size: 54321, - }, - ]; - const registry = { - fields: { - SchemaField: props => { - testProps = props; - return
; - }, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - const text = $('li', container).textContent; - expect(text).to.contain('Test file name.pdf'); - expect(text).to.contain('53KB'); - - expect(testProps.schema.type).to.eq('string'); - }); - - it('should render file with attachmentName', () => { - let testProps = null; - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - items: [ - { - type: 'object', - properties: { - name: { - type: 'string', - }, - }, - }, - ], - }; - const uiSchema = fileUploadUI('Files', { - attachmentName: ({ fileId, index }) => ({ - 'ui:title': 'Document name', - 'ui:options': { - widgetProps: { - 'aria-describedby': fileId, - 'data-index': index, - }, - }, - }), - }); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - size: 987654, - }, - ]; - const registry = { - fields: { - SchemaField: props => { - testProps = props; - return
; - }, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - const text = $('li').textContent; - expect(text).to.contain('Test file name.pdf'); - expect(text).to.contain('965KB'); - - const deleteButton = $('.delete-upload', container); - expect(deleteButton?.getAttribute('label')).to.eq( - 'Delete Test file name.pdf', - ); - - // check ids & index passed into SchemaField - expect(testProps.schema).to.equal(schema.items[0].properties.name); - expect(testProps.registry.formContext.pagePerItemIndex).to.eq(0); - - const { widgetProps } = testProps.uiSchema['ui:options']; - expect(widgetProps['aria-describedby']).to.eq('field_file_name_0'); - expect(widgetProps['data-index']).to.eq(0); - }); - - it('should render file with attachmentSchema', () => { - let testProps = null; - const idSchema = { - $id: 'field', - }; - const schema = { - type: 'array', - additionalItems: { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - items: [ - { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - ], - }; - const uiSchema = fileUploadUI('Files', { - attachmentName: false, - attachmentSchema: ({ fileId, index }) => ({ - 'ui:title': 'Document type', - 'ui:options': { - widgetProps: { - 'aria-describedby': fileId, - 'data-index': index, - }, - }, - }), - }); - const formData = [ - { - name: 'Test file name.pdf', - attachmentId: '1234', - }, - ]; - const registry = { - fields: { - SchemaField: props => { - testProps = props; - return
; - }, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.delete-upload', container).getAttribute('label')).to.eq( - 'Delete Test file name.pdf', - ); - - // check ids & index passed into SchemaField - const { widgetProps } = testProps.uiSchema['ui:options']; - expect(testProps.schema).to.equal(schema.items[0].properties.attachmentId); - expect(testProps.registry.formContext.pagePerItemIndex).to.eq(0); - expect(widgetProps['aria-describedby']).to.eq('field_file_name_0'); - expect(widgetProps['data-index']).to.eq(0); - }); - - // Accessibility checks - it('should render a div wrapper when not on the review page', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - items: [ - { - type: 'object', - properties: { - name: { - type: 'string', - }, - }, - }, - ], - }; - const uiSchema = fileUploadUI('Files', { - attachmentSchema: { - 'ui:title': 'Document ID', - }, - attachmentName: { - 'ui:title': 'Document name', - }, - }); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // expect dl wrapper on review page - expect($('div.review', container)).to.exist; - }); - - it('should render a dl wrapper when on the review page', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: { - type: 'object', - properties: { - attachmentId: { - type: 'string', - }, - }, - }, - items: [ - { - type: 'object', - properties: { - name: { - type: 'string', - }, - }, - }, - ], - }; - const uiSchema = fileUploadUI('Files', { - attachmentName: { - 'ui:title': 'Document name', - }, - }); - const formData = [ - { - confirmationCode: 'abcdef', - name: 'Test file name.pdf', - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // expect dl wrapper on review page - expect($('dl.review', container)).to.exist; - }); - - it('should render schema title', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - title: 'schema title', - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI(

uiSchema title

); - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('#upload-button', container).getAttribute('label')).to.contain( - 'schema title', - ); - }); - - it('should render cancel button with secondary class', () => { - const idSchema = { - $id: 'field', - }; - const schema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - }; - const uiSchema = fileUploadUI('Files'); - const formData = [ - { - uploading: true, - }, - ]; - const registry = { - fields: { - SchemaField: () =>
, - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - const cancelButton = $('.cancel-upload', container); - expect(cancelButton.getAttribute('text')).to.equal('Cancel'); - }); - - describe('enableShortWorkflow is true', () => { - const mockIdSchema = { - $id: 'field', - }; - const mockSchema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - maxItems: 4, - }; - const mockUiSchema = fileUploadUI('Files'); - const mockFormDataWithError = [ - { - errorMessage: 'some error message', - }, - ]; - const mockErrorSchemaWithError = { - 0: { - __errors: ['ERROR-123'], - }, - }; - const mockRegistry = { - fields: { - SchemaField: () =>
, - }, - }; - - it('should not render main upload button while file has error', () => { - const idSchema = { - $id: 'myIdSchemaId', - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, - ); - - // id for main upload button is interpolated {idSchema.$id}_add_label - const mainUploadButton = $('#myIdSchemaId_add_label', container); - expect(mainUploadButton).to.not.exist; - }); - - it('should render Upload a new file button for file with error', () => { - const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, - ); - - // This button is specific to the file that has the error - const errorFileUploadButton = $('.retry-upload', container); - expect(errorFileUploadButton.getAttribute('text')).to.equal( - 'Upload a new file', - ); - }); - - it('should render Try again button for file with error', () => { - const errorSchema = { - 0: { - __errors: [FILE_UPLOAD_NETWORK_ERROR_MESSAGE], - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, - ); - - // This button is specific to the file that has the error - const individualFileTryAgainButton = $('.retry-upload', container); - expect(individualFileTryAgainButton.getAttribute('text')).to.equal( - 'Try again', - ); - }); - - it('should render remove file button as cancel', () => { - const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, - ); - - // This button is specific to the file that has the error - const cancelButton = $('.delete-upload', container); - expect(cancelButton.getAttribute('text')).to.equal('Cancel'); - }); - - it('should render delete button for successfully uploaded file', () => { - const formData = [ - { - uploading: false, - }, - ]; - const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, - ); - - // This button is specific to the file that was uploaded - const deleteButton = $('.delete-upload', container); - expect(deleteButton.getAttribute('text')).to.equal('Delete file'); - }); - }); - - describe('enableShortWorkflow is false', () => { - const mockIdSchema = { - $id: 'field', - }; - const mockSchema = { - additionalItems: {}, - items: [ - { - properties: {}, - }, - ], - maxItems: 4, - }; - const mockUiSchema = fileUploadUI('Files'); - const mockFormDataWithError = [ - { - errorMessage: 'some error message', - }, - ]; - const mockErrorSchemaWithError = { - 0: { - __errors: ['ERROR-123'], - }, - }; - const mockRegistry = { - fields: { - SchemaField: () =>
, - }, - }; - - it('should render main upload button while any file has error', () => { - const idSchema = { - $id: 'myIdSchemaId', - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // id for main upload button is interpolated {idSchema.$id}_add_label - const mainUploadButton = $('#myIdSchemaId_add_label', container); - expect(mainUploadButton).to.exist; - expect($('.usa-input-error-message', container).textContent).to.eq( - 'Error ERROR-123', - ); - }); - - it('should not render missing password error', () => { - const idSchema = { - $id: 'myIdSchemaId', - }; - const mockFormDataWithPasswordError = [ - { - errorMessage: MISSING_PASSWORD_ERROR, - }, - ]; - const mockErrorSchemaWithPasswordError = [ - { - __errors: [MISSING_PASSWORD_ERROR], - }, - ]; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - expect($('.usa-input-error-message', container)).to.not.exist; - }); - - it('should render remove file button as Delete file', () => { - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // This button is specific to the file that has the error - const deleteFileButton = $('.delete-upload', container); - expect(deleteFileButton.getAttribute('text')).to.equal('Delete file'); - }); - - it('should render delete button for successfully uploaded file', () => { - const formData = [ - { - uploading: false, - }, - ]; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // This button is specific to the file that was uploaded - const deleteButton = $('.delete-upload', container); - expect(deleteButton.getAttribute('text')).to.equal('Delete file'); - }); - - it('should not render individual file Try again button', () => { - const errorSchema = { - 0: { - __errors: [FILE_UPLOAD_NETWORK_ERROR_MESSAGE], - }, - }; - const { container } = render( - f} - requiredSchema={requiredSchema} - />, - ); - - // The retry button should be the only primary button. Should not be present - // with enableShortWorkflow not enabled - const individualFileTryAgainButton = $('.retry-upload', container); - expect(individualFileTryAgainButton).to.not.exist; - }); - }); -}); diff --git a/src/platform/forms-system/src/js/fields/FileField.jsx b/src/platform/forms-system/src/js/fields/FileField.jsx index f008553a1576..c78ede372512 100644 --- a/src/platform/forms-system/src/js/fields/FileField.jsx +++ b/src/platform/forms-system/src/js/fields/FileField.jsx @@ -46,6 +46,7 @@ import { MISSING_PASSWORD_ERROR } from '../validation'; * @property {string} buttonText='Upload' - upload button text * @property {string} addAnotherLabel='Upload another' - upload another text, * replaces upload button text when greater than one upload is showing + * @property {string} ariaLabelAdditionalText additional screen-reader text to be appended to upload button's aria-label attribute. * @property {string} tryAgain='Try again' - button in enableShortWorkflow * @property {string} newFile='Upload a new file' - button in enableShortWorkflow * @property {string} cancel='Cancel' - button visible while uploading & in enableShortWorkflow @@ -110,6 +111,7 @@ const FileField = props => { const content = { upload: uiOptions.buttonText || 'Upload', uploadAnother: uiOptions.addAnotherLabel || 'Upload another', + ariaLabelAdditionalText: uiOptions.ariaLabelAdditionalText || '', passwordLabel: fileName => `Add a password for ${fileName}`, tryAgain: 'Try again', tryAgainLabel: fileName => `Try uploading ${fileName} again`, @@ -675,7 +677,10 @@ const FileField = props => { secondary class="vads-u-padding-x--0 vads-u-padding-y--1" onClick={() => fileInputRef?.current?.click()} - label={`${uploadText} ${titleString || ''}`} + // label is the aria-label + label={`${uploadText} ${titleString || ''}. ${ + content.ariaLabelAdditionalText + }`} text={uploadText} uswds />