diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 522384725587..fdca1e0260ae 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -14,14 +14,11 @@ concurrency: jobs: build: name: Build - runs-on: self-hosted + runs-on: ubuntu-16-cores-latest outputs: entry_names: ${{ steps.get-changed-apps.outputs.entry_names }} continuous_deployment: ${{ steps.get-changed-apps.outputs.continuous_deployment }} - env: - NODE_EXTRA_CA_CERTS: /etc/ssl/certs/VA-Internal-S2-RCA1-v1.cer.pem - strategy: fail-fast: false matrix: @@ -210,7 +207,7 @@ jobs: name: Unit Tests needs: [fetch-allow-lists, unit-tests-prep] timeout-minutes: 30 - runs-on: ubuntu-16-cores-latest + runs-on: ubuntu-latest outputs: app_folders: ${{ steps.get-changed-apps.outputs.folders }} changed-files: ${{ steps.get-changed-apps.outputs.changed_files }} @@ -222,8 +219,7 @@ jobs: fail-fast: false max-parallel: 72 matrix: - ci_node_index: [mock-form, ezr, post-911-gib-status, appeals, facility-locator, pre-need-integration, ask-a-question, financial-status-report, pre-need, ask-va, auth, avs, gi, hca, rated-disabilities, burial-poc-v6, burials, caregivers, check-in, claims-status, combined-debt-portal, coronavirus-research, coronavirus-screener, debt-letters, dhp-connected-devices, disability-benefits, discharge-wizard, ds-playground, ds-v3-playground, e-folders, edu-benefits, education-letters, enrollment-verification, find-forms, fry-dea, health-care-supply-reordering, income-limits, ivc-champva, letters, lgy, login, medical-copays, messages, mhv-inherited-proofing, mhv-landing-page, mhv, mock-sip-form, my-education-benefits, office-directory, pact-act, pensions, personalization, proxy-rewrite, public-outreach-materials, representative-search, representatives, resources-and-support, sah, search, simple-forms, static-pages, terms-of-use, third-party-app-directory, toe, travel-pay, vaos, verify-your-enrollment, verify, veteran-id-card, virtual-agent, vre, yellow-ribbon] - + ci_node_index: [0,1,2,3,4,5,6,7,8,9] steps: - name: Checkout uses: actions/checkout@v4 @@ -251,14 +247,14 @@ jobs: output-type: 'folder' - name: Run unit tests - run: yarn test:unit --app-folder ${{ matrix.ci_node_index }} ${APP_FOLDERS:+"{script,$APP_FOLDERS}/**/*.unit.spec.js?(x)"} --coverage + run: yarn test:unit ${APP_FOLDERS:+"{script,$APP_FOLDERS}/**/*.unit.spec.js?(x)"} --coverage env: MOCHA_FILE: test-results/unit-tests.xml CHANGED_FILES: ${{ steps.get-changed-apps.outputs.changed_files }} APP_FOLDERS: ${{ steps.get-changed-apps.outputs.folders }} STEP: ${{ matrix.ci_node_index }} IS_STRESS_TEST: false - NUM_CONTAINERS: 72 + NUM_CONTAINERS: 10 - name: Archive unit test results if: ${{ always() }} @@ -334,12 +330,11 @@ jobs: unit-tests-stress-test: name: Unit Test Stability Review - runs-on: self-hosted + runs-on: ubuntu-latest needs: [fetch-allow-lists, unit-tests-prep] if: ${{ always() && needs.unit-tests-prep.outputs.tests-to-stress-test != '[]' && github.ref != 'refs/heads/main' }} env: - NODE_EXTRA_CA_CERTS: /etc/ssl/certs/VA-Internal-S2-RCA1-v1.cer.pem TESTS_TO_VERIFY: ${{ needs.unit-tests-prep.outputs.tests-to-stress-test }} DISALLOWED_TESTS: '[]' diff --git a/config/mocha.json b/config/mocha.json index 342062513872..4fb456de0026 100644 --- a/config/mocha.json +++ b/config/mocha.json @@ -4,7 +4,6 @@ "core-js/stable", "regenerator-runtime/runtime", "blob-polyfill", - "choma", "isomorphic-fetch", "mocha-snapshots", "src/platform/testing/unit/mocha-setup.js", diff --git a/package.json b/package.json index b336ca84bdd1..48de1b96dc70 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "chai-dom": "^1.9.0", "chalk": "^4.1.2", "chokidar": "^3.5.2", - "choma": "^1.1.0", "clear": "^0.1.0", "cli-spinner": "^0.2.10", "cli-table": "^0.3.6", @@ -318,7 +317,7 @@ "url-search-params-polyfill": "^8.1.1", "uswds": "1.6.10", "vanilla-lazyload": "^16.1.0", - "vets-json-schema": "https://github.com/department-of-veterans-affairs/vets-json-schema.git#39dd490a3031bbb28ec30acdb1efa131122a77c8" + "vets-json-schema": "https://github.com/department-of-veterans-affairs/vets-json-schema.git#9e97ed438ab16e131513e7df72712dc91db4e6a3" }, "resolutions": { "**/lodash": "4.17.21", diff --git a/script/run-unit-test.js b/script/run-unit-test.js index 32c46f8f02ca..6c55a6a4c048 100644 --- a/script/run-unit-test.js +++ b/script/run-unit-test.js @@ -1,14 +1,15 @@ /* eslint-disable no-console */ const commandLineArgs = require('command-line-args'); const glob = require('glob'); +const path = require('path'); const printUnitTestHelp = require('./run-unit-test-help'); const { runCommand } = require('./utils'); // For usage instructions see https://github.com/department-of-veterans-affairs/vets-website#unit-tests const specDirs = '{src,script}'; const defaultPath = `./${specDirs}/**/*.unit.spec.js?(x)`; -// const numContainers = process.env.NUM_CONTAINERS || 10; -// const matrixStep = process.env.STEP || 1; +const numContainers = process.env.NUM_CONTAINERS || 1; +const matrixStep = process.env.STEP || 0; const COMMAND_LINE_OPTIONS_DEFINITIONS = [ { name: 'log-level', type: String, defaultValue: 'log' }, @@ -26,16 +27,28 @@ const COMMAND_LINE_OPTIONS_DEFINITIONS = [ defaultValue: [defaultPath], }, ]; +const allUnitTests = glob.sync(defaultPath); +const allUnitTestDirs = Array.from( + new Set( + allUnitTests.map(spec => + JSON.stringify( + path + .dirname(spec) + .split('/') + .slice(1, 4), + ), + ), + ), +).filter(spec => spec !== undefined); -// const allUnitTests = glob.sync(defaultPath); -// function splitArray(array, chunks) { -// const [...arrayCopy] = array; -// const arrayChunks = []; -// while (arrayCopy.length) { -// arrayChunks.push(arrayCopy.splice(0, chunks)); -// } -// return arrayChunks; -// } +function splitArray(array, chunks) { + const [...arrayCopy] = array; + const arrayChunks = []; + while (arrayCopy.length) { + arrayChunks.push(arrayCopy.splice(0, chunks)); + } + return arrayChunks; +} const options = commandLineArgs(COMMAND_LINE_OPTIONS_DEFINITIONS); let coverageInclude = ''; @@ -77,23 +90,48 @@ if (process.env.TESTS_TO_VERIFY) { testsToVerify = JSON.parse(process.env.TESTS_TO_VERIFY).join(' '); } -// const splitUnitTests = splitArray( -// allUnitTests, -// Math.ceil(allUnitTests.length / numContainers), -// ); -console.log('app folder selected: ', options['app-folder']); -// const testsToRun = options['app-folder'] -// ? `--recursive ${options.path.map(p => `'${p}'`).join(' ')}` -// : splitUnitTests[matrixStep].join(' '); +const splitUnitTests = splitArray( + allUnitTestDirs, + Math.ceil(allUnitTestDirs.length / numContainers), +); +const appsToRun = options['app-folder'] + ? [options['app-folder']] + : splitUnitTests[matrixStep]; +if (testsToVerify === null) { + for (const dir of appsToRun) { + const updatedPath = options['app-folder'] + ? options.path.map(p => `'${p}'`).join(' ') + : options.path[0].replace( + `/${specDirs}/`, + `/${JSON.parse(dir).join('/')}/`, + ); + const testsToRun = options['app-folder'] + ? `--recursive ${updatedPath}` + : `--recursive ${glob.sync(updatedPath)}`; + const command = `LOG_LEVEL=${options[ + 'log-level' + ].toLowerCase()} ${testRunner} --max-old-space-size=8192 --config ${configFile} ${testsToRun.replace( + /,/g, + ' ', + )} `; + if (testsToRun !== '') { + runCommand(command); + } else { + console.log('This app has no tests to run'); + } + } +} else { + const command = `LOG_LEVEL=${options[ + 'log-level' + ].toLowerCase()} ${testRunner} --max-old-space-size=8192 --config ${configFile} ${testsToVerify}`; + runCommand(command); +} + // const command = `LOG_LEVEL=${options[ // 'log-level' // ].toLowerCase()} ${testRunner} --max-old-space-size=8192 --config ${configFile} ${testsToVerify || // testsToRun} `; -const command = `LOG_LEVEL=${options[ - 'log-level' -].toLowerCase()} ${testRunner} --max-old-space-size=8192 --config ${configFile} ${testsToVerify || - `--recursive ${options.path.map(p => `'${p}'`).join(' ')}`} `; - -console.log(command); - -runCommand(command); +// const command = `LOG_LEVEL=${options[ +// 'log-level' +// ].toLowerCase()} ${testRunner} --max-old-space-size=8192 --config ${configFile} ${testsToVerify || +// `--recursive ${options.path.map(p => `'${p}'`).join(' ')}`} `; diff --git a/src/applications/appeals/shared/utils/useBrowserMonitoring.js b/src/applications/appeals/shared/utils/useBrowserMonitoring.js index 30766da7fbbc..70926e98cde4 100644 --- a/src/applications/appeals/shared/utils/useBrowserMonitoring.js +++ b/src/applications/appeals/shared/utils/useBrowserMonitoring.js @@ -61,7 +61,7 @@ const defaultLogSettings = { forwardErrorsToLogs: true, forwardConsoleLogs: ['error'], forwardReports: [], - telemetrySampleRate: 20, // default + telemetrySampleRate: 100, // default 20 }; const initializeBrowserLogging = customLogSettings => { diff --git a/src/applications/avs/components/YourTreatmentPlan.jsx b/src/applications/avs/components/YourTreatmentPlan.jsx index 82d359f0342d..85dd6cad7067 100644 --- a/src/applications/avs/components/YourTreatmentPlan.jsx +++ b/src/applications/avs/components/YourTreatmentPlan.jsx @@ -13,9 +13,10 @@ const YourTreatmentPlan = props => { const { avs } = props; const { medChangesSummary, orders } = avs; - const medChanges = !allArraysEmpty(medChangesSummary) - ? medChangesSummary - : null; + const medChanges = + medChangesSummary && !allArraysEmpty(medChangesSummary) + ? medChangesSummary + : null; const medsIntro = ( <> diff --git a/src/applications/avs/containers/Avs.jsx b/src/applications/avs/containers/Avs.jsx index e7f7bb306100..636ae5a786f3 100644 --- a/src/applications/avs/containers/Avs.jsx +++ b/src/applications/avs/containers/Avs.jsx @@ -11,6 +11,8 @@ import { RequiredLoginView } from '@department-of-veterans-affairs/platform-user import { getAvs } from '../api/v0'; import { getFormattedAppointmentDate } from '../utils'; +import { useDatadogRum } from '../hooks/useDatadogRum'; + import BreadCrumb from '../components/BreadCrumb'; import MoreInformation from '../components/MoreInformation'; import TextWithLineBreaks from '../components/TextWithLineBreaks'; @@ -24,6 +26,8 @@ const generateAppointmentHeader = avs => { }; const Avs = props => { + useDatadogRum(); + const user = useSelector(selectUser); const { avsEnabled, featureTogglesLoading } = useSelector( state => { diff --git a/src/applications/avs/hooks/useDatadogRum.jsx b/src/applications/avs/hooks/useDatadogRum.jsx new file mode 100644 index 000000000000..16003666079d --- /dev/null +++ b/src/applications/avs/hooks/useDatadogRum.jsx @@ -0,0 +1,54 @@ +import { useEffect } from 'react'; +import { datadogRum } from '@datadog/browser-rum'; + +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; + +const datadogRumConfig = { + applicationId: '8880279e-5c40-4f82-90f9-9a3cdb6d461b', + clientToken: 'pubcf8129b0768db883d760a1fd6abdc8a0', + site: 'ddog-gov.com', + service: 'avs', + env: environment.vspEnvironment(), + sampleRate: 100, + sessionReplaySampleRate: 100, + trackInteractions: true, + trackFrustrations: true, + trackResources: true, + trackLongTasks: true, + defaultPrivacyLevel: 'mask', + beforeSend: event => { + // Prevent PII from being sent to Datadog with click actions. + if (event.action?.type === 'click') { + // eslint-disable-next-line no-param-reassign + event.action.target.name = 'AVS item'; + } + return true; + }, +}; + +const initializeDatadogRum = () => { + if ( + // Prevent RUM from running on local/CI environments. + environment.BASE_URL.indexOf('localhost') < 0 && + // Prevent re-initializing the SDK. + !window.DD_RUM?.getInitConfiguration() && + !window.Mocha + ) { + if (!datadogRumConfig.env) { + datadogRumConfig.env = environment.vspEnvironment(); + } + datadogRum.init(datadogRumConfig); + datadogRum.startSessionReplayRecording(); + } +}; + +const useDatadogRum = config => { + useEffect( + () => { + initializeDatadogRum(); + }, + [config], + ); +}; + +export { useDatadogRum }; diff --git a/src/applications/avs/tests/components/YourTreatmentPlan.unit.spec.jsx b/src/applications/avs/tests/components/YourTreatmentPlan.unit.spec.jsx index c68171581d75..3cfa07cf0816 100644 --- a/src/applications/avs/tests/components/YourTreatmentPlan.unit.spec.jsx +++ b/src/applications/avs/tests/components/YourTreatmentPlan.unit.spec.jsx @@ -56,9 +56,7 @@ describe('Avs: Your Treatment Plan', () => { delete avs.orders; delete avs.patientInstructions; delete avs.clinicalReminders; - avs.medChangesSummary.discontinuedMeds = []; - avs.medChangesSummary.newMedications = []; - avs.medChangesSummary.changedMedications = []; + avs.medChangesSummary = null; const props = { avs }; const screen = render(); expect(screen.queryByTestId('new-orders-heading')).to.not.exist; @@ -73,4 +71,14 @@ describe('Avs: Your Treatment Plan', () => { expect(screen.queryByTestId('discontinued-medications-list')).to.not.exist; expect(screen.queryByTestId('changed-medications-list')).to.not.exist; }); + + it('Med Changes section is not shown if all sub-sections are empty', async () => { + const avs = replacementFunctions.cloneDeep(avsData); + avs.medChangesSummary.discontinuedMeds = []; + avs.medChangesSummary.newMedications = []; + avs.medChangesSummary.changedMedications = []; + const props = { avs }; + const screen = render(); + expect(screen.queryByTestId('changed-medications-list')).to.not.exist; + }); }); diff --git a/src/applications/caregivers/components/ApplicationDownloadLink.jsx b/src/applications/caregivers/components/ApplicationDownloadLink.jsx index 5508b451ed0e..88b6f66165d5 100644 --- a/src/applications/caregivers/components/ApplicationDownloadLink.jsx +++ b/src/applications/caregivers/components/ApplicationDownloadLink.jsx @@ -94,7 +94,7 @@ const ApplicationDownloadLink = ({ form }) => { if (errors?.length > 0) { return (
- +

Something went wrong

diff --git a/src/applications/caregivers/components/ConfirmationPage/ConfirmationScreenView.jsx b/src/applications/caregivers/components/ConfirmationPage/ConfirmationScreenView.jsx index 2e731abcf8e9..840460291006 100644 --- a/src/applications/caregivers/components/ConfirmationPage/ConfirmationScreenView.jsx +++ b/src/applications/caregivers/components/ConfirmationPage/ConfirmationScreenView.jsx @@ -15,7 +15,7 @@ const ConfirmationScreenView = ({ form, name, timestamp }) => { return ( <>
- +

Thank you for completing your application

@@ -26,52 +26,46 @@ const ConfirmationScreenView = ({ form, name, timestamp }) => {
- -

- Your application information -

-
-
-
- Veteran’s name -
-
- {name.first} {name.middle} {name.last} {name.suffix} -
-
- {!!timestamp && ( -
-
- Date you applied -
-
- {moment(timestamp).format('MMM D, YYYY')} -
-
- )} -
-
- Confirmation for your records -
-
- You can print this confirmation page for your records. You can - also download your completed application as a{' '} - - PDF - - . -
-
-
+ +

Your application information

+ +

Veteran’s name

+

+ {name.first} {name.middle} {name.last} {name.suffix} +

+ + {timestamp ? ( + <> +

Date you applied

+

+ {moment(timestamp).format('MMM D, YYYY')} +

+ + ) : null} + +

Confirmation for your records

+

+ You can print this confirmation page for your records. You can also + download your completed application as a{' '} + + PDF + + . +

- window.print()} /> + window.print()} + data-testid="cg-print-button" + uswds + />
-
+ ); }; diff --git a/src/applications/caregivers/components/FormAlerts/CheckUploadWarning.jsx b/src/applications/caregivers/components/FormAlerts/CheckUploadWarning.jsx index 50c70d104dfc..2d95321a211b 100644 --- a/src/applications/caregivers/components/FormAlerts/CheckUploadWarning.jsx +++ b/src/applications/caregivers/components/FormAlerts/CheckUploadWarning.jsx @@ -8,7 +8,7 @@ const CheckUploadWarning = () => { return (
- +

Check your upload before you continue

It’s easy to upload the wrong file by mistake. We want to make sure diff --git a/src/applications/caregivers/components/FormAlerts/GeneralErrorAlert.jsx b/src/applications/caregivers/components/FormAlerts/GeneralErrorAlert.jsx new file mode 100644 index 000000000000..d649b5aaf90a --- /dev/null +++ b/src/applications/caregivers/components/FormAlerts/GeneralErrorAlert.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const GeneralErrorAlert = () => ( +

+ +

Something went wrong

+

+ We’re sorry. Something went wrong on our end. Please try again later. +

+
+
+); + +export default GeneralErrorAlert; diff --git a/src/applications/caregivers/components/FormAlerts/SecondaryRequiredAlert.jsx b/src/applications/caregivers/components/FormAlerts/SecondaryRequiredAlert.jsx new file mode 100644 index 000000000000..a2a5cc2d9cdb --- /dev/null +++ b/src/applications/caregivers/components/FormAlerts/SecondaryRequiredAlert.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const SecondaryRequiredAlert = () => ( + +

We need you to add a Family Caregiver

+

+ We can’t process your application unless you add a Family Caregiver. + Please go back and add either a Primary or Secondary Family Caregiver to + your application. +

+
+); + +export default SecondaryRequiredAlert; diff --git a/src/applications/caregivers/components/FormAlerts/SubmissionErrorAlert.jsx b/src/applications/caregivers/components/FormAlerts/SubmissionErrorAlert.jsx index 2166b2267557..5085f970649e 100644 --- a/src/applications/caregivers/components/FormAlerts/SubmissionErrorAlert.jsx +++ b/src/applications/caregivers/components/FormAlerts/SubmissionErrorAlert.jsx @@ -12,7 +12,7 @@ const SubmissionErrorAlert = ({ form }) => { return (
- +

We didn’t receive your online application

We’re sorry. Something went wrong when you tried to submit your diff --git a/src/applications/caregivers/components/FormAlerts/index.jsx b/src/applications/caregivers/components/FormAlerts/index.jsx deleted file mode 100644 index 62648efe7113..000000000000 --- a/src/applications/caregivers/components/FormAlerts/index.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -export const GeneralErrorAlert = () => ( -

- -

Something went wrong

-

- We’re sorry. Something went wrong on our end. Please try again later. -

-
-
-); - -export const SecondaryRequiredAlert = () => ( - -

We need you to add a Family Caregiver

-

- We can’t process your application unless you add a Family Caregiver. - Please go back and add either a Primary or Secondary Family Caregiver to - your application. -

-
-); diff --git a/src/applications/caregivers/components/FormDescriptions/PrimaryCaregiverDescription.jsx b/src/applications/caregivers/components/FormDescriptions/PrimaryCaregiverDescription.jsx index 411833807556..88d3fa9d7cdd 100644 --- a/src/applications/caregivers/components/FormDescriptions/PrimaryCaregiverDescription.jsx +++ b/src/applications/caregivers/components/FormDescriptions/PrimaryCaregiverDescription.jsx @@ -48,6 +48,7 @@ const PrimaryCaregiverDescription = ({

Family caregivers are approved and designated by VA as Primary diff --git a/src/applications/caregivers/components/FormDescriptions/SecondaryCaregiverDescription.jsx b/src/applications/caregivers/components/FormDescriptions/SecondaryCaregiverDescription.jsx index 8842067e90fc..f44959d1a236 100644 --- a/src/applications/caregivers/components/FormDescriptions/SecondaryCaregiverDescription.jsx +++ b/src/applications/caregivers/components/FormDescriptions/SecondaryCaregiverDescription.jsx @@ -44,6 +44,7 @@ const SecondaryCaregiverDescription = ({

Family caregivers are approved and designated by VA as Primary diff --git a/src/applications/caregivers/components/FormDescriptions/index.jsx b/src/applications/caregivers/components/FormDescriptions/index.jsx index d10e0d6a8c25..f0049dc41e2e 100644 --- a/src/applications/caregivers/components/FormDescriptions/index.jsx +++ b/src/applications/caregivers/components/FormDescriptions/index.jsx @@ -42,6 +42,7 @@ export const HeathCareCoverageDescription = (

This information helps us determine if you may be eligible for health care @@ -126,7 +127,10 @@ export const RepresentativeDescription = ( that proves you have this authority.

- + We can only accept a document that proves you have legal authority to make decisions for the Veteran (such as a valid Power of Attorney, legal guardianship order, or other legal document). We can’t accept a marriage @@ -182,6 +186,7 @@ export const VeteranSSNDescription = ( We need the Veteran’s Social Security number or tax identification number to process the application when it’s submitted online, but it’s not a diff --git a/src/applications/caregivers/components/FormFields/AddressWithAutofill.jsx b/src/applications/caregivers/components/FormFields/AddressWithAutofill.jsx index eb8b85326fd3..4f0195228290 100644 --- a/src/applications/caregivers/components/FormFields/AddressWithAutofill.jsx +++ b/src/applications/caregivers/components/FormFields/AddressWithAutofill.jsx @@ -132,6 +132,7 @@ const PrimaryAddressWithAutofill = props => { checked={formData['view:autofill']} label="Use the same address as the Veteran" onVaChange={handleCheck} + uswds /> { hint="This is the address where the Caregiver lives" className="cg-address-input" error={showError('street') || null} - required onInput={handleChange} onBlur={handleBlur} + required + uswds /> { className="cg-address-input" onInput={handleChange} onBlur={handleBlur} + uswds /> { label="City" className="cg-address-input" error={showError('city') || null} - required onInput={handleChange} onBlur={handleBlur} + required + uswds /> { label="State" className="cg-address-select" error={showError('state') || null} - required onVaSelect={handleChange} onBlur={handleBlur} + required + uswds > - {constants.states.USA.map(state => (
@@ -88,9 +93,9 @@ const FacilitySearch = props => { FacilitySearch.propTypes = { formContext: PropTypes.object, - onChange: PropTypes.func, plannedClinic: PropTypes.string, value: PropTypes.string, + onChange: PropTypes.func, }; export default FacilitySearch; diff --git a/src/applications/caregivers/components/FormFields/VaMedicalCenter.jsx b/src/applications/caregivers/components/FormFields/VaMedicalCenter.jsx index da621e832360..e7d7990d5d8d 100644 --- a/src/applications/caregivers/components/FormFields/VaMedicalCenter.jsx +++ b/src/applications/caregivers/components/FormFields/VaMedicalCenter.jsx @@ -7,7 +7,7 @@ import { VaSelect } from '@department-of-veterans-affairs/component-library/dist import environment from 'platform/utilities/environment'; import { apiRequest } from 'platform/utilities/api'; import { focusElement } from 'platform/utilities/ui'; -import { GeneralErrorAlert } from '../FormAlerts'; +import GeneralErrorAlert from '../FormAlerts/GeneralErrorAlert'; const apiRequestWithUrl = `${ environment.API_URL @@ -86,9 +86,7 @@ const VaMedicalCenter = props => { // render the static facility name on review page if (reviewMode) { - return ( - {getFacilityName(value)} - ); + return {getFacilityName(value)}; } // render loading indicator while we fetch @@ -109,11 +107,11 @@ const VaMedicalCenter = props => { value={value} label="VA medical center" error={showError() || null} - required onVaSelect={handleChange} onBlur={handleBlur} + required + uswds > - {facilities.map(f => (
-
+ ); }; diff --git a/src/applications/ezr/components/FormDescriptions/DependentDescription.jsx b/src/applications/ezr/components/FormDescriptions/DependentDescription.jsx index b00f20ae35aa..e76ac475acee 100644 --- a/src/applications/ezr/components/FormDescriptions/DependentDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/DependentDescription.jsx @@ -5,6 +5,7 @@ const DependentDescription = () => (

diff --git a/src/applications/ezr/components/FormDescriptions/DependentSupportDescription.jsx b/src/applications/ezr/components/FormDescriptions/DependentSupportDescription.jsx index bd8a35fc6562..a0c1a8615ee2 100644 --- a/src/applications/ezr/components/FormDescriptions/DependentSupportDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/DependentSupportDescription.jsx @@ -4,6 +4,7 @@ const DependentSupportDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/ExpensesDescriptions.jsx b/src/applications/ezr/components/FormDescriptions/ExpensesDescriptions.jsx index 68b75f148541..e25e24d1db40 100644 --- a/src/applications/ezr/components/FormDescriptions/ExpensesDescriptions.jsx +++ b/src/applications/ezr/components/FormDescriptions/ExpensesDescriptions.jsx @@ -4,6 +4,7 @@ export const EducationalExpensesDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/HealthInsuranceDescriptions.jsx b/src/applications/ezr/components/FormDescriptions/HealthInsuranceDescriptions.jsx index c2f6ac92add9..f14741536d40 100644 --- a/src/applications/ezr/components/FormDescriptions/HealthInsuranceDescriptions.jsx +++ b/src/applications/ezr/components/FormDescriptions/HealthInsuranceDescriptions.jsx @@ -18,6 +18,7 @@ export const HealthInsuranceAddtlInfoDescription = () => (

diff --git a/src/applications/ezr/components/FormDescriptions/IncomeDescriptions.jsx b/src/applications/ezr/components/FormDescriptions/IncomeDescriptions.jsx index e8e5cced80c9..5c5fc47ed664 100644 --- a/src/applications/ezr/components/FormDescriptions/IncomeDescriptions.jsx +++ b/src/applications/ezr/components/FormDescriptions/IncomeDescriptions.jsx @@ -4,6 +4,7 @@ export const GrossIncomeDescription = (

@@ -23,6 +24,7 @@ export const OtherIncomeDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/InsurancePolicyDescriptions.jsx b/src/applications/ezr/components/FormDescriptions/InsurancePolicyDescriptions.jsx index e4d087920714..c7ec3d0dc4e0 100644 --- a/src/applications/ezr/components/FormDescriptions/InsurancePolicyDescriptions.jsx +++ b/src/applications/ezr/components/FormDescriptions/InsurancePolicyDescriptions.jsx @@ -14,7 +14,7 @@ export const PolicyOrGroupDescription = ( ); export const TricarePolicyDescription = ( - +

You can use your Department of Defense benefits number (DBN) or your diff --git a/src/applications/ezr/components/FormDescriptions/MaritalStatusDescription.jsx b/src/applications/ezr/components/FormDescriptions/MaritalStatusDescription.jsx index 761732172517..1e03919ebd2f 100644 --- a/src/applications/ezr/components/FormDescriptions/MaritalStatusDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/MaritalStatusDescription.jsx @@ -4,6 +4,7 @@ const MaritalStatusDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/MedicareClaimNumberDescription.jsx b/src/applications/ezr/components/FormDescriptions/MedicareClaimNumberDescription.jsx index 3faf34f27860..c09c44a27343 100644 --- a/src/applications/ezr/components/FormDescriptions/MedicareClaimNumberDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/MedicareClaimNumberDescription.jsx @@ -5,6 +5,7 @@ const MedicareClaimNumberDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/SigiDescription.jsx b/src/applications/ezr/components/FormDescriptions/SigiDescription.jsx index c3e1a3b73881..a01e04970c47 100644 --- a/src/applications/ezr/components/FormDescriptions/SigiDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/SigiDescription.jsx @@ -4,6 +4,7 @@ const SigiDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/SpouseFinancialSupportDescription.jsx b/src/applications/ezr/components/FormDescriptions/SpouseFinancialSupportDescription.jsx index 8e92c555bd18..a8fa71a9fcdf 100644 --- a/src/applications/ezr/components/FormDescriptions/SpouseFinancialSupportDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/SpouseFinancialSupportDescription.jsx @@ -4,6 +4,7 @@ const SpouseFinancialSupportDescription = (

diff --git a/src/applications/ezr/components/FormDescriptions/SpouseInfoDescription.jsx b/src/applications/ezr/components/FormDescriptions/SpouseInfoDescription.jsx index 59e3082125d8..e2f9d153df4c 100644 --- a/src/applications/ezr/components/FormDescriptions/SpouseInfoDescription.jsx +++ b/src/applications/ezr/components/FormDescriptions/SpouseInfoDescription.jsx @@ -5,6 +5,7 @@ const SpouseInfoDescription = (

diff --git a/src/applications/ezr/components/FormPages/DependentInformation.jsx b/src/applications/ezr/components/FormPages/DependentInformation.jsx index 3b69c2ffc17d..7386bc290e03 100644 --- a/src/applications/ezr/components/FormPages/DependentInformation.jsx +++ b/src/applications/ezr/components/FormPages/DependentInformation.jsx @@ -150,13 +150,14 @@ const DependentInformation = props => { {/** Cancel confirmation modal trigger */}

diff --git a/src/applications/ezr/components/FormPages/DependentSummary.jsx b/src/applications/ezr/components/FormPages/DependentSummary.jsx index 75fbaf165f9e..960b3c0d3a06 100644 --- a/src/applications/ezr/components/FormPages/DependentSummary.jsx +++ b/src/applications/ezr/components/FormPages/DependentSummary.jsx @@ -134,6 +134,7 @@ const DependentSummary = props => { text={content['button-update-page']} label={content['household-dependent-update-button-aria-label']} data-testid="ezr-update-button" + uswds /> )} diff --git a/src/applications/ezr/components/FormPages/InsurancePolicyInformation.jsx b/src/applications/ezr/components/FormPages/InsurancePolicyInformation.jsx index c821dad00ffe..774a0f00a498 100644 --- a/src/applications/ezr/components/FormPages/InsurancePolicyInformation.jsx +++ b/src/applications/ezr/components/FormPages/InsurancePolicyInformation.jsx @@ -111,13 +111,14 @@ const InsurancePolicyInformation = props => { {/** Cancel confirmation modal trigger */}
diff --git a/src/applications/ezr/components/FormPages/InsuranceSummary.jsx b/src/applications/ezr/components/FormPages/InsuranceSummary.jsx index 72fc032ad6d6..33b0a5eeb354 100644 --- a/src/applications/ezr/components/FormPages/InsuranceSummary.jsx +++ b/src/applications/ezr/components/FormPages/InsuranceSummary.jsx @@ -145,6 +145,7 @@ const InsuranceSummary = props => { text={content['button-update-page']} label={content['insurance-update-button-aria-label']} data-testid="ezr-update-button" + uswds /> )} diff --git a/src/applications/ezr/components/FormReview/DependentsReviewPage.jsx b/src/applications/ezr/components/FormReview/DependentsReviewPage.jsx index 869d8b0bd25d..7e6e0ee548b8 100644 --- a/src/applications/ezr/components/FormReview/DependentsReviewPage.jsx +++ b/src/applications/ezr/components/FormReview/DependentsReviewPage.jsx @@ -27,16 +27,13 @@ const DependentsReviewPage = ({ data, editPage }) => {

{content['household-dependent-review-header-title']}

- + secondary + uswds + />
{reviewRows}
diff --git a/src/applications/ezr/components/FormReview/InsurancePolicyReviewPage.jsx b/src/applications/ezr/components/FormReview/InsurancePolicyReviewPage.jsx index 3a54080fba38..a41abd0c1668 100644 --- a/src/applications/ezr/components/FormReview/InsurancePolicyReviewPage.jsx +++ b/src/applications/ezr/components/FormReview/InsurancePolicyReviewPage.jsx @@ -25,14 +25,13 @@ const InsurancePolicyReviewPage = ({ data, editPage }) => {

{content['insurance-review-header-title']}

- + secondary + uswds + />
{reviewRows}
diff --git a/src/applications/ezr/components/PreSubmitNotice/NoticeAgreement.jsx b/src/applications/ezr/components/PreSubmitNotice/NoticeAgreement.jsx index 34aaa4f1e9be..f83dc9c30f0f 100644 --- a/src/applications/ezr/components/PreSubmitNotice/NoticeAgreement.jsx +++ b/src/applications/ezr/components/PreSubmitNotice/NoticeAgreement.jsx @@ -38,6 +38,7 @@ const NoticeAgreement = () => (

I understand that pursuant to 38 U.S.C. Section 1729 and 42 U.S.C. 2651, diff --git a/src/applications/ezr/components/PreSubmitNotice/index.jsx b/src/applications/ezr/components/PreSubmitNotice/index.jsx index e153a97128c1..c9dc27f0d81d 100644 --- a/src/applications/ezr/components/PreSubmitNotice/index.jsx +++ b/src/applications/ezr/components/PreSubmitNotice/index.jsx @@ -45,6 +45,7 @@ const PreSubmitNotice = props => { error={error} onVaChange={event => setAccepted(event.target.checked)} label={content['presubmit-checkbox-label']} + uswds /> ); diff --git a/src/applications/ezr/config/chapters/insuranceInformation/medicare.js b/src/applications/ezr/config/chapters/insuranceInformation/medicare.js index 7acb79bcaa5c..0c73bd3b2792 100644 --- a/src/applications/ezr/config/chapters/insuranceInformation/medicare.js +++ b/src/applications/ezr/config/chapters/insuranceInformation/medicare.js @@ -1,15 +1,12 @@ -import PrefillMessage from 'platform/forms/save-in-progress/PrefillMessage'; import { yesNoUI, yesNoSchema, - descriptionUI, } from 'platform/forms-system/src/js/web-component-patterns'; import MedicarePartADescription from '../../../components/FormDescriptions/MedicarePartADescription'; import content from '../../../locales/en/content.json'; export default { uiSchema: { - ...descriptionUI(PrefillMessage, { hideOnReview: true }), 'view:isEnrolledMedicarePartA': { 'ui:title': MedicarePartADescription, isEnrolledMedicarePartA: yesNoUI(content['insurance-medicare-title']), diff --git a/src/applications/ezr/config/chapters/insuranceInformation/partAEffectiveDate.js b/src/applications/ezr/config/chapters/insuranceInformation/partAEffectiveDate.js index 3dd38ef68136..9c9d22de0ea6 100644 --- a/src/applications/ezr/config/chapters/insuranceInformation/partAEffectiveDate.js +++ b/src/applications/ezr/config/chapters/insuranceInformation/partAEffectiveDate.js @@ -1,5 +1,4 @@ import ezrSchema from 'vets-json-schema/dist/10-10EZR-schema.json'; -import PrefillMessage from 'platform/forms/save-in-progress/PrefillMessage'; import { descriptionUI, currentOrPastDateUI, @@ -14,7 +13,6 @@ const { medicareClaimNumber } = ezrSchema.properties; export default { uiSchema: { - ...descriptionUI(PrefillMessage, { hideOnReview: true }), medicarePartAEffectiveDate: currentOrPastDateUI({ title: content['insurance-medicare-part-a-title'], hint: content['insuance-medicare-part-a-hint'], diff --git a/src/applications/ezr/config/chapters/veteranInformation/contactInformation.js b/src/applications/ezr/config/chapters/veteranInformation/contactInformation.js index d9f93be70d0c..2b277875da06 100644 --- a/src/applications/ezr/config/chapters/veteranInformation/contactInformation.js +++ b/src/applications/ezr/config/chapters/veteranInformation/contactInformation.js @@ -1,9 +1,7 @@ import ezrSchema from 'vets-json-schema/dist/10-10EZR-schema.json'; -import PrefillMessage from 'platform/forms/save-in-progress/PrefillMessage'; import { emailUI, phoneUI, - descriptionUI, titleUI, } from 'platform/forms-system/src/js/web-component-patterns'; import ContactInfoDescription from '../../../components/FormDescriptions/ContactInfoDescription'; @@ -13,7 +11,6 @@ const { email, homePhone, mobilePhone } = ezrSchema.properties; export default { uiSchema: { - ...descriptionUI(PrefillMessage, { hideOnReview: true }), 'view:contactInformation': { ...titleUI(content['vet-contact-info-title'], ContactInfoDescription), homePhone: { diff --git a/src/applications/ezr/config/chapters/veteranInformation/genderIdentity.js b/src/applications/ezr/config/chapters/veteranInformation/genderIdentity.js index 6d9bbdb83283..991e1f84368a 100644 --- a/src/applications/ezr/config/chapters/veteranInformation/genderIdentity.js +++ b/src/applications/ezr/config/chapters/veteranInformation/genderIdentity.js @@ -1,5 +1,4 @@ import ezrSchema from 'vets-json-schema/dist/10-10EZR-schema.json'; -import PrefillMessage from 'platform/forms/save-in-progress/PrefillMessage'; import { radioUI, titleUI, @@ -13,7 +12,6 @@ const { sigiGenders } = ezrSchema.properties; export default { uiSchema: { - ...descriptionUI(PrefillMessage, { hideOnReview: true }), 'view:sigiGenders': { ...titleUI(content['vet-gender-identity-title']), ...descriptionUI(SigiDescription, { hideOnReview: true }), diff --git a/src/applications/ezr/config/chapters/veteranInformation/homeAddress.js b/src/applications/ezr/config/chapters/veteranInformation/homeAddress.js index 37dd024c14bc..6059ddcf80e3 100644 --- a/src/applications/ezr/config/chapters/veteranInformation/homeAddress.js +++ b/src/applications/ezr/config/chapters/veteranInformation/homeAddress.js @@ -1,10 +1,8 @@ import merge from 'lodash/merge'; import ezrSchema from 'vets-json-schema/dist/10-10EZR-schema.json'; -import PrefillMessage from 'platform/forms/save-in-progress/PrefillMessage'; import { addressUI, addressSchema, - descriptionUI, titleUI, titleSchema, } from 'platform/forms-system/src/js/web-component-patterns'; @@ -16,7 +14,6 @@ const { export default { uiSchema: { - ...descriptionUI(PrefillMessage, { hideOnReview: true }), 'view:pageTitle': titleUI(content['vet-home-address-title']), veteranHomeAddress: addressUI(), }, diff --git a/src/applications/ezr/tests/e2e/ezr-dependents.cypress.spec.js b/src/applications/ezr/tests/e2e/ezr-dependents.cypress.spec.js index 2bafdd603a17..b76de544930f 100644 --- a/src/applications/ezr/tests/e2e/ezr-dependents.cypress.spec.js +++ b/src/applications/ezr/tests/e2e/ezr-dependents.cypress.spec.js @@ -38,11 +38,11 @@ function submitDependentInformation(dependent, showIncomePages) { advanceFromDependentsToReview(testData); // accept the privacy agreement - cy.get('[name="privacyAgreementAccepted"]') + cy.get('va-checkbox[name="privacyAgreementAccepted"]') .scrollIntoView() .shadow() - .find('[type="checkbox"]') - .check(); + .find('label') + .click(); // submit form cy.findByText(/submit/i, { selector: 'button' }).click(); diff --git a/src/applications/ezr/tests/e2e/ezr-insurance.cypress.spec.js b/src/applications/ezr/tests/e2e/ezr-insurance.cypress.spec.js index 2006917a4417..fea42f44e95d 100644 --- a/src/applications/ezr/tests/e2e/ezr-insurance.cypress.spec.js +++ b/src/applications/ezr/tests/e2e/ezr-insurance.cypress.spec.js @@ -59,11 +59,11 @@ describe('EZR Insurance Policies', () => { goToNextPage('review-and-submit'); // accept the privacy agreement - cy.get('[name="privacyAgreementAccepted"]') + cy.get('va-checkbox[name="privacyAgreementAccepted"]') .scrollIntoView() .shadow() - .find('[type="checkbox"]') - .check(); + .find('label') + .click(); // submit form cy.findByText(/submit/i, { selector: 'button' }).click(); diff --git a/src/applications/ezr/tests/e2e/ezr-noFinancials.cypress.spec.js b/src/applications/ezr/tests/e2e/ezr-noFinancials.cypress.spec.js index 802b8c5011db..d67777f70476 100644 --- a/src/applications/ezr/tests/e2e/ezr-noFinancials.cypress.spec.js +++ b/src/applications/ezr/tests/e2e/ezr-noFinancials.cypress.spec.js @@ -48,11 +48,11 @@ describe('EZR No Financial Submission', () => { advanceFromHouseholdToReview(); // accept the privacy agreement - cy.get('[name="privacyAgreementAccepted"]') + cy.get('va-checkbox[name="privacyAgreementAccepted"]') .scrollIntoView() .shadow() - .find('[type="checkbox"]') - .check(); + .find('label') + .click(); // submit form cy.findByText(/submit/i, { selector: 'button' }).click(); diff --git a/src/applications/ezr/tests/e2e/ezr.cypress.spec.js b/src/applications/ezr/tests/e2e/ezr.cypress.spec.js index 3a002fd20bbf..62b9487bc9df 100644 --- a/src/applications/ezr/tests/e2e/ezr.cypress.spec.js +++ b/src/applications/ezr/tests/e2e/ezr.cypress.spec.js @@ -65,11 +65,11 @@ const testConfig = createTestConfig( }, 'review-and-submit': ({ afterHook }) => { afterHook(() => { - cy.get('[name="privacyAgreementAccepted"]') + cy.get('va-checkbox[name="privacyAgreementAccepted"]') .scrollIntoView() .shadow() - .find('[type="checkbox"]') - .check(); + .find('label') + .click(); cy.findByText(/submit/i, { selector: 'button' }).click(); }); }, diff --git a/src/applications/ezr/tests/unit/components/FormReview/DependentsReviewPage.unit.spec.js b/src/applications/ezr/tests/unit/components/FormReview/DependentsReviewPage.unit.spec.js index 137c612b55bb..57efc8092e12 100644 --- a/src/applications/ezr/tests/unit/components/FormReview/DependentsReviewPage.unit.spec.js +++ b/src/applications/ezr/tests/unit/components/FormReview/DependentsReviewPage.unit.spec.js @@ -20,7 +20,9 @@ describe('ezr DependentsReviewPage', () => { context('when no dependents are reported', () => { it('should not render edit button', () => { const { container } = render(); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); expect(selector).to.not.exist; }); @@ -53,7 +55,9 @@ describe('ezr DependentsReviewPage', () => { it('should render edit button', () => { const { container } = render(); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); expect(selector).to.exist; }); @@ -83,7 +87,9 @@ describe('ezr DependentsReviewPage', () => { it('should fire event to trigger the edit flow', () => { const { container } = render(); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); fireEvent.click(selector); expect(props.editPage.called).to.be.true; }); diff --git a/src/applications/ezr/tests/unit/components/FormReview/InsurancePolicyReviewPage.unit.spec.js b/src/applications/ezr/tests/unit/components/FormReview/InsurancePolicyReviewPage.unit.spec.js index 43a0e14f1b23..013dc46568ee 100644 --- a/src/applications/ezr/tests/unit/components/FormReview/InsurancePolicyReviewPage.unit.spec.js +++ b/src/applications/ezr/tests/unit/components/FormReview/InsurancePolicyReviewPage.unit.spec.js @@ -24,7 +24,9 @@ describe('ezr InsurancePolicyReviewPage', () => { const { container } = render( , ); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); expect(selector).to.not.exist; }); @@ -56,7 +58,9 @@ describe('ezr InsurancePolicyReviewPage', () => { it('should render edit button', () => { const { container } = render(); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); expect(selector).to.exist; }); @@ -83,7 +87,9 @@ describe('ezr InsurancePolicyReviewPage', () => { it('should fire event to trigger the edit flow', () => { const { container } = render(); - const selector = container.querySelector('.edit-btn'); + const selector = container.querySelector( + `va-button[text="${content['button-edit']}"]`, + ); fireEvent.click(selector); expect(props.editPage.called).to.be.true; }); diff --git a/src/applications/financial-status-report/components/bankruptcy/BankruptcyDetails.jsx b/src/applications/financial-status-report/components/bankruptcy/BankruptcyDetails.jsx index 4c4fb6fcef38..d8fd58816cb0 100644 --- a/src/applications/financial-status-report/components/bankruptcy/BankruptcyDetails.jsx +++ b/src/applications/financial-status-report/components/bankruptcy/BankruptcyDetails.jsx @@ -181,7 +181,6 @@ const BankruptcyDetails = ({ /> {contentBeforeButtons} - - {contentAfterButtons} ); diff --git a/src/applications/financial-status-report/components/employment/EmploymentHistoryWidget.jsx b/src/applications/financial-status-report/components/employment/EmploymentHistoryWidget.jsx index abd0b7c4b651..968c5b21105d 100644 --- a/src/applications/financial-status-report/components/employment/EmploymentHistoryWidget.jsx +++ b/src/applications/financial-status-report/components/employment/EmploymentHistoryWidget.jsx @@ -16,7 +16,6 @@ const EmploymentHistoryWidget = props => { const { goToPath, goForward, - onReviewPage, contentBeforeButtons, contentAfterButtons, } = props; @@ -40,7 +39,6 @@ const EmploymentHistoryWidget = props => { const navButtons = ( ); - const updateButton = ; return (

@@ -73,7 +71,7 @@ const EmploymentHistoryWidget = props => { {contentBeforeButtons} - {onReviewPage ? updateButton : navButtons} + {navButtons} {contentAfterButtons}{' '}
); diff --git a/src/applications/financial-status-report/components/employment/EnhancedEmploymentRecord.jsx b/src/applications/financial-status-report/components/employment/EnhancedEmploymentRecord.jsx index 3450f574804f..78b1940637fe 100644 --- a/src/applications/financial-status-report/components/employment/EnhancedEmploymentRecord.jsx +++ b/src/applications/financial-status-report/components/employment/EnhancedEmploymentRecord.jsx @@ -13,6 +13,7 @@ import { jobButtonConstants, } from '../../utils/session'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; +import ButtonGroup from '../shared/ButtonGroup'; const RETURN_PATH = '/employment-history'; @@ -142,28 +143,15 @@ const EmploymentRecord = props => { handleChange('isCurrent', value === 'true'); setCurrentlyWorksHere(value === 'true'); }, - getContinueButtonText: () => { - if ( - employmentRecord.isCurrent || - getJobButton() === jobButtonConstants.FIRST_JOB - ) { - return 'Continue'; - } - - if (getJobButton() === jobButtonConstants.EDIT_JOB) { - return 'Update employment record'; - } - return 'Add employment record'; - }, getCancelButtonText: () => { if (getJobButton() === jobButtonConstants.FIRST_JOB) { return 'Back'; } if (getJobButton() === jobButtonConstants.EDIT_JOB) { - return 'Cancel Edit Entry'; + return 'Cancel edit entry'; } - return 'Cancel Add Entry'; + return 'Cancel add entry'; }, }; @@ -233,24 +221,21 @@ const EmploymentRecord = props => { uswds /> -

- - -

+ + ); diff --git a/src/applications/financial-status-report/components/employment/EnhancedSpouseEmploymentRecord.jsx b/src/applications/financial-status-report/components/employment/EnhancedSpouseEmploymentRecord.jsx index 91799f6fa1ad..b1e71f3265cb 100644 --- a/src/applications/financial-status-report/components/employment/EnhancedSpouseEmploymentRecord.jsx +++ b/src/applications/financial-status-report/components/employment/EnhancedSpouseEmploymentRecord.jsx @@ -143,28 +143,15 @@ const EmploymentRecord = props => { handleChange('isCurrent', value === 'true'); setCurrentlyWorksHere(value === 'true'); }, - getContinueButtonText: () => { - if ( - employmentRecord.isCurrent || - getJobButton() === jobButtonConstants.FIRST_JOB - ) { - return 'Continue'; - } - - if (getJobButton() === jobButtonConstants.EDIT_JOB) { - return 'Update employment record'; - } - return 'Add employment record'; - }, getCancelButtonText: () => { if (getJobButton() === jobButtonConstants.FIRST_JOB) { return 'Back'; } if (getJobButton() === jobButtonConstants.EDIT_JOB) { - return 'Cancel Edit Entry'; + return 'Cancel edit entry'; } - return 'Cancel Add Entry'; + return 'Cancel add entry'; }, }; @@ -244,7 +231,7 @@ const EmploymentRecord = props => { isSecondary: true, }, { - label: handlers.getContinueButtonText(), + label: 'Continue', onClick: updateFormData, isSubmitting: true, }, diff --git a/src/applications/financial-status-report/components/employment/SpouseEmploymentHistoryWidget.jsx b/src/applications/financial-status-report/components/employment/SpouseEmploymentHistoryWidget.jsx index 17deb617bf11..bb2878e8053b 100644 --- a/src/applications/financial-status-report/components/employment/SpouseEmploymentHistoryWidget.jsx +++ b/src/applications/financial-status-report/components/employment/SpouseEmploymentHistoryWidget.jsx @@ -15,7 +15,6 @@ const SpouseEmploymentHistoryWidget = props => { const { goToPath, goForward, - onReviewPage, contentBeforeButtons, contentAfterButtons, } = props; @@ -38,7 +37,7 @@ const SpouseEmploymentHistoryWidget = props => { const navButtons = ( ); - const updateButton = ; + const emptyPrompt = `Select the ‘add additional job link to add another job. Select the continue button to move on to the next question.`; return ( @@ -76,7 +75,7 @@ const SpouseEmploymentHistoryWidget = props => { {contentBeforeButtons} - {onReviewPage ? updateButton : navButtons} + {navButtons} {contentAfterButtons} ); diff --git a/src/applications/financial-status-report/components/householdExpenses/CreditCardBill.jsx b/src/applications/financial-status-report/components/householdExpenses/CreditCardBill.jsx index 19a99572cbed..17d07e375715 100644 --- a/src/applications/financial-status-report/components/householdExpenses/CreditCardBill.jsx +++ b/src/applications/financial-status-report/components/householdExpenses/CreditCardBill.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { setData } from 'platform/forms-system/src/js/actions'; import { isValidCurrency } from '../../utils/validations'; +import ButtonGroup from '../shared/ButtonGroup'; const defaultRecord = [ { @@ -151,27 +152,26 @@ const CreditCardBill = props => { }, }; + const addCancelButtonsText = + creditCardBills.length === index ? 'Add' : 'Update'; + const renderAddCancelButtons = () => { return ( <> - - + ); }; @@ -179,22 +179,20 @@ const CreditCardBill = props => { const renderContinueBackButtons = () => { return ( <> - - + ); }; @@ -206,7 +204,7 @@ const CreditCardBill = props => {

{`${ creditCardBills.length === index ? 'Add' : 'Update' - } a credit card bill`} + } credit card bill`}

Enter your credit card bill’s information. diff --git a/src/applications/financial-status-report/components/householdExpenses/InstallmentContract.jsx b/src/applications/financial-status-report/components/householdExpenses/InstallmentContract.jsx index 4b2679a8c82d..c5fc4056fce2 100644 --- a/src/applications/financial-status-report/components/householdExpenses/InstallmentContract.jsx +++ b/src/applications/financial-status-report/components/householdExpenses/InstallmentContract.jsx @@ -9,6 +9,7 @@ import { import { parseISODate } from 'platform/forms-system/src/js/helpers'; import { isValidCurrency } from '../../utils/validations'; import ContractsExplainer from './ContractsExplainer'; +import ButtonGroup from '../shared/ButtonGroup'; const defaultRecord = [ { @@ -187,27 +188,26 @@ const InstallmentContract = props => { }, }; + const addUpdateButtonsText = + installmentContracts.length === index ? 'Add' : 'Update'; + const renderAddCancelButtons = () => { return ( <> - - + ); }; @@ -215,22 +215,20 @@ const InstallmentContract = props => { const renderContinueBackButtons = () => { return ( <> - - + ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/AddIncome.jsx b/src/applications/financial-status-report/components/householdIncome/AddIncome.jsx index 4b16854be61d..4b7c6ef0b213 100644 --- a/src/applications/financial-status-report/components/householdIncome/AddIncome.jsx +++ b/src/applications/financial-status-report/components/householdIncome/AddIncome.jsx @@ -6,6 +6,7 @@ import { } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; import { MAX_ASSET_NAME_LENGTH } from '../../constants/checkboxSelections'; +import ButtonGroup from '../shared/ButtonGroup'; const SUMMARY_PATH = '/other-income-summary'; const CHECKLIST_PATH = '/additional-income-checklist'; @@ -91,9 +92,12 @@ const AddIncome = ({ data, goToPath, setFormData }) => { }, }); } + handlers.onSubmit(event); }, }; + const labelText = addlIncRecords.length === index ? 'Add' : 'Update'; + return ( <>

@@ -135,26 +139,20 @@ const AddIncome = ({ data, goToPath, setFormData }) => { uswds />
-

- - -

+ diff --git a/src/applications/financial-status-report/components/householdIncome/GrossMonthlyIncomeInput.jsx b/src/applications/financial-status-report/components/householdIncome/GrossMonthlyIncomeInput.jsx index 1a615d6acefb..63de0bb1c07f 100644 --- a/src/applications/financial-status-report/components/householdIncome/GrossMonthlyIncomeInput.jsx +++ b/src/applications/financial-status-report/components/householdIncome/GrossMonthlyIncomeInput.jsx @@ -7,7 +7,7 @@ import { getJobIndex } from '../../utils/session'; import { isValidCurrency } from '../../utils/validations'; const GrossMonthlyIncomeInput = props => { - const { goToPath, goBack, onReviewPage = false, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -131,7 +131,6 @@ const GrossMonthlyIncomeInput = props => { }; const navButtons = ; - const updateButton = ; return (
@@ -176,7 +175,7 @@ const GrossMonthlyIncomeInput = props => {
  • Divide that number by 12
  • - {onReviewPage ? updateButton : navButtons} + {navButtons}
    ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/PayrollDeductionChecklist.jsx b/src/applications/financial-status-report/components/householdIncome/PayrollDeductionChecklist.jsx index 219e84bb6d8c..c05aa74ced7f 100644 --- a/src/applications/financial-status-report/components/householdIncome/PayrollDeductionChecklist.jsx +++ b/src/applications/financial-status-report/components/householdIncome/PayrollDeductionChecklist.jsx @@ -8,7 +8,7 @@ import Checklist from '../shared/CheckList'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; const PayrollDeductionChecklist = props => { - const { goToPath, goBack, onReviewPage, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -122,7 +122,6 @@ const PayrollDeductionChecklist = props => { }; const navButtons = ; - const updateButton = ; const title = `Your job at ${employerName}`; const prompt = 'Which of these payroll deductions do you pay for?'; @@ -142,7 +141,7 @@ const PayrollDeductionChecklist = props => { the deductions that apply to you.

    - {onReviewPage ? updateButton : navButtons} + {navButtons} ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/PayrollDeductionInputList.jsx b/src/applications/financial-status-report/components/householdIncome/PayrollDeductionInputList.jsx index 970942f09ec3..3dcd5ca10099 100644 --- a/src/applications/financial-status-report/components/householdIncome/PayrollDeductionInputList.jsx +++ b/src/applications/financial-status-report/components/householdIncome/PayrollDeductionInputList.jsx @@ -9,9 +9,10 @@ import { } from '../../utils/session'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; import { isValidCurrency } from '../../utils/validations'; +import ButtonGroup from '../shared/ButtonGroup'; const PayrollDeductionInputList = props => { - const { goToPath, goBack, onReviewPage = false, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -143,26 +144,21 @@ const PayrollDeductionInputList = props => { }; const navButtons = ( -

    - - -

    + ); - const updateButton = ; return (
    @@ -226,7 +222,7 @@ const PayrollDeductionInputList = props => { - {onReviewPage ? updateButton : navButtons} + {navButtons}
    ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/SpouseAddIncome.jsx b/src/applications/financial-status-report/components/householdIncome/SpouseAddIncome.jsx index a06edc636f61..31da11536fbb 100644 --- a/src/applications/financial-status-report/components/householdIncome/SpouseAddIncome.jsx +++ b/src/applications/financial-status-report/components/householdIncome/SpouseAddIncome.jsx @@ -6,6 +6,7 @@ import { } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; import { MAX_ASSET_NAME_LENGTH } from '../../constants/checkboxSelections'; +import ButtonGroup from '../shared/ButtonGroup'; const SUMMARY_PATH = '/spouse-other-income-summary'; const CHECKLIST_PATH = '/spouse-additional-income-checklist'; @@ -94,9 +95,12 @@ const SpouseAddIncome = ({ data, goToPath, setFormData }) => { }, }); } + handlers.onSubmit(event); }, }; + const labelText = spAddlIncome.length === index ? 'Add' : 'Update'; + return ( <>
    @@ -140,26 +144,21 @@ const SpouseAddIncome = ({ data, goToPath, setFormData }) => { uswds />
    -

    - - -

    + + diff --git a/src/applications/financial-status-report/components/householdIncome/SpouseGrossMonthlyIncomeInput.jsx b/src/applications/financial-status-report/components/householdIncome/SpouseGrossMonthlyIncomeInput.jsx index 0d6f8aac9e6d..51d2075a502a 100644 --- a/src/applications/financial-status-report/components/householdIncome/SpouseGrossMonthlyIncomeInput.jsx +++ b/src/applications/financial-status-report/components/householdIncome/SpouseGrossMonthlyIncomeInput.jsx @@ -7,7 +7,7 @@ import { getJobIndex } from '../../utils/session'; import { isValidCurrency } from '../../utils/validations'; const SpouseGrossMonthlyIncomeInput = props => { - const { goToPath, goBack, onReviewPage = false, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -131,7 +131,6 @@ const SpouseGrossMonthlyIncomeInput = props => { }; const navButtons = ; - const updateButton = ; return (
    @@ -178,7 +177,7 @@ const SpouseGrossMonthlyIncomeInput = props => {
  • Divide that number by 12
  • - {onReviewPage ? updateButton : navButtons} + {navButtons}
    ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionChecklist.jsx b/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionChecklist.jsx index d749b65b53df..59e422c0aa3e 100644 --- a/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionChecklist.jsx +++ b/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionChecklist.jsx @@ -8,7 +8,7 @@ import Checklist from '../shared/CheckList'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; const SpousePayrollDeductionChecklist = props => { - const { goToPath, goBack, onReviewPage, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -121,7 +121,7 @@ const SpousePayrollDeductionChecklist = props => { }; const navButtons = ; - const updateButton = ; + const title = `Your spouse’s job at ${employerName}`; const prompt = 'Which of these payroll deductions does your spouse pay for?'; @@ -144,7 +144,7 @@ const SpousePayrollDeductionChecklist = props => { spouse.

    - {onReviewPage ? updateButton : navButtons} + {navButtons} ); }; diff --git a/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionInputList.jsx b/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionInputList.jsx index db9f04c94241..c6c1bff86fcb 100644 --- a/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionInputList.jsx +++ b/src/applications/financial-status-report/components/householdIncome/SpousePayrollDeductionInputList.jsx @@ -10,9 +10,10 @@ import { } from '../../utils/session'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; import { isValidCurrency } from '../../utils/validations'; +import ButtonGroup from '../shared/ButtonGroup'; const SpousePayrollDeductionInputList = props => { - const { goToPath, goBack, onReviewPage = false, setFormData } = props; + const { goToPath, goBack, setFormData } = props; const editIndex = getJobIndex(); @@ -144,26 +145,21 @@ const SpousePayrollDeductionInputList = props => { }; const navButtons = ( -

    - - -

    + ); - const updateButton = ; return (
    @@ -231,7 +227,7 @@ const SpousePayrollDeductionInputList = props => { - {onReviewPage ? updateButton : navButtons} + {navButtons}
    ); }; @@ -256,5 +252,4 @@ SpousePayrollDeductionInputList.propTypes = { goBack: PropTypes.func.isRequired, goToPath: PropTypes.func.isRequired, setFormData: PropTypes.func.isRequired, - onReviewPage: PropTypes.bool, }; diff --git a/src/applications/financial-status-report/components/otherAssets/AddAsset.jsx b/src/applications/financial-status-report/components/otherAssets/AddAsset.jsx index a3b543a80877..c1c197236e21 100644 --- a/src/applications/financial-status-report/components/otherAssets/AddAsset.jsx +++ b/src/applications/financial-status-report/components/otherAssets/AddAsset.jsx @@ -6,6 +6,7 @@ import { } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; import { MAX_ASSET_NAME_LENGTH } from '../../constants/checkboxSelections'; +import ButtonGroup from '../shared/ButtonGroup'; const SUMMARY_PATH = '/other-assets-summary'; const CHECKLIST_PATH = '/other-assets-checklist'; @@ -84,12 +85,15 @@ const AddAsset = ({ data, goToPath, setFormData }) => { }, }); } + handlers.onSubmit(event); }, }; + const labelText = otherAssets.length === index ? 'Add' : 'Update'; + return ( <> -
    +
    { -

    - - -

    +
    diff --git a/src/applications/financial-status-report/components/otherAssets/EnhancedVehicleRecord.jsx b/src/applications/financial-status-report/components/otherAssets/EnhancedVehicleRecord.jsx index 8c0a84ab1ae4..459b0801c3c3 100644 --- a/src/applications/financial-status-report/components/otherAssets/EnhancedVehicleRecord.jsx +++ b/src/applications/financial-status-report/components/otherAssets/EnhancedVehicleRecord.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { VaTextInput } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; +import ButtonGroup from '../shared/ButtonGroup'; const defaultRecord = { make: '', @@ -118,25 +119,25 @@ const EnhancedVehicleRecord = ({ data, goToPath, setFormData }) => { } }; + const labelText = automobiles.length === editIndex ? 'Add' : 'Update'; + const renderAddCancelButtons = () => { return ( <> - - + ); }; @@ -144,22 +145,20 @@ const EnhancedVehicleRecord = ({ data, goToPath, setFormData }) => { const renderContinueBackButtons = () => { return ( <> - - + ); }; diff --git a/src/applications/financial-status-report/components/otherExpenses/AddOtherExpense.jsx b/src/applications/financial-status-report/components/otherExpenses/AddOtherExpense.jsx index c8daa6636f29..5ce250bd31ea 100644 --- a/src/applications/financial-status-report/components/otherExpenses/AddOtherExpense.jsx +++ b/src/applications/financial-status-report/components/otherExpenses/AddOtherExpense.jsx @@ -6,6 +6,7 @@ import { } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; import { MAX_OTHER_LIVING_NAME_LENGTH } from '../../constants/checkboxSelections'; +import ButtonGroup from '../shared/ButtonGroup'; const SUMMARY_PATH = '/other-expenses-summary'; const CHECKLIST_PATH = '/other-expenses-checklist'; @@ -91,6 +92,7 @@ const AddOtherExpense = ({ data, goToPath, setFormData }) => { otherExpenses: newExpenses, }); } + handlers.onSubmit(event); }, }; @@ -99,6 +101,8 @@ const AddOtherExpense = ({ data, goToPath, setFormData }) => { ? 'Add your additional living expense' : 'Update your living expense'; + const labelText = otherExpenses.length === index ? 'Add' : 'Update'; + return ( <>
    @@ -140,22 +144,20 @@ const AddOtherExpense = ({ data, goToPath, setFormData }) => { uswds />
    - - +
    diff --git a/src/applications/financial-status-report/components/utilityBills/AddUtilityBill.jsx b/src/applications/financial-status-report/components/utilityBills/AddUtilityBill.jsx index 3d3c95223f21..e5bfe607150d 100644 --- a/src/applications/financial-status-report/components/utilityBills/AddUtilityBill.jsx +++ b/src/applications/financial-status-report/components/utilityBills/AddUtilityBill.jsx @@ -6,6 +6,7 @@ import { } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { isValidCurrency } from '../../utils/validations'; import { MAX_UTILITY_NAME_LENGTH } from '../../constants/checkboxSelections'; +import ButtonGroup from '../shared/ButtonGroup'; const SUMMARY_PATH = '/utility-bill-summary'; const CHECKLIST_PATH = '/utility-bill-checklist'; @@ -81,6 +82,7 @@ const AddUtilityBill = ({ data, goToPath, setFormData }) => { utilityRecords: newUtility, }); } + handlers.onSubmit(event); }, }; @@ -89,6 +91,8 @@ const AddUtilityBill = ({ data, goToPath, setFormData }) => { ? 'Add your additional utility bill' : 'Update your utility bill'; + const labelText = utilityRecords.length === index ? 'Add' : 'Update'; + return ( <>
    @@ -128,26 +132,21 @@ const AddUtilityBill = ({ data, goToPath, setFormData }) => { value={utilityAmount || ''} uswds /> -

    - - -

    + + diff --git a/src/applications/financial-status-report/sass/financial-status-report.scss b/src/applications/financial-status-report/sass/financial-status-report.scss index 3cecf6df2931..39123d893ebd 100644 --- a/src/applications/financial-status-report/sass/financial-status-report.scss +++ b/src/applications/financial-status-report/sass/financial-status-report.scss @@ -387,9 +387,12 @@ $chapters: veteranInformationChapter, householdIncomeChapter, .va-button-override va-button { &::part(button) { white-space: nowrap; + z-index: 1; + position: relative; } @media (max-width: $xsmall-screen) { width: -webkit-fill-available; + margin-bottom: 1rem; &::part(button) { width: 100%; } diff --git a/src/applications/financial-status-report/tests/e2e/efsr-5655.cypress.spec.js b/src/applications/financial-status-report/tests/e2e/efsr-5655.cypress.spec.js index 159004bdcf39..80ad1be6d844 100644 --- a/src/applications/financial-status-report/tests/e2e/efsr-5655.cypress.spec.js +++ b/src/applications/financial-status-report/tests/e2e/efsr-5655.cypress.spec.js @@ -239,8 +239,9 @@ const testConfig = createTestConfig( .shadow() .find('input') .type('1500'); - cy.findAllByText(/Continue/i, { selector: 'button' }) - .first() + cy.get('va-button[data-testid="custom-button-group-button"]') + .shadow() + .find('button:contains("Continue")') .click({ waitForAnimations: true }); }); }, @@ -388,8 +389,9 @@ const testConfig = createTestConfig( .shadow() .find('input') .type('10'); - cy.findAllByText(/Continue/i, { selector: 'button' }) - .first() + cy.get('va-button[data-testid="custom-button-group-button"]') + .shadow() + .find('button:contains("Continue")') .click(); // cy.get('.usa-button-primary').click(); }); @@ -451,10 +453,9 @@ const testConfig = createTestConfig( .shadow() .find('input') .type('10'); - cy.findAllByText(/Continue/i, { - selector: 'button', - }) - .first() + cy.get('va-button[data-testid="custom-button-group-button"]') + .shadow() + .find('button:contains("Continue")') .click(); // cy.get('.usa-button-primary').click(); }); diff --git a/src/applications/gi/components/VaAccordionGi.jsx b/src/applications/gi/components/VaAccordionGi.jsx index 6aeaadadf77e..a2753f4b5ce7 100644 --- a/src/applications/gi/components/VaAccordionGi.jsx +++ b/src/applications/gi/components/VaAccordionGi.jsx @@ -7,8 +7,15 @@ import { const VaAccordionGi = ({ title, children, expanded, onChange }) => { return ( - - + + { + if (e.target.tagName === 'VA-ACCORDION-ITEM') { + onChange(); + } + }} + >

    {title}

    {children}
    diff --git a/src/applications/gi/components/profile/JumpLink.jsx b/src/applications/gi/components/profile/JumpLink.jsx index 79018bc546c2..fc62a639463a 100644 --- a/src/applications/gi/components/profile/JumpLink.jsx +++ b/src/applications/gi/components/profile/JumpLink.jsx @@ -4,13 +4,23 @@ import scrollTo from 'platform/utilities/ui/scrollTo'; import recordEvent from 'platform/monitoring/record-event'; import { isProductionOfTestProdEnv } from '../../utils/helpers'; -export default function JumpLink({ label, jumpToId, iconToggle = true }) { +export default function JumpLink({ + label, + jumpToId, + iconToggle = true, + onClick, + dataTestId, + customClass, +}) { const jumpLinkClicked = e => { e.preventDefault(); scrollTo(jumpToId, getScrollOptions()); }; const handleClick = e => { + if (onClick) { + onClick(); + } jumpLinkClicked(e); recordEvent({ event: 'nav-jumplink-click', @@ -35,14 +45,17 @@ export default function JumpLink({ label, jumpToId, iconToggle = true }) { return ( -

    - {iconToggle &&

    - +

    We didn’t receive your online application

    diff --git a/src/applications/hca/components/FormDescriptions/AgentOrangeExposureDescription.jsx b/src/applications/hca/components/FormDescriptions/AgentOrangeExposureDescription.jsx new file mode 100644 index 000000000000..cc74dc4dac9f --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/AgentOrangeExposureDescription.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const AgentOrangeExposureDescription = ( +

      +
    • Cambodia at Mimot or Krek, Kampong Cham Province
    • +
    • Guam, American Samoa, or their territorial waters
    • +
    • In or near the Korean demilitarized zone
    • +
    • Johnston Atoll or on a ship that called at Johnston Atoll
    • +
    • Laos
    • +
    • + Any location where you had contact with C-123 airplanes while serving in + the Air Force or the Air Force Reserves +
    • +
    • A U.S. or Royal Thai military base in Thailand
    • +
    • Vietnam or the waters in or off of Vietnam
    • +
    +); + +export default AgentOrangeExposureDescription; diff --git a/src/applications/hca/components/FormDescriptions/CombatOperationServiceDescription.jsx b/src/applications/hca/components/FormDescriptions/CombatOperationServiceDescription.jsx new file mode 100644 index 000000000000..97ee4d33a6d5 --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/CombatOperationServiceDescription.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const CombatOperationServiceDescription = ( +
      +
    • Enduring Freedom
    • +
    • Freedom’s Sentinel
    • +
    • Iraqi Freedom
    • +
    • New Dawn
    • +
    • Inherent Resolve
    • +
    • Resolute Support Mission
    • +
    +); + +export default CombatOperationServiceDescription; diff --git a/src/applications/hca/components/FormDescriptions/DateRangeDescription.jsx b/src/applications/hca/components/FormDescriptions/DateRangeDescription.jsx new file mode 100644 index 000000000000..31d32e37811e --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/DateRangeDescription.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +const DateRangeDescription = ( + + You only need to enter one date range. We’ll use this information to find + your record. + +); + +export default DateRangeDescription; diff --git a/src/applications/hca/components/FormDescriptions/GulfWarServiceDescription.jsx b/src/applications/hca/components/FormDescriptions/GulfWarServiceDescription.jsx new file mode 100644 index 000000000000..d51328a503b3 --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/GulfWarServiceDescription.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const GulfWarServiceDescription = ( +
      +
    • Afghanistan
    • +
    • Arabian Sea
    • +
    • Bahrain
    • +
    • Djibouti
    • +
    • Egypt
    • +
    • Gulf of Aden
    • +
    • Gulf of Oman
    • +
    • Iraq
    • +
    • Israel
    • +
    • Jordan
    • +
    • Kuwait
    • +
    • Lebanon
    • +
    • Neutral zone between Iraq and Saudi Arabia
    • +
    • Oman
    • +
    • Persian Gulf
    • +
    • Qatar
    • +
    • Red Sea
    • +
    • Saudi Arabia
    • +
    • Somalia
    • +
    • Syria
    • +
    • Turkey
    • +
    • United Arab Emirates
    • +
    • Uzbekistan
    • +
    • Yemen
    • +
    +); + +export default GulfWarServiceDescription; diff --git a/src/applications/hca/components/FormDescriptions/RadiationCleanupDescription.jsx b/src/applications/hca/components/FormDescriptions/RadiationCleanupDescription.jsx new file mode 100644 index 000000000000..885f4158ce49 --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/RadiationCleanupDescription.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const RadiationCleanupDescription = ( +
      +
    • The cleanup of Hiroshima and Nagasaki or Enewetak Atoll
    • +
    • + The cleanup of an Air Force B-52 bomber carrying nuclear weapons off the + coast of Palomares, Spain +
    • +
    • + The response to the fire onboard an Air Force B-52 bomber carrying nuclear + weapons near Thule Air Force Base in Greenland +
    • +
    +); + +export default RadiationCleanupDescription; diff --git a/src/applications/hca/components/FormDescriptions/ToxicExposureDescription.jsx b/src/applications/hca/components/FormDescriptions/ToxicExposureDescription.jsx new file mode 100644 index 000000000000..002d935c83b4 --- /dev/null +++ b/src/applications/hca/components/FormDescriptions/ToxicExposureDescription.jsx @@ -0,0 +1,88 @@ +import React from 'react'; + +const ToxicExposureDescription = ( + <> +

    + Next we’ll ask you more questions about your military service history and + any toxic exposure during your military service. +

    +

    + Toxic exposure is exposure to any hazards or substances like Agent Orange, + burn pits, radiation, asbestos, or contaminated water. +

    +

    + + Learn more about exposures on our public health website (opens in new + tab) + +

    +

    + Why we ask for this information +

    +

    + It’s your choice whether you want to answer more questions about your + military service history and toxic exposure during your military service. + Before you decide, here’s what to know about how we’ll use this + information. +

    +

    We use this information in these ways:

    +
      +
    • + We’ll determine if you’re more likely to get VA health care benefits. We + call this “enhanced eligibility status.” +
    • +
    • + We’ll add information about your military service history and toxic + exposure to your VA medical record. +
    • +
    + +

    + You may qualify for enhanced eligibility status if you receive any of + these benefits: +

    +
      +
    • VA pension
    • +
    • VA service-connected disability compensation
    • +
    • Medicaid benefits
    • +
    +

    + You may also qualify for enhanced eligibility status if you fit one of + these descriptions: +

    +
      +
    • You’re a former Prisoner of War (POW).
    • +
    • You received a Purple Heart.
    • +
    • You received a Medal of Honor.
    • +
    • + You served in Southwest Asia during the Gulf War between August 2, + 1990, and November 11, 1998. +
    • +
    • + You were exposed to toxins or hazards by working with chemicals, + pesticides, lead, asbestos, certain paints, nuclear weapons, x-rays, + or other toxins. This exposure could have happened while training or + serving on active duty, even if you were never deployed. +
    • +
    • + You served at least 30 days at Camp Lejeune between August 1, 1953, + and December 31, 1987. +
    • +
    • + You served in a location where you had exposure to Agent Orange during + the Vietnam War era. +
    • +
    +
    + +); + +export default ToxicExposureDescription; diff --git a/src/applications/hca/components/FormDescriptions/index.jsx b/src/applications/hca/components/FormDescriptions/index.jsx index 55457cfa9992..d0fb5a1eec2b 100644 --- a/src/applications/hca/components/FormDescriptions/index.jsx +++ b/src/applications/hca/components/FormDescriptions/index.jsx @@ -20,6 +20,7 @@ export const BirthSexDescription = (

    @@ -96,6 +97,7 @@ export const SIGIGenderDescription = (

    @@ -123,14 +125,22 @@ export const SIGIGenderDescription = ( ); /** CHAPTER 2: Military Service */ -export const ServiceHistoryTitle = ( - <> - Service history - . -

    - Check all that apply to you. -
    - +export const ServiceDateRangeDescription = ( +
    + If you don’t know the exact date, enter your best guess +
    +); + +export const OtherToxicExposureDescription = ( + ); /** CHAPTER 3: VA Benefits */ @@ -148,6 +158,7 @@ export const CompensationTypeDescription = (

    @@ -190,6 +201,7 @@ export const PensionTypeDescription = (

    @@ -237,6 +249,7 @@ export const DependentDescription = () => {

    @@ -277,6 +290,7 @@ export const DependentSupportDescription = (

    @@ -299,6 +313,7 @@ export const EducationalExpensesDescription = (

    @@ -329,6 +344,7 @@ export const GrossIncomeDescription = (

    @@ -379,6 +395,7 @@ export const MaritalStatusDescription = (

    @@ -395,6 +412,7 @@ export const MedicalExpensesDescription = (

    @@ -426,6 +444,7 @@ export const OtherIncomeDescription = (

    @@ -471,6 +490,7 @@ export const SpouseAdditionalInformationDescription = () => {

    @@ -486,6 +506,7 @@ export const SpouseFinancialSupportDescription = (

    @@ -510,6 +531,7 @@ export const EssentialCoverageDescription = ( To avoid the penalty for not having insurance, you must be enrolled in a health plan that qualifies as minimum essential coverage. Being signed up @@ -571,6 +593,7 @@ export const HealthInsuranceCoverageDescription = (

    @@ -624,6 +647,7 @@ export const MedicareClaimNumberDescription = (

    @@ -690,7 +714,7 @@ export const PolicyOrGroupDescription = ( ); export const TricarePolicyDescription = ( - +

    You can use your Department of Defense benefits number (DBN) or your diff --git a/src/applications/hca/components/FormFields/DependentList.jsx b/src/applications/hca/components/FormFields/DependentList.jsx index 207cf212dc43..5204d2315228 100644 --- a/src/applications/hca/components/FormFields/DependentList.jsx +++ b/src/applications/hca/components/FormFields/DependentList.jsx @@ -122,6 +122,7 @@ const DependentList = ({ labelledBy, list, mode, onDelete }) => { className="fas fa-chevron-right vads-u-margin-left--0p5" /> + {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */} + uswds + /> )} ); diff --git a/src/applications/hca/components/FormPages/DisabilityConfirmation.jsx b/src/applications/hca/components/FormPages/DisabilityConfirmation.jsx index be45bfab320c..b5cbbf226015 100644 --- a/src/applications/hca/components/FormPages/DisabilityConfirmation.jsx +++ b/src/applications/hca/components/FormPages/DisabilityConfirmation.jsx @@ -13,12 +13,11 @@ const DisabilityConfirmation = ({ data, goBack, goForward }) => { }; return ( - -

    +

    Confirm that you receive service-connected pay for a 50% or higher disability rating

    @@ -46,7 +45,7 @@ const DisabilityConfirmation = ({ data, goBack, goForward }) => { />
    - + ); }; diff --git a/src/applications/hca/components/FormPages/FinancialConfirmation.jsx b/src/applications/hca/components/FormPages/FinancialConfirmation.jsx index a62f5a5f9217..9b150d73624e 100644 --- a/src/applications/hca/components/FormPages/FinancialConfirmation.jsx +++ b/src/applications/hca/components/FormPages/FinancialConfirmation.jsx @@ -6,12 +6,11 @@ const DisabilityConfirmation = props => { const { goBack, goForward } = props; return ( - -

    +

    Confirm that you don’t want to provide your household financial information

    @@ -49,7 +48,7 @@ const DisabilityConfirmation = props => { />
    - + ); }; diff --git a/src/applications/hca/components/FormPages/FinancialOnboarding.jsx b/src/applications/hca/components/FormPages/FinancialOnboarding.jsx index 14fde2ce8c75..a0add6d7d34e 100644 --- a/src/applications/hca/components/FormPages/FinancialOnboarding.jsx +++ b/src/applications/hca/components/FormPages/FinancialOnboarding.jsx @@ -61,7 +61,10 @@ const HouseholdFinancialOnboarding = props => { with the Internal Revenue Service (IRS).

    - +

    You may qualify for enhanced eligibility status if you receive any diff --git a/src/applications/hca/components/FormReview/DependentsReviewPage.jsx b/src/applications/hca/components/FormReview/DependentsReviewPage.jsx index 7efc45b798f4..dee74d7133de 100644 --- a/src/applications/hca/components/FormReview/DependentsReviewPage.jsx +++ b/src/applications/hca/components/FormReview/DependentsReviewPage.jsx @@ -26,14 +26,13 @@ const DependentsReviewPage = ({ data, editPage }) => {

    Your Dependents

    - + secondary + uswds + />
    {reviewRows}
    diff --git a/src/applications/hca/components/IntroductionPage/EnrollmentStatus/FAQ/ApplyButton.jsx b/src/applications/hca/components/IntroductionPage/EnrollmentStatus/FAQ/ApplyButton.jsx index 7c4cbd4727a6..e1912e7d3dd4 100644 --- a/src/applications/hca/components/IntroductionPage/EnrollmentStatus/FAQ/ApplyButton.jsx +++ b/src/applications/hca/components/IntroductionPage/EnrollmentStatus/FAQ/ApplyButton.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import recordEvent from 'platform/monitoring/record-event'; const ApplyButton = ({ event, label, clickEvent }) => ( + // eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component
    ); }; diff --git a/src/applications/mhv/medical-records/containers/VaccineDetails.jsx b/src/applications/mhv/medical-records/containers/VaccineDetails.jsx index 9a966a7db5f9..3ce1bff48f20 100644 --- a/src/applications/mhv/medical-records/containers/VaccineDetails.jsx +++ b/src/applications/mhv/medical-records/containers/VaccineDetails.jsx @@ -51,7 +51,7 @@ const VaccineDetails = props => { ); const { vaccineId } = useParams(); const dispatch = useDispatch(); - const activeAlert = useAlerts(); + const activeAlert = useAlerts(dispatch); useEffect( () => { diff --git a/src/applications/mhv/medical-records/containers/Vaccines.jsx b/src/applications/mhv/medical-records/containers/Vaccines.jsx index 0db7f45e59a5..ab09d0924d99 100644 --- a/src/applications/mhv/medical-records/containers/Vaccines.jsx +++ b/src/applications/mhv/medical-records/containers/Vaccines.jsx @@ -53,7 +53,7 @@ const Vaccines = props => { FEATURE_FLAG_NAMES.mhvMedicalRecordsAllowTxtDownloads ], ); - const activeAlert = useAlerts(); + const activeAlert = useAlerts(dispatch); useEffect( () => { diff --git a/src/applications/mhv/medical-records/containers/VitalDetails.jsx b/src/applications/mhv/medical-records/containers/VitalDetails.jsx index 1e987103bf91..5127884fac9a 100644 --- a/src/applications/mhv/medical-records/containers/VitalDetails.jsx +++ b/src/applications/mhv/medical-records/containers/VitalDetails.jsx @@ -61,7 +61,7 @@ const VitalDetails = props => { const [currentVitals, setCurrentVitals] = useState([]); const [currentPage, setCurrentPage] = useState(1); const paginatedVitals = useRef([]); - const activeAlert = useAlerts(); + const activeAlert = useAlerts(dispatch); useEffect( () => { diff --git a/src/applications/mhv/medical-records/containers/Vitals.jsx b/src/applications/mhv/medical-records/containers/Vitals.jsx index 1d68442ab94d..88c39870e708 100644 --- a/src/applications/mhv/medical-records/containers/Vitals.jsx +++ b/src/applications/mhv/medical-records/containers/Vitals.jsx @@ -24,7 +24,7 @@ const Vitals = () => { const user = useSelector(state => state.user.profile); const [cards, setCards] = useState(null); const dispatch = useDispatch(); - const activeAlert = useAlerts(); + const activeAlert = useAlerts(dispatch); useEffect( () => { diff --git a/src/applications/mhv/medical-records/hooks/use-alerts.js b/src/applications/mhv/medical-records/hooks/use-alerts.js index 89f9497822bc..93dfa3bf240a 100644 --- a/src/applications/mhv/medical-records/hooks/use-alerts.js +++ b/src/applications/mhv/medical-records/hooks/use-alerts.js @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; +import { clearAlerts } from '../actions/alerts'; -const useAlerts = () => { +const useAlerts = dispatch => { const alertList = useSelector(state => state.mr.alerts?.alertList); const [activeAlert, setActiveAlert] = useState(); @@ -21,6 +22,15 @@ const useAlerts = () => { [alertList], ); + useEffect( + () => { + return () => { + if (dispatch) dispatch(clearAlerts()); + }; + }, + [dispatch], + ); + return activeAlert; }; diff --git a/src/applications/mhv/medical-records/reducers/alerts.js b/src/applications/mhv/medical-records/reducers/alerts.js index 8e08594e0560..d9ac2b4aa2ab 100644 --- a/src/applications/mhv/medical-records/reducers/alerts.js +++ b/src/applications/mhv/medical-records/reducers/alerts.js @@ -16,12 +16,20 @@ export const alertsReducer = (state = initialState, action) => { datestamp: new Date(), isActive: true, type: action.payload.type, + errorMessage: action.payload.error.message, + errorStackTrace: action.payload.error.stack, }; return { ...state, alertList: [...state.alertList, newAlert], }; } + case Actions.Alerts.CLEAR_ALERT: { + return { + ...state, + alertList: state.alertList.map(item => ({ ...item, isActive: false })), + }; + } default: return state; } diff --git a/src/applications/mhv/medical-records/reducers/careSummariesAndNotes.js b/src/applications/mhv/medical-records/reducers/careSummariesAndNotes.js index 1b6149a9b995..215e47a45d11 100644 --- a/src/applications/mhv/medical-records/reducers/careSummariesAndNotes.js +++ b/src/applications/mhv/medical-records/reducers/careSummariesAndNotes.js @@ -16,33 +16,30 @@ const initialState = { }; const extractName = record => { - if ( - record.content && - record.content.length > 0 && - record.content[0].attachment && - record.content[0].attachment.title - ) { - return record.content[0].attachment.title; - } return ( - isArrayAndHasItems(record.type?.coding) && record.type.coding[0].display + record.content?.[0]?.attachment?.title || + (isArrayAndHasItems(record.type?.coding) + ? record.type.coding[0].display + : null) ); }; const extractType = record => { - return isArrayAndHasItems(record.type?.coding) && record.type.coding[0].code; + return isArrayAndHasItems(record.type?.coding) + ? record.type.coding[0].code + : null; }; const extractAuthenticator = record => { return extractContainedResource(record, record.authenticator?.reference) - ?.name[0].text; + ?.name?.[0]?.text; }; const extractAuthor = record => { return extractContainedResource( record, isArrayAndHasItems(record.author) && record.author[0].reference, - )?.name[0].text; + )?.name?.[0]?.text; }; const extractLocation = record => { @@ -54,19 +51,21 @@ const extractLocation = record => { }; const extractNote = record => { - return ( + if ( isArrayAndHasItems(record.content) && - typeof record.content[0].attachment?.data === 'string' && - Buffer.from(record.content[0].attachment.data, 'base64') + typeof record.content[0].attachment?.data === 'string' + ) { + return Buffer.from(record.content[0].attachment.data, 'base64') .toString('utf-8') - .replace(/\r\n|\r/g, '\n') // Standardize line endings - ); + .replace(/\r\n|\r/g, '\n'); // Standardize line endings + } + return null; }; export const getDateSigned = record => { - const ext = record.authenticator.extension; - if (isArrayAndHasItems(ext) && ext[0].valueDateTime) { - return formatDateLong(ext[0].valueDateTime); + if (isArrayAndHasItems(record.authenticator?.extension)) { + const ext = record.authenticator.extension.find(e => e.valueDateTime); + return ext ? formatDateLong(ext.valueDateTime) : null; } return null; }; @@ -84,10 +83,11 @@ const convertAdmissionAndDischargeDetails = record => { dischargeDate: record.context?.period?.end ? formatDateLong(record.context?.period?.end) : EMPTY_FIELD, - admittedBy: summary - .split('ATTENDING:')[1] - .split('\n')[0] - .trim(), + admittedBy: + summary + .split('ATTENDING:')[1] + ?.split('\n')[0] + ?.trim() || EMPTY_FIELD, dischargedBy: extractAuthor(record) || EMPTY_FIELD, location: extractLocation(record) || EMPTY_FIELD, summary: summary || EMPTY_FIELD, diff --git a/src/applications/mhv/medical-records/routes.jsx b/src/applications/mhv/medical-records/routes.jsx index adb6f7f63565..e24376d38a91 100644 --- a/src/applications/mhv/medical-records/routes.jsx +++ b/src/applications/mhv/medical-records/routes.jsx @@ -1,7 +1,9 @@ import React from 'react'; import { Switch, Route } from 'react-router-dom'; import FEATURE_FLAG_NAMES from '@department-of-veterans-affairs/platform-utilities/featureFlagNames'; +import PageNotFound from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; import FeatureFlagRoute from './components/shared/FeatureFlagRoute'; +import AppRoute from './components/shared/AppRoute'; import HealthConditions from './containers/HealthConditions'; import VaccineDetails from './containers/VaccineDetails'; import Vaccines from './containers/Vaccines'; @@ -19,122 +21,124 @@ import DownloadRecordsPage from './containers/DownloadRecordsPage'; import SettingsPage from './containers/SettingsPage'; import RadiologyImagesList from './containers/RadiologyImagesList'; import RadiologySingleImage from './containers/RadiologySingleImage'; -import App from './containers/App'; const routes = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); export default routes; diff --git a/src/applications/mhv/medical-records/sass/landing-page.scss b/src/applications/mhv/medical-records/sass/landing-page.scss index 2d6cddab0f03..7624f9b60cba 100644 --- a/src/applications/mhv/medical-records/sass/landing-page.scss +++ b/src/applications/mhv/medical-records/sass/landing-page.scss @@ -1,10 +1,8 @@ .landing-page { - .section-link { - a { - color: $color-link-default; - } - a:visited { - color: $color-link-default; - } + a { + color: $color-link-default; + } + a:visited { + color: $color-link-default; } } diff --git a/src/applications/mhv/medical-records/sass/medical-records.scss b/src/applications/mhv/medical-records/sass/medical-records.scss index 48063941716a..67f67f80f437 100644 --- a/src/applications/mhv/medical-records/sass/medical-records.scss +++ b/src/applications/mhv/medical-records/sass/medical-records.scss @@ -127,3 +127,7 @@ font-family: Roboto Mono; // font-family: monospace; } + +va-loading-indicator { + margin: 200px auto; +} diff --git a/src/applications/mhv/medical-records/tests/actions/alerts.unit.spec.js b/src/applications/mhv/medical-records/tests/actions/alerts.unit.spec.js new file mode 100644 index 000000000000..973a73d09624 --- /dev/null +++ b/src/applications/mhv/medical-records/tests/actions/alerts.unit.spec.js @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { Actions } from '../../util/actionTypes'; +import { addAlert, clearAlerts } from '../../actions/alerts'; +import * as Constants from '../../util/constants'; + +describe('Add alert action ', () => { + it('should dispatch an add alerts action', () => { + const dispatch = sinon.spy(); + const error = new Error('This is an error'); + return addAlert(Constants.ALERT_TYPE_ERROR, error)(dispatch).then(() => { + expect(dispatch.firstCall.args[0].type).to.equal( + Actions.Alerts.ADD_ALERT, + ); + }); + }); +}); + +describe('Clear alerts action', () => { + it('should dispatch a clear alerts action', () => { + const dispatch = sinon.spy(); + return clearAlerts()(dispatch).then(() => { + expect(dispatch.firstCall.args[0].type).to.equal( + Actions.Alerts.CLEAR_ALERT, + ); + }); + }); +}); diff --git a/src/applications/mhv/medical-records/tests/actions/allergies.unit.spec.js b/src/applications/mhv/medical-records/tests/actions/allergies.unit.spec.js index d60fa4f8d956..27511ee75c7e 100644 --- a/src/applications/mhv/medical-records/tests/actions/allergies.unit.spec.js +++ b/src/applications/mhv/medical-records/tests/actions/allergies.unit.spec.js @@ -35,7 +35,7 @@ describe('Get allergies action', () => { }); }); -describe('Get allergy action', () => { +describe('Get allergy details action', () => { it('should dispatch a get details action', () => { const mockData = allergy; mockApiRequest(mockData); @@ -44,18 +44,7 @@ describe('Get allergy action', () => { expect(dispatch.firstCall.args[0].type).to.equal(Actions.Allergies.GET); }); }); - it('should dispatch an add alert action', () => { - const mockData = allergy; - mockApiRequest(mockData, false); - const dispatch = sinon.spy(); - return getAllergyDetails()(dispatch).then(() => { - expect(typeof dispatch.firstCall.args[0]).to.equal('function'); - }); - }); -}); - -describe('Get allergy details action ', () => { - it('should dispatch a get details action and pull the list', () => { + it('should dispatch a get details action and pull from the list argument', () => { const dispatch = sinon.spy(); return getAllergyDetails('1', [{ id: '1' }])(dispatch).then(() => { expect(dispatch.firstCall.args[0].type).to.equal( @@ -64,6 +53,8 @@ describe('Get allergy details action ', () => { }); }); it('should dispatch an add alert action', () => { + const mockData = allergy; + mockApiRequest(mockData, false); const dispatch = sinon.spy(); return getAllergyDetails()(dispatch).then(() => { expect(typeof dispatch.firstCall.args[0]).to.equal('function'); diff --git a/src/applications/mhv/medical-records/tests/actions/careSummariesAndNotes.unit.spec.js b/src/applications/mhv/medical-records/tests/actions/careSummariesAndNotes.unit.spec.js index 091457575fe5..2f70a05bcf03 100644 --- a/src/applications/mhv/medical-records/tests/actions/careSummariesAndNotes.unit.spec.js +++ b/src/applications/mhv/medical-records/tests/actions/careSummariesAndNotes.unit.spec.js @@ -26,9 +26,15 @@ describe('Get care summaries and notes list action', () => { const mockData = notes; mockApiRequest(mockData, false); const dispatch = sinon.spy(); - return getCareSummariesAndNotesList()(dispatch).then(() => { - expect(typeof dispatch.firstCall.args[0]).to.equal('function'); - }); + return getCareSummariesAndNotesList()(dispatch) + .then(() => { + throw new Error( + 'Expected getCareSummariesAndNotesList() to throw an error.', + ); + }) + .catch(() => { + expect(typeof dispatch.firstCall.args[0]).to.equal('function'); + }); }); }); diff --git a/src/applications/mhv/medical-records/tests/containers/App.unit.spec.jsx b/src/applications/mhv/medical-records/tests/containers/App.unit.spec.jsx index 689aed61feaf..e97e36eb1d8b 100644 --- a/src/applications/mhv/medical-records/tests/containers/App.unit.spec.jsx +++ b/src/applications/mhv/medical-records/tests/containers/App.unit.spec.jsx @@ -52,7 +52,7 @@ describe('App', () => { // services: [backendServices.HEALTH_RECORDS], }, }, - sm: { + mr: { breadcrumbs: { list: [], }, @@ -173,7 +173,7 @@ describe('App', () => { globalDowntime: true, isReady: true, isPending: false, - serviceMap: downtime([]), + serviceMap: downtime(['global']), dismissedDowntimeWarnings: [], }, ...initialState, @@ -214,15 +214,18 @@ describe('App', () => { path: `/`, }); expect( - screen.getByText('This tool is down for maintenance', { + screen.getByText('Maintenance on My HealtheVet', { selector: 'h3', exact: true, }), ); expect( - screen.getByText('We’re making some updates to this tool', { - exact: false, - }), + screen.getByText( + 'We’re working on My HealtheVet. The maintenance will last 48 hours.', + { + exact: false, + }, + ), ); }); @@ -246,15 +249,18 @@ describe('App', () => { path: `/`, }); expect( - screen.getByText('This tool is down for maintenance', { + screen.getByText('Maintenance on My HealtheVet', { selector: 'h3', exact: true, }), ); expect( - screen.getByText('We’re making some updates to this tool', { - exact: false, - }), + screen.getByText( + 'We’re working on My HealtheVet. The maintenance will last 48 hours', + { + exact: false, + }, + ), ); }); @@ -278,7 +284,7 @@ describe('App', () => { path: `/`, }); const downtimeComponent = screen.queryByText( - 'This tool is down for maintenance', + 'Maintenance on My HealtheVet', { selector: 'h3', exact: true, @@ -288,6 +294,50 @@ describe('App', () => { }); }); + it('renders breadcrumbs when downtime and at the landing page', () => { + const screen = renderWithStoreAndRouter(, { + initialState: { + featureToggles: { + // eslint-disable-next-line camelcase + mhv_medical_records_to_va_gov_release: true, + }, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: downtime(['mhv_mr']), + dismissedDowntimeWarnings: [], + }, + ...initialState, + }, + reducers: reducer, + path: `/`, + }); + expect(screen.getByTestId('no-breadcrumbs')).to.exist; + }); + + it('does not render breadcrumbs when downtime and not at the landing page', () => { + const screen = renderWithStoreAndRouter(, { + initialState: { + featureToggles: { + // eslint-disable-next-line camelcase + mhv_medical_records_to_va_gov_release: true, + }, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: downtime(['mhv_mr']), + dismissedDowntimeWarnings: [], + }, + ...initialState, + }, + reducers: reducer, + path: `/vaccines`, + }); + expect(screen.queryByTestId('no-breadcrumbs')).to.not.exist; + }); + describe('Side Nav feature flag functionality', () => { it('feature flag set to false', () => { const screen = renderWithStoreAndRouter( diff --git a/src/applications/mhv/medical-records/tests/containers/LandingPage.unit.spec.jsx b/src/applications/mhv/medical-records/tests/containers/LandingPage.unit.spec.jsx index e279d7d55040..883b6357a1a5 100644 --- a/src/applications/mhv/medical-records/tests/containers/LandingPage.unit.spec.jsx +++ b/src/applications/mhv/medical-records/tests/containers/LandingPage.unit.spec.jsx @@ -1,17 +1,79 @@ import { expect } from 'chai'; import React from 'react'; import { renderWithStoreAndRouter } from '@department-of-veterans-affairs/platform-testing/react-testing-library-helpers'; -import { beforeEach } from 'mocha'; +import { createServiceMap } from '@department-of-veterans-affairs/platform-monitoring'; +import { addHours, format } from 'date-fns'; import LandingPage from '../../containers/LandingPage'; +import reducer from '../../reducers'; describe('Landing Page', () => { - let screen; + const initialState = { + user: { + login: { + currentlyLoggedIn: true, + }, + profile: { + // services: [backendServices.HEALTH_RECORDS], + }, + }, + mr: { + breadcrumbs: { + list: [], + }, + }, + }; - beforeEach(() => { - screen = renderWithStoreAndRouter(, {}); - }); + const downtimeApproaching = maintenanceWindows => { + return createServiceMap( + maintenanceWindows.map(maintenanceWindow => { + return { + attributes: { + externalService: maintenanceWindow, + status: 'downtimeApproaching', + startTime: format(addHours(new Date(), 1), "yyyy-LL-dd'T'HH:mm:ss"), + endTime: format(addHours(new Date(), 3), "yyyy-LL-dd'T'HH:mm:ss"), + }, + }; + }), + ); + }; it('renders without errors', () => { + const screen = renderWithStoreAndRouter(, {}); expect(screen).to.exist; }); + + it('displays downtimeNotification when downtimeApproaching is true', () => { + const customState = { + featureToggles: {}, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: downtimeApproaching(['mhv_mr']), + dismissedDowntimeWarnings: [], + }, + ...initialState, + }; + + const screen = renderWithStoreAndRouter(, { + initialState: customState, + reducers: reducer, + }); + + expect( + screen.getByText('Upcoming maintenance on My HealtheVet', { + selector: 'h3', + exact: true, + }), + ); + expect( + screen.getByText( + 'We’ll be working on My HealtheVet soon. The maintenance will last 2 hours', + { + exact: false, + }, + ), + ); + }); }); diff --git a/src/applications/mhv/medical-records/tests/e2e/fixtures/non_mr_user.json b/src/applications/mhv/medical-records/tests/e2e/fixtures/non_mr_user.json new file mode 100644 index 000000000000..dbd7b18594fa --- /dev/null +++ b/src/applications/mhv/medical-records/tests/e2e/fixtures/non_mr_user.json @@ -0,0 +1,140 @@ +{ + "data": { + "id": "", + "type": "users_scaffolds", + "attributes": { + "services": [ + "facilities", + "hca", + "edu-benefits", + "form-save-in-progress", + "form-prefill", + "rx", + "messaging", + "add-person", + "user-profile", + "appeals-status", + "identity-proofed", + "vet360" + ], + "account": { + "accountUuid": "5180b4d9-4dbb-49fc-b5aa-86802f6b7044" + }, + "profile": { + "email": "test@test123.com", + "firstName": "Safari", + "middleName": null, + "lastName": "Mhvtp", + "birthDate": "2002-04-26", + "gender": "M", + "zip": null, + "lastSignedIn": "2023-06-22T19:16:07.273Z", + "loa": { + "current": 3, + "highest": 3 + }, + "multifactor": true, + "verified": true, + "signIn": { + "serviceName": "mhv", + "authBroker": "sis", + "clientId": "vaweb" + }, + "authnContext": "myhealthevet_loa3", + "inheritedProofVerified": false, + "claims": { + "appeals": true, + "ch33BankAccounts": false, + "coe": true, + "communicationPreferences": true, + "connectedApps": true, + "medicalCopays": true, + "militaryHistory": true, + "paymentHistory": false, + "personalInformation": true, + "ratingInfo": false + } + }, + "vaProfile": { + "status": "OK", + "birthDate": "20020426", + "familyName": "Mhvtp", + "gender": "M", + "givenNames": [ + "Safari" + ], + "isCernerPatient": false, + "facilities": [ + { + "facilityId": "979", + "isCerner": false + } + ], + "vaPatient": true, + "mhvAccountState": "MULTIPLE" + }, + "veteranStatus": null, + "inProgressForms": [], + "prefillsAvailable": [ + "21-686C", + "40-10007", + "0873", + "22-1990", + "22-1990N", + "22-1990E", + "22-1990EMEB", + "22-1995", + "22-5490", + "22-5490E", + "22-5495", + "22-0993", + "22-0994", + "FEEDBACK-TOOL", + "22-10203", + "22-1990S", + "22-1990EZ", + "21-526EZ", + "1010ez", + "21P-530", + "21P-527EZ", + "686C-674", + "20-0995", + "20-0996", + "10182", + "MDOT", + "5655", + "28-8832", + "28-1900", + "26-1880", + "26-4555" + ], + "vet360ContactInformation": { + "email": null, + "residentialAddress": null, + "mailingAddress": null, + "mobilePhone": null, + "homePhone": null, + "workPhone": null, + "temporaryPhone": null, + "faxNumber": null, + "textPermission": null + }, + "session": { + "authBroker": "sis", + "ssoe": false, + "transactionid": null + } + } + }, + "meta": { + "errors": [ + { + "externalService": "EMIS", + "startTime": "2023-06-22T19:17:15Z", + "endTime": null, + "description": "EMISRedis::VeteranStatus::RecordNotFound, NOT_FOUND", + "status": 404 + } + ] + } +} \ No newline at end of file diff --git a/src/applications/mhv/medical-records/tests/e2e/medical-records-page-not-found.cypress.spec.js b/src/applications/mhv/medical-records/tests/e2e/medical-records-page-not-found.cypress.spec.js new file mode 100644 index 000000000000..3f34b2e76d76 --- /dev/null +++ b/src/applications/mhv/medical-records/tests/e2e/medical-records-page-not-found.cypress.spec.js @@ -0,0 +1,15 @@ +import { notFoundHeading } from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; +import { rootUrl } from '../../manifest.json'; + +describe('Page Not Found', () => { + it('Visit an unsupported URL and get a page not found', () => { + cy.visit(`${rootUrl}/path1`); + cy.injectAxeThenAxeCheck(); + cy.findByRole('heading', { name: notFoundHeading }).should.exist; + cy.get('[data-testid="mhv-mr-navigation"]').should('not.exist'); + + cy.visit(`${rootUrl}/path1/path2`); + cy.findByRole('heading', { name: notFoundHeading }).should.exist; + cy.get('[data-testid="mhv-mr-navigation"]').should('not.exist'); + }); +}); diff --git a/src/applications/mhv/medical-records/tests/e2e/medical-records-unauthenticated-landing-page.cypress.spec.js b/src/applications/mhv/medical-records/tests/e2e/medical-records-unauthenticated-landing-page.cypress.spec.js new file mode 100644 index 000000000000..d36a3a09335a --- /dev/null +++ b/src/applications/mhv/medical-records/tests/e2e/medical-records-unauthenticated-landing-page.cypress.spec.js @@ -0,0 +1,23 @@ +import MedicalRecordsSite from './mr_site/MedicalRecordsSite'; +// import VitalsDetailsPage from './pages/VitalsDetailsPage'; + +describe('Medical Records Unauthenticated Users', () => { + it('Visits Medical Records Unauthenticated Users', () => { + const site = new MedicalRecordsSite(); + // Unauthenticated users implement visibility restrictions + site.login(false); + site.loadPageUnauthenticated(); + cy.url().should('contain', 'my-health/medical-records'); + + // Authenticated users + /* + site.login(); + cy.visit('my-health/medical-records/vitals'); + // Verify "Vitals" Page title Text + VitalsDetailsPage.verifyVitalsPageText('Vitals'); +*/ + // Axe check + cy.injectAxe(); + cy.axeCheck('main'); + }); +}); diff --git a/src/applications/mhv/medical-records/tests/e2e/mr_site/MedicalRecordsSite.js b/src/applications/mhv/medical-records/tests/e2e/mr_site/MedicalRecordsSite.js index 3c6ea35e18f8..fbd972c995ed 100644 --- a/src/applications/mhv/medical-records/tests/e2e/mr_site/MedicalRecordsSite.js +++ b/src/applications/mhv/medical-records/tests/e2e/mr_site/MedicalRecordsSite.js @@ -1,4 +1,5 @@ import mockUser from '../fixtures/user.json'; +import mockNonMRuser from '../fixtures/non_mr_user.json'; class MedicalRecordsSite { login = (isMRUser = true) => { @@ -69,6 +70,41 @@ class MedicalRecordsSite { ], }, }).as('featureToggle'); + } else { + cy.login(); + window.localStorage.setItem('isLoggedIn', true); + cy.intercept('GET', '/v0/user', mockNonMRuser).as('mockUser'); + cy.intercept('GET', '/v0/feature_toggles?*', { + data: { + type: 'feature_toggles', + features: [ + { + name: 'mhvMedicalRecordsToVAGovRelease', + value: true, + }, + { + name: 'mhv_medical_records_to_va_gov_release', + value: true, + }, + { + name: 'mhvMedicalRecordsDisplayDomains', + value: true, + }, + { + name: 'mhv_medical_records_display_domains', + value: true, + }, + { + name: 'mhv_medical_records_allow_txt_downloads', + value: true, + }, + { + name: 'mhv_medical_records_display_vaccines', + value: true, + }, + ], + }, + }).as('featureToggle'); } }; @@ -126,5 +162,10 @@ class MedicalRecordsSite { } }); }; + + loadPageUnauthenticated = () => { + cy.visit('my-health/medical-records'); + // cy.wait('@mockUser'); + }; } export default MedicalRecordsSite; diff --git a/src/applications/mhv/medical-records/tests/reducers/alerts.unit.spec.js b/src/applications/mhv/medical-records/tests/reducers/alerts.unit.spec.js new file mode 100644 index 000000000000..c0cf721ca5c2 --- /dev/null +++ b/src/applications/mhv/medical-records/tests/reducers/alerts.unit.spec.js @@ -0,0 +1,56 @@ +import { expect } from 'chai'; +import { alertsReducer } from '../../reducers/alerts'; +import * as Constants from '../../util/constants'; +import { Actions } from '../../util/actionTypes'; + +describe('alertsReducer function', () => { + const prevState = { + alertList: [ + { + datestamp: new Date(), + isActive: true, + type: 'error', + errorMessage: 'An error', + errorStackTrace: + 'Error: An error\n' + + ' at Context. (/Users/matthewwright/Projects/va/vets-website/src/applications/mhv/medical-records/tests/reducers/alerts.unit.spec.js:13:16)\n' + + ' at callFn (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runnable.js:366:21)\n' + + ' at Test.Runnable.run (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runnable.js:354:5)\n' + + ' at Test.Runnable.run (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha-snapshots/src/index.js:19:22)\n' + + ' at Runner.runTest (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:666:10)\n' + + ' at /Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:789:12\n' + + ' at next (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:581:14)\n' + + ' at /Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:591:7\n' + + ' at next (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:474:14)\n' + + ' at Immediate._onImmediate (/Users/matthewwright/Projects/va/vets-website/node_modules/mocha/lib/runner.js:559:5)\n' + + ' at processImmediate (internal/timers.js:461:21)\n' + + ' at process.callbackTrampoline (internal/async_hooks.js:129:14)', + }, + ], + }; + + it('should return initial state with new alert when case is ADD_ALERT', () => { + const action = { + type: Actions.Alerts.ADD_ALERT, + payload: { + type: Constants.ALERT_TYPE_ERROR, + error: new Error('Another error'), + }, + }; + const state = alertsReducer(prevState, action); + const activeError = state.alertList[state.alertList.length - 1]; + expect(activeError.datestamp).to.exist; + expect(activeError.isActive).to.eq(true); + expect(activeError.type).to.eq('error'); + expect(activeError.errorMessage).to.eq('Another error'); + expect(activeError.errorStackTrace.slice(0, 21)).to.eq( + 'Error: Another error\n', + ); + }); + + it('should set all alerts to inactive when case is CLEAR_ALERT', () => { + const action = { type: Actions.Alerts.CLEAR_ALERT }; + const state = alertsReducer(prevState, action); + expect(state.alertList.every(item => item.isActive === false)).to.be.true; + }); +}); diff --git a/src/applications/mhv/medical-records/util/actionTypes.js b/src/applications/mhv/medical-records/util/actionTypes.js index 32ab98f2335b..53b94e684331 100644 --- a/src/applications/mhv/medical-records/util/actionTypes.js +++ b/src/applications/mhv/medical-records/util/actionTypes.js @@ -1,6 +1,7 @@ export const Actions = { Alerts: { ADD_ALERT: 'MR_ALERT_ADD_ALERT', + CLEAR_ALERT: 'MR_ALERT_CLEAR', }, Breadcrumbs: { SET_BREAD_CRUMBS: 'MR_SET_BREAD_CRUMBS', diff --git a/src/applications/mhv/medications/api/rxApi.js b/src/applications/mhv/medications/api/rxApi.js index 7f9cb8857f26..512fd5766a89 100644 --- a/src/applications/mhv/medications/api/rxApi.js +++ b/src/applications/mhv/medications/api/rxApi.js @@ -17,6 +17,17 @@ export const getPaginatedSortedList = (pageNumber = 1, sortEndpoint = '') => { ); }; +export const getRefillablePrescriptionList = () => { + return apiRequest( + `${apiBasePath}/prescriptions/list_refillable_prescriptions`, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); +}; + export const getPrescriptionImage = cmopNdcNumber => { return apiRequest( `${apiBasePath}/prescriptions/get_prescription_image/${cmopNdcNumber}`, @@ -59,3 +70,15 @@ export const fillRx = id => { }, }); }; + +export const fillRxs = ids => { + const idParams = ids.map(id => `ids[]=${id}`).join('&'); + const url = `${apiBasePath}/prescriptions/refill_prescriptions?${idParams}`; + const requestOptions = { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + }; + return apiRequest(url, requestOptions); +}; diff --git a/src/applications/mhv/medications/components/RefillPrescriptions/RenewablePrescriptions.jsx b/src/applications/mhv/medications/components/RefillPrescriptions/RenewablePrescriptions.jsx new file mode 100644 index 000000000000..bb1c1c99a80f --- /dev/null +++ b/src/applications/mhv/medications/components/RefillPrescriptions/RenewablePrescriptions.jsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { setBreadcrumbs } from '../../actions/breadcrumbs'; +import { setPrescriptionDetails } from '../../actions/prescriptions'; + +import { dateFormat } from '../../util/helpers'; + +const RenewablePrescriptions = ({ renewablePrescriptionsList }) => { + // Hooks + const dispatch = useDispatch(); + + // Pagination + const MAX_PAGE_LIST_LENGTH = 20; + const [pagination, setPagination] = useState({ + currentPage: 1, + totalPages: Math.ceil( + renewablePrescriptionsList.length / MAX_PAGE_LIST_LENGTH, + ), + }); + + const onPageChange = page => { + setPagination(prevState => ({ + ...prevState, + currentPage: page, + })); + }; + + const startIdx = (pagination.currentPage - 1) * MAX_PAGE_LIST_LENGTH; + const endIdx = pagination.currentPage * MAX_PAGE_LIST_LENGTH; + const paginatedRenewablePrescriptions = renewablePrescriptionsList.slice( + startIdx, + endIdx, + ); + + // Functions + const onRxLinkClick = rx => { + dispatch( + setBreadcrumbs( + [ + { + url: '/my-health/medications/about', + label: 'About medications', + }, + { + url: `/my-health/medications`, + label: 'Medications', + }, + ], + { + url: `/my-health/medications/prescription/${rx.prescriptionId}`, + label: rx?.prescriptionName, + }, + ), + ); + dispatch(setPrescriptionDetails(rx)); + }; + + return ( +
    +

    + If your prescription isn’t ready to refill +

    +

    + You may need to renew it. Here are some recent prescriptions you may + need to renew.{' '} + +

    +

    + Note: If your prescriptions isn’t in this list, find it + in your medications list.{' '} + + Go to your medications list + +

    +

    + Showing {renewablePrescriptionsList.length} prescription + {renewablePrescriptionsList.length !== 1 ? 's' : ''} you may need to + renew +

    +
    +
    + {paginatedRenewablePrescriptions.map((prescription, idx) => ( +
    +

    + onRxLinkClick(prescription)} + > + {prescription.prescriptionName} + +

    +

    + Prescription number: {prescription.prescriptionNumber} +
    + + Last filled on:{' '} + {dateFormat( + prescription.rxRfRecords?.[0]?.[1]?.find( + record => record.dispensedDate, + )?.dispensedDate || prescription.dispensedDate, + 'MMMM D, YYYY', + )} + +

    +
    + ))} +
    + {renewablePrescriptionsList.length > MAX_PAGE_LIST_LENGTH && ( + onPageChange(e.detail.page)} + page={pagination.currentPage} + pages={pagination.totalPages} + unbounded + uswds + data-testid="renew-pagination" + /> + )} +
    +
    +
    + ); +}; + +RenewablePrescriptions.propTypes = { + renewablePrescriptionsList: PropTypes.array.isRequired, +}; + +export default RenewablePrescriptions; diff --git a/src/applications/mhv/medications/containers/RefillPrescriptions.jsx b/src/applications/mhv/medications/containers/RefillPrescriptions.jsx new file mode 100644 index 000000000000..6bf1b0df54e4 --- /dev/null +++ b/src/applications/mhv/medications/containers/RefillPrescriptions.jsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { VaButton } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import PropTypes from 'prop-types'; +import { updatePageTitle } from '../../shared/util/helpers'; +import { dateFormat } from '../util/helpers'; +import { getRefillablePrescriptionList, fillRxs } from '../api/rxApi'; +import { selectRefillContentFlag } from '../util/selectors'; +import { setBreadcrumbs } from '../actions/breadcrumbs'; +import RenewablePrescriptions from '../components/RefillPrescriptions/RenewablePrescriptions'; + +const RefillPrescriptions = ({ refillList = [], isLoadingList = true }) => { + // Hooks + const location = useLocation(); + const dispatch = useDispatch(); + + // State + const [isLoading, updateLoadingStatus] = useState(isLoadingList); + const [selectedRefillList, setSelectedRefillList] = useState([]); + const [fullRefillList, setFullRefillList] = useState(refillList); + + // Selectors + const selectedSortOption = useSelector( + state => state.rx.prescriptions?.selectedSortOption, + ); + const showRefillContent = useSelector(selectRefillContentFlag); + + // Memoized Values + const selectedRefillListLength = useMemo(() => selectedRefillList.length, [ + selectedRefillList, + ]); + const renewablePrescriptionsList = useMemo( + () => + fullRefillList.filter( + rx => rx.dispStatus === 'Active' && rx.refillRemaining === 0, + ), + [fullRefillList], + ); + + // Functions + const onRequestRefills = () => { + if (selectedRefillListLength > 0) { + fillRxs(selectedRefillList); + } + }; + const onSelectPrescription = id => { + if (!selectedRefillList.includes(id)) { + setSelectedRefillList([...selectedRefillList, id]); + } else { + setSelectedRefillList(selectedRefillList.filter(item => item !== id)); + } + }; + const onSelectAll = () => { + if (selectedRefillListLength !== fullRefillList.length) { + setSelectedRefillList(fullRefillList.map(p => p.prescriptionId)); + } + }; + + useEffect( + () => { + if (!fullRefillList) { + updateLoadingStatus(true); + } + getRefillablePrescriptionList().then(({ data }) => { + setFullRefillList( + data + .map(({ attributes }) => attributes) + .sort((a, b) => + a.prescriptionName.localeCompare(b.prescriptionName), + ), + ); + updateLoadingStatus(false); + }); + dispatch( + setBreadcrumbs( + [ + { + url: `/my-health/medications/1`, + label: 'Medications', + }, + ], + { + url: `/my-health/medications/refill/`, + label: 'Refill', + }, + ), + ); + updatePageTitle('Refill prescriptions - Medications | Veterans Affairs'); + }, + // disabled warning: fullRefillList must be left of out dependency array to avoid infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + [dispatch, location.pathname, selectedSortOption], + ); + + const content = () => { + if (!showRefillContent || isLoading) { + return ( +
    + +
    + ); + } + return ( +
    + + + Back to Medications + + +
    +

    + Refill prescriptions +

    +

    + Ready to refill +

    +

    + You have {fullRefillList.length}{' '} + {`prescription${fullRefillList.length !== 1 ? 's' : ''}`} ready to + refill. +

    + onSelectAll()} + text="Select all" + /> +
    + {fullRefillList + .slice() + .filter(rx => rx.refillRemaining > 0) + .map((prescription, idx) => ( +
    + + e.type === 'change' && + onSelectPrescription(prescription.prescriptionId) + } + /> + +

    + Prescription number: {prescription.prescriptionNumber} +
    + + Last filled on:{' '} + {dateFormat( + prescription.rxRfRecords?.[0]?.[1]?.find( + record => record.dispensedDate, + )?.dispensedDate || prescription.dispensedDate, + 'MMMM D, YYYY', + )} + +
    + {prescription.refillRemaining} refills left +

    +
    + ))} +
    + onRequestRefills()} + text={`Request refill${selectedRefillListLength !== 1 ? 's' : ''}`} + /> +
    + +
    + ); + }; + + return <>{content()}; +}; + +// These have been added for testing purposes only +// While the list and loading status is being determined locally +RefillPrescriptions.propTypes = { + isLoadingList: PropTypes.bool, + refillList: PropTypes.array, +}; + +export default RefillPrescriptions; diff --git a/src/applications/mhv/medications/containers/RxBreadcrumbs.jsx b/src/applications/mhv/medications/containers/RxBreadcrumbs.jsx index 78c598286511..0e6e98c24301 100644 --- a/src/applications/mhv/medications/containers/RxBreadcrumbs.jsx +++ b/src/applications/mhv/medications/containers/RxBreadcrumbs.jsx @@ -19,6 +19,7 @@ const RxBreadcrumbs = () => { return ( <> {!medicationsUrls.MEDICATIONS_ABOUT.endsWith(location.pathname) && + !medicationsUrls.MEDICATIONS_REFILL.endsWith(location.pathname) && allCrumbs.length > 0 && allCrumbs[0]?.url && (
    diff --git a/src/applications/mhv/medications/routes.jsx b/src/applications/mhv/medications/routes.jsx index f6523ab67c7d..dde15e9a1e17 100644 --- a/src/applications/mhv/medications/routes.jsx +++ b/src/applications/mhv/medications/routes.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Switch, Route } from 'react-router-dom'; +import PropTypes from 'prop-types'; import PageNotFound from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; import App from './containers/App'; import PrescriptionDetails from './containers/PrescriptionDetails'; @@ -7,39 +8,61 @@ import RxBreadcrumbs from './containers/RxBreadcrumbs'; import Prescriptions from './containers/Prescriptions'; import LandingPage from './containers/LandingPage'; import PrescriptionsPrintOnly from './containers/PrescriptionsPrintOnly'; +import RefillPrescriptions from './containers/RefillPrescriptions'; -const routes = ( -
    -
    +/** + * Route that wraps its children within the application component. + */ +const AppRoute = ({ children, ...rest }) => { + return ( + -
    - - - - - -
    - -
    - -
    -
    -
    - - - - - - -
    -
    +
    {children}
    +
    + ); +}; + +AppRoute.propTypes = { + children: PropTypes.object, +}; + +const routes = ( +
    +
    + + + + + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    +
    +
    + + + + + + +
    ); diff --git a/src/applications/mhv/medications/sass/medications.scss b/src/applications/mhv/medications/sass/medications.scss index e0855003621f..688dbe623f68 100644 --- a/src/applications/mhv/medications/sass/medications.scss +++ b/src/applications/mhv/medications/sass/medications.scss @@ -18,6 +18,31 @@ .link-button:hover { background-color: rgba(0, 0, 0, 0.05); } +.refill-back-link { + margin-top: 50px; + display: block; + margin-bottom: 16px; + margin-left: 13px; + width: fit-content; +} +.refill-back-arrow { + &:before { + content: "\f053"; + font-family: "Font Awesome 5 Free"; + font-weight: bold; + font-size: 12px; + position: absolute; + top: 5px; + } +} +.refill-loading-indicator { + margin-top: 70px; +} +.renew-pagination-container { + width: 100%; + display: flex; + justify-content: center; +} .main-content { padding: 0; margin-bottom: 40px; diff --git a/src/applications/mhv/medications/tests/components/RefillPrescriptions/RenewablePrescriptions.unit.spec.jsx b/src/applications/mhv/medications/tests/components/RefillPrescriptions/RenewablePrescriptions.unit.spec.jsx new file mode 100644 index 000000000000..790ddc2a1b67 --- /dev/null +++ b/src/applications/mhv/medications/tests/components/RefillPrescriptions/RenewablePrescriptions.unit.spec.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { renderWithStoreAndRouter } from '@department-of-veterans-affairs/platform-testing/react-testing-library-helpers'; +import { expect } from 'chai'; +import { fireEvent } from '@testing-library/react'; +import { $ } from '@department-of-veterans-affairs/platform-forms-system/ui'; +import RenewablePrescriptions from '../../../components/RefillPrescriptions/RenewablePrescriptions'; +import reducer from '../../../reducers'; +import prescriptions from '../../fixtures/refillablePrescriptionsList.json'; +import { dateFormat } from '../../../util/helpers'; + +describe('Renew Prescriptions Component', () => { + const initialState = { + featureToggles: { + // eslint-disable-next-line camelcase + mhv_medications_display_refill_content: true, + }, + user: { + login: { + currentlyLoggedIn: true, + }, + }, + }; + + const setup = (state = initialState, list = prescriptions) => { + const rxList = list + .slice() + .filter(rx => rx.dispStatus === 'Active' && rx.refillRemaining === 0) + .sort((a, b) => a.prescriptionName.localeCompare(b.prescriptionName)); + return renderWithStoreAndRouter( + , + { + initialState: state, + reducers: reducer, + path: '/refill', + }, + ); + }; + + it('renders without errors', () => { + const screen = setup(); + expect(screen); + }); + + it('Clicks the medications list page link', async () => { + const screen = setup(); + const link = await screen.findByTestId('medications-page-link'); + fireEvent.click(link); + }); + + it('Clicks the details page link', async () => { + const screen = setup(); + const link = await screen.findByTestId('medication-details-page-link-0'); + fireEvent.click(link); + }); + + it('Clicks the details page link', async () => { + const screen = setup(); + const link = await screen.findByTestId('medication-details-page-link-0'); + fireEvent.click(link); + }); + + it('Clicks the medications list page link', async () => { + const screen = setup(); + const link = await screen.findByTestId('medications-page-link'); + fireEvent.click(link); + }); + + it('Shows the correct last filled on date for renew', async () => { + const screen = setup(); + const lastFilledEl = await screen.findByTestId('renew-last-filled-1'); + expect(lastFilledEl).to.exist; + const rx = prescriptions + .slice() + .sort((a, b) => a.prescriptionName.localeCompare(b.prescriptionName)) + .find( + ({ refillRemaining, dispStatus, dispensedDate }) => + dispStatus === 'Active' && + refillRemaining === 0 && + dispensedDate !== null, + ); + expect(lastFilledEl).to.have.text( + `Last filled on: ${dateFormat(rx.dispensedDate)}`, + ); + }); + + it('Shows the correct "last filled on" date (w/rxRfRecords) for renew', async () => { + const screen = setup(); + const lastFilledEl = await screen.findByTestId(`renew-last-filled-0`); + screen.debug(); + expect(lastFilledEl).to.exist; + const rx = prescriptions.find( + ({ prescriptionId }) => prescriptionId === 22217089, + ); + expect(lastFilledEl).to.have.text( + `Last filled on: ${dateFormat(rx.rxRfRecords[0][1][0].dispensedDate)}`, + ); + }); + + it('Shows the correct text for 1 prescription', async () => { + const screen = setup(initialState, [prescriptions[1]]); + const countEl = await screen.findByTestId('renew-page-list-count'); + expect(countEl).to.exist; + expect(countEl).to.have.text( + 'Showing 1 prescription you may need to renew', + ); + }); + + it('Shows the correct text for 2 prescriptions', async () => { + const screen = setup(initialState, [prescriptions[1], prescriptions[1]]); + const countEl = await screen.findByTestId('renew-page-list-count'); + expect(countEl).to.exist; + expect(countEl).to.have.text( + 'Showing 2 prescriptions you may need to renew', + ); + }); + + it('Simulates page change in VaPagination to page 2', async () => { + const screen = setup(undefined, new Array(25).fill(prescriptions[1])); + const pagination = await $('va-pagination', screen.container); + const event = new CustomEvent('pageSelect', { + bubbles: true, + detail: { page: 2 }, + }); + pagination.dispatchEvent(event); + }); +}); diff --git a/src/applications/mhv/medications/tests/containers/RefillPrescriptions.unit.spec.jsx b/src/applications/mhv/medications/tests/containers/RefillPrescriptions.unit.spec.jsx new file mode 100644 index 000000000000..eebceff88f19 --- /dev/null +++ b/src/applications/mhv/medications/tests/containers/RefillPrescriptions.unit.spec.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { renderWithStoreAndRouter } from '@department-of-veterans-affairs/platform-testing/react-testing-library-helpers'; +import { expect } from 'chai'; +import { fireEvent, waitFor } from '@testing-library/react'; +import RefillPrescriptions from '../../containers/RefillPrescriptions'; +import reducer from '../../reducers'; +import prescriptions from '../fixtures/refillablePrescriptionsList.json'; +import { dateFormat } from '../../util/helpers'; + +describe('Refill Prescriptions Component', () => { + const initialState = { + featureToggles: { + // eslint-disable-next-line camelcase + mhv_medications_display_refill_content: true, + }, + user: { + login: { + currentlyLoggedIn: true, + }, + }, + }; + + const setup = ( + state = initialState, + list = prescriptions, + isLoadingList = false, + ) => { + return renderWithStoreAndRouter( + , + { + initialState: state, + reducers: reducer, + path: '/refill', + }, + ); + }; + + it('renders without errors', () => { + const screen = setup(); + expect(screen); + }); + + it('should render loading state', async () => { + const screen = setup(initialState, [], true); + waitFor(() => { + expect(screen.findByTestId('loading-indicator')).to.exist; + expect(screen.findByText('Loading')).to.exist; + }); + }); + + it('Shows h1 and h2', async () => { + const screen = setup(); + const title = await screen.findByTestId('refill-page-title'); + expect(title).to.exist; + expect(title).to.have.text('Refill prescriptions'); + const subtitle = await screen.findByTestId('refill-page-subtitle'); + expect(subtitle).to.exist; + expect(subtitle).to.have.text('Ready to refill'); + }); + + it('Shows the request refill button', async () => { + const screen = setup(); + const button = await screen.findByTestId('request-refill-button'); + expect(button).to.exist; + expect(button).to.have.property('text', 'Request refills'); + button.click(); + }); + + it('Shows the select all button', async () => { + const screen = setup(); + const button = await screen.findByTestId('select-all-button'); + expect(button).to.exist; + expect(button).to.have.property('text', 'Select all'); + button.click(); + }); + + it('Clicks the medications list page link', async () => { + const screen = setup(); + const link = await screen.findByTestId('back-to-medications-page-link'); + fireEvent.click(link); + }); + + it('Shows the correct "last filled on" date for refill', async () => { + const screen = setup(); + const lastFilledEl = await screen.findByTestId('refill-last-filled-0'); + expect(lastFilledEl).to.exist; + expect(lastFilledEl).to.have.text( + `Last filled on: ${dateFormat(prescriptions[0].dispensedDate)}`, + ); + }); + + it('Shows the correct "last filled on" date (w/rxRfRecords) for refill', async () => { + const screen = setup(); + const lastFilledEl = await screen.findByTestId(`refill-last-filled-5`); + expect(lastFilledEl).to.exist; + const rx = prescriptions.find( + ({ prescriptionId }) => prescriptionId === 22217099, + ); + expect(lastFilledEl).to.have.text( + `Last filled on: ${dateFormat(rx.rxRfRecords[0][1][0].dispensedDate)}`, + ); + }); + + it('Checks the checkbox for first prescription', async () => { + const screen = setup(); + const checkbox = await screen.findByTestId( + 'refill-prescription-checkbox-0', + ); + checkbox.click(); + }); + + it('Unchecks the checkbox for first prescription', async () => { + const screen = setup(); + const checkbox = await screen.findByTestId( + 'refill-prescription-checkbox-0', + ); + checkbox.click(); + checkbox.click(); + }); + + it('Shows the correct text for one prescription', async () => { + const screen = setup(initialState, [prescriptions[0]]); + const countEl = await screen.findByTestId('refill-page-list-count'); + expect(countEl).to.exist; + expect(countEl).to.have.text('You have 1 prescription ready to refill.'); + }); + + it('Completes api request with selected prescriptions', async () => { + const screen = setup(); + const checkbox = await screen.findByTestId( + 'refill-prescription-checkbox-0', + ); + checkbox.click(); + const button = await screen.findByTestId('request-refill-button'); + button.click(); + }); +}); diff --git a/src/applications/mhv/medications/tests/e2e/medications-landing-page.cypress.spec.js b/src/applications/mhv/medications/tests/e2e/medications-landing-page.cypress.spec.js index 2cd85d20ad73..bb20330b6f84 100644 --- a/src/applications/mhv/medications/tests/e2e/medications-landing-page.cypress.spec.js +++ b/src/applications/mhv/medications/tests/e2e/medications-landing-page.cypress.spec.js @@ -1,5 +1,7 @@ +import { notFoundHeading } from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; import MedicationsSite from './med_site/MedicationsSite'; import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import { rootUrl } from '../../manifest.json'; describe('Medications Landing Page', () => { it('visits Medications landing Page', () => { @@ -11,4 +13,10 @@ describe('Medications Landing Page', () => { cy.injectAxe(); cy.axeCheck('main'); }); + + it('Visit unsupported URL and get a page not found', () => { + cy.visit(`${rootUrl}/dummy/dummy`); + cy.injectAxeThenAxeCheck(); + cy.findByRole('heading', { name: notFoundHeading }).should.exist; + }); }); diff --git a/src/applications/mhv/medications/tests/fixtures/refillablePrescriptionsList.json b/src/applications/mhv/medications/tests/fixtures/refillablePrescriptionsList.json new file mode 100644 index 000000000000..a22c005f6152 --- /dev/null +++ b/src/applications/mhv/medications/tests/fixtures/refillablePrescriptionsList.json @@ -0,0 +1,438 @@ +[ + { + + "prescriptionId": 22377956, + "prescriptionNumber": "2720554", + "prescriptionName": "MELOXICAM 15MG TAB", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-13T04:00:00.000Z", + "refillRemaining": 5, + "facilityName": "DAYT29", + "orderedDate": "2023-07-12T04:00:00.000Z", + "quantity": 30, + "expirationDate": "2024-07-12T04:00:00.000Z", + "dispensedDate": "Sat, 22 Oct 2022 00:00:00 EDT", + "stationNumber": "989", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "TAKE ONE TABLET BY MOUTH DAILY FOR 30 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "DAYTON", + "modifiedDate": "2023-08-11T15:56:58.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "00597-0030-01", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + + "prescriptionId": 22377955, + "prescriptionNumber": "2720553", + "prescriptionName": "MEGESTROL ACETATE 200MG/5ML ORAL SUSP", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-18T04:00:00.000Z", + "refillRemaining": 0, + "facilityName": "DAYT29", + "orderedDate": "2023-07-17T04:00:00.000Z", + "quantity": 1, + "expirationDate": "2024-07-17T04:00:00.000Z", + "dispensedDate": "2024-07-17T04:00:00.000Z", + "stationNumber": "989", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "TAKE 1 TEASPOONFUL DAILY FOR 30 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "DAYTON", + "modifiedDate": "2023-08-11T15:56:58.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "00015-0508-42", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + "prescriptionId": 22377954, + "prescriptionNumber": "2720552", + "prescriptionName": "MEFLOQUINE HCL 250MG TAB", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-08T04:00:00.000Z", + "refillRemaining": 5, + "facilityName": "DAYT29", + "orderedDate": "2023-07-07T04:00:00.000Z", + "quantity": 30, + "expirationDate": "2024-07-07T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "989", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "TAKE ONE TABLET BY MOUTH DAILY FOR 30 DAYS TEST INDI TEST PI", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": "TEST RE", + "divisionName": "DAYTON", + "modifiedDate": "2023-08-11T15:56:58.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "00054-0025-11", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + "prescriptionId": 22377951, + "prescriptionNumber": "3636956", + "prescriptionName": "LARYNG TUBE, CANNULA #8 LGT", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-18T04:00:00.000Z", + "refillRemaining": 5, + "facilityName": "SLC10 TEST LAB", + "orderedDate": "2023-07-17T04:00:00.000Z", + "quantity": 1, + "expirationDate": "2024-07-17T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "979", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "DEVICE(S) TRACH WEEKLY FOR 60 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "VAMC SLC-OUTPTRX", + "modifiedDate": "2023-08-11T15:56:57.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": null, + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + "prescriptionId": 22377950, + "prescriptionNumber": "3636955", + "prescriptionName": "LANOLIN ANHYDROUS OINT", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-08T04:00:00.000Z", + "refillRemaining": 3, + "facilityName": "SLC10 TEST LAB", + "orderedDate": "2023-07-07T04:00:00.000Z", + "quantity": 4, + "expirationDate": "2024-07-07T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "979", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "APPLY 10MG TO THE AFFECTED AREA EVERY DAY FOR 45 DAYS TEST IN TEST PI", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": "TestRE", + "divisionName": "VAMC SLC-OUTPTRX", + "modifiedDate": "2023-08-11T15:56:57.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "00168-0052-16", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + "prescriptionId": 22377949, + "prescriptionNumber": "3636954", + "prescriptionName": "LABETALOL HCL 300MG TAB", + "refillStatus": "active", + "refillSubmitDate": null, + "refillDate": "2023-07-13T04:00:00.000Z", + "refillRemaining": 4, + "facilityName": "SLC10 TEST LAB", + "orderedDate": "2023-07-12T04:00:00.000Z", + "quantity": 60, + "expirationDate": "2024-07-12T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "979", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "TAKE ONE TABLET BY MOUTH EVERY TWELVE (12) HOURS FOR 30 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "VAMC SLC-OUTPTRX", + "modifiedDate": "2023-08-11T15:56:56.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "00172-4366-60", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [], + "tracking": false + }, + { + "prescriptionId": 22217099, + "prescriptionNumber": "3636937", + "prescriptionName": "FEEDING BAG,1000ML KENDAL #8884773600", + "refillStatus": "expired", + "refillSubmitDate": "2023-06-21T19:18:00.000Z", + "refillDate": "2023-06-21T04:00:00.000Z", + "refillRemaining": 1, + "facilityName": "SLC10 TEST LAB", + "orderedDate": "2023-05-26T04:00:00.000Z", + "quantity": 1, + "expirationDate": "2023-06-25T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "979", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "1 MISCORAL WEEKLY FOR 30 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "VAMC SLC-OUTPTRX", + "modifiedDate": "2023-08-04T13:57:12.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "08884-7736-00", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [ + [ + "rf_record", + [ + { + "refillStatus": "suspended", + "refillSubmitDate": null, + "refillDate": "Sat, 15 Jul 2023 00:00:00 EDT", + "refillRemaining": 4, + "facilityName": "DAYT29", + "isRefillable": false, + "isTrackable": false, + "prescriptionId": 22332828, + "sig": null, + "orderedDate": "Fri, 04 Aug 2023 00:00:00 EDT", + "quantity": null, + "expirationDate": null, + "prescriptionNumber": "2720542", + "prescriptionName": "ONDANSETRON 8 MG TAB", + "dispensedDate": "2023-07-13T04:00:00.000Z", + "stationNumber": "989", + "inCernerTransition": false, + "notRefillableDisplayMessage": null, + "cmopDivisionPhone": null, + "cmopNdcNumber": null, + "id": 22332828, + "userId": 16955936, + "providerFirstName": null, + "providerLastName": null, + "remarks": null, + "divisionName": null, + "modifiedDate": null, + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Suspended", + "ndc": null, + "reason": null, + "prescriptionNumberIndex": "RF1", + "prescriptionSource": "RF", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": null, + "rxRfRecords": null, + "tracking": false + } + ] + ] + ], + "tracking": false + }, + { + "prescriptionId": 22217089, + "prescriptionNumber": "3636937", + "prescriptionName": "FEEDING BAG,1000ML KENDAL #8884773600", + "refillStatus": "expired", + "refillSubmitDate": "2023-06-21T19:18:00.000Z", + "refillDate": "2023-06-21T04:00:00.000Z", + "refillRemaining": 0, + "facilityName": "SLC10 TEST LAB", + "orderedDate": "2023-05-26T04:00:00.000Z", + "quantity": 1, + "expirationDate": "2023-06-25T04:00:00.000Z", + "dispensedDate": null, + "stationNumber": "979", + "isRefillable": true, + "isTrackable": false, + "cmopNdcNumber": null, + "inCernerTransition": false, + "notRefillableDisplayMessage": "A refill request cannot be submitted at this time. Please review the prescription status and fill date. If you need more of this medication, please call the pharmacy phone number on your prescription label.", + "sig": "1 MISCORAL WEEKLY FOR 30 DAYS", + "cmopDivisionPhone": null, + "userId": 16955936, + "providerFirstName": "MOHAMMAD", + "providerLastName": "ISLAM", + "remarks": null, + "divisionName": "VAMC SLC-OUTPTRX", + "modifiedDate": "2023-08-04T13:57:12.000Z", + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Active", + "ndc": "08884-7736-00", + "reason": null, + "prescriptionNumberIndex": "RX", + "prescriptionSource": "RX", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": [], + "rxRfRecords": [ + [ + "rf_record", + [ + { + "refillStatus": "suspended", + "refillSubmitDate": null, + "refillDate": "Sat, 15 Jul 2023 00:00:00 EDT", + "refillRemaining": 4, + "facilityName": "DAYT29", + "isRefillable": false, + "isTrackable": false, + "prescriptionId": 22332828, + "sig": null, + "orderedDate": "Fri, 04 Aug 2023 00:00:00 EDT", + "quantity": null, + "expirationDate": null, + "prescriptionNumber": "2720542", + "prescriptionName": "ONDANSETRON 8 MG TAB", + "dispensedDate": "2023-07-13T04:00:00.000Z", + "stationNumber": "989", + "inCernerTransition": false, + "notRefillableDisplayMessage": null, + "cmopDivisionPhone": null, + "cmopNdcNumber": null, + "id": 22332828, + "userId": 16955936, + "providerFirstName": null, + "providerLastName": null, + "remarks": null, + "divisionName": null, + "modifiedDate": null, + "institutionId": null, + "dialCmopDivisionPhone": "", + "dispStatus": "Suspended", + "ndc": null, + "reason": null, + "prescriptionNumberIndex": "RF1", + "prescriptionSource": "RF", + "disclaimer": null, + "indicationForUse": null, + "indicationForUseFlag": null, + "category": "Rx Medication", + "trackingList": null, + "rxRfRecords": null, + "tracking": false + } + ] + ] + ], + "tracking": false + } + ] \ No newline at end of file diff --git a/src/applications/mhv/medications/util/constants.js b/src/applications/mhv/medications/util/constants.js index aab074e0ffad..e9ccb0930db7 100644 --- a/src/applications/mhv/medications/util/constants.js +++ b/src/applications/mhv/medications/util/constants.js @@ -18,6 +18,7 @@ export const medicationsUrls = { MEDICATIONS_URL: '/my-health/medications', MEDICATIONS_LOGIN: '/my-health/medications?next=loginModal&oauth=true', MEDICATIONS_ABOUT: '/my-health/medications/about', + MEDICATIONS_REFILL: '/my-health/medications/refill', }; export const dispStatusForRefillsLeft = [ diff --git a/src/applications/mhv/medications/util/selectors.js b/src/applications/mhv/medications/util/selectors.js new file mode 100644 index 000000000000..0ba4d2035726 --- /dev/null +++ b/src/applications/mhv/medications/util/selectors.js @@ -0,0 +1,4 @@ +import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; + +export const selectRefillContentFlag = state => + state.featureToggles[FEATURE_FLAG_NAMES.mhvMedicationsDisplayRefillContent]; diff --git a/src/applications/mhv/secure-messaging/components/shared/SmBreadcrumbs.jsx b/src/applications/mhv/secure-messaging/components/shared/SmBreadcrumbs.jsx index 863e16ea4679..ad8c570ceccf 100644 --- a/src/applications/mhv/secure-messaging/components/shared/SmBreadcrumbs.jsx +++ b/src/applications/mhv/secure-messaging/components/shared/SmBreadcrumbs.jsx @@ -133,7 +133,7 @@ const SmBreadcrumbs = () => { !crumbs?.label ? 'breadcrumbs--hidden' : '' }`} > - + {crumbs && (
    • diff --git a/src/applications/mhv/secure-messaging/tests/e2e/fixtures/thread-response.json b/src/applications/mhv/secure-messaging/tests/e2e/fixtures/thread-response.json index ab21cee853ad..2d1cbf7d79b8 100644 --- a/src/applications/mhv/secure-messaging/tests/e2e/fixtures/thread-response.json +++ b/src/applications/mhv/secure-messaging/tests/e2e/fixtures/thread-response.json @@ -18,7 +18,7 @@ "triageGroupName": "Jeasmitha-Cardio-Clinic", "proxySenderName": null, "threadId": 7176615, - "folderId": 0, + "folderId": -1, "messageBody": "REPLY\r\nFROM ESCALATED FOLDER\r\n\r\n\r\n\r\nMohammad R Islam\r\nSenior Software Test Engineer", "draftDate": null, "toDate": null, @@ -46,7 +46,7 @@ "triageGroupName": "***Jeasmitha-Cardio-Clinic***", "proxySenderName": null, "threadId": 7176615, - "folderId": 0, + "folderId": -1, "messageBody": "Draft2", "draftDate": null, "toDate": null, diff --git a/src/applications/mhv/secure-messaging/tests/e2e/pages/PatientInboxPage.js b/src/applications/mhv/secure-messaging/tests/e2e/pages/PatientInboxPage.js index a0efb11d76ea..fe37c1613cee 100644 --- a/src/applications/mhv/secure-messaging/tests/e2e/pages/PatientInboxPage.js +++ b/src/applications/mhv/secure-messaging/tests/e2e/pages/PatientInboxPage.js @@ -179,6 +179,16 @@ class PatientInboxPage { { data: this.singleThread.data[0] }, ).as('fist-message-in-thread'); + if (this.singleThread.data.length > 1) { + cy.intercept( + 'GET', + `${Paths.SM_API_EXTENDED}/${ + this.singleThread.data[1].attributes.messageId + }`, + { data: this.singleThread.data[1] }, + ).as('second-message-in-thread'); + } + cy.contains(mockMessages.data[0].attributes.subject).click({ waitForAnimations: true, }); diff --git a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-compose.cypress.spec.js b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-compose.cypress.spec.js index 2062a0e5fb61..b0ed6239baef 100644 --- a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-compose.cypress.spec.js +++ b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-compose.cypress.spec.js @@ -31,7 +31,7 @@ describe('Secure Messaging Compose', () => { cy.axeCheck(AXE_CONTEXT); }); - it('verify subject field max size', () => { + it.skip('verify subject field max size', () => { const charsLimit = 50; const normalText = 'Qwerty1234'; const maxText = 'Qwerty1234Qwerty1234Qwerty1234Qwerty1234Qwerty1234'; diff --git a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-accordions.cypress.spec.js b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-accordions.cypress.spec.js new file mode 100644 index 000000000000..ef39f3192a2c --- /dev/null +++ b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-accordions.cypress.spec.js @@ -0,0 +1,34 @@ +import SecureMessagingSite from './sm_site/SecureMessagingSite'; +import PatientInboxPage from './pages/PatientInboxPage'; +import mockThreadResponse from './fixtures/thread-response.json'; +import { AXE_CONTEXT } from './utils/constants'; + +describe('Secure Messaging Thread Details', () => { + const landingPage = new PatientInboxPage(); + const site = new SecureMessagingSite(); + const date = new Date(); + const messageIdList = mockThreadResponse.data.map( + el => el.attributes.messageId, + ); + + it('verify expanded messages focus', () => { + site.login(); + landingPage.loadInboxMessages(); + landingPage.loadSingleThread(mockThreadResponse, date); + + for (let i = 1; i < messageIdList.length; i += 1) { + cy.get(`[data-testid="expand-message-button-${messageIdList[i]}"]`) + .shadow() + .find('[part="accordion-header"]') + .click(); + + cy.get(`[data-testid="expand-message-button-${messageIdList[i]}"]`) + .shadow() + .find('[part="accordion-header"]') + .should('have.attr', 'aria-expanded', 'true'); + } + + cy.injectAxe(); + cy.axeCheck(AXE_CONTEXT); + }); +}); diff --git a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details-expand-all.cypress.spec.js b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details-expand-all.cypress.spec.js index c23e1eba5758..2fac39cb9d61 100644 --- a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details-expand-all.cypress.spec.js +++ b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details-expand-all.cypress.spec.js @@ -34,8 +34,6 @@ describe('Secure Messaging Message Details', () => { detailsPage.verifyExpandedMessageToDisplay(mockParentMessageDetails, 0); detailsPage.expandAllThreadMessages(); - // there should only call to /messages/message/* - cy.wait('@message1'); detailsPage.verifyExpandedThreadBodyDisplay(defaultMockThread, 2); detailsPage.verifyExpandedThreadAttachmentDisplay(defaultMockThread, 2); diff --git a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details.cypress.spec.js b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details.cypress.spec.js index b70a03d96a05..b924e5e814c8 100644 --- a/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details.cypress.spec.js +++ b/src/applications/mhv/secure-messaging/tests/e2e/secure-messaging-message-thread-details.cypress.spec.js @@ -42,12 +42,11 @@ describe('Secure Messaging Message Details', () => { detailsPage.expandThreadMessageDetails(updatedMockThread, 1); // cy.reload(true); detailsPage.verifyExpandedMessageToDisplay(mockParentMessageDetails, 1); - // detailsPage.verifyExpandedMessageFromDisplay(mockParentMessageDetails); // TODO need to check the logic on displaying triage grop name only on received messages - // detailsPage.verifyExpandedMessageIDDisplay(mockParentMessageDetails); //TODO UCD is still determining whether to display this + // detailsPage.verifyExpandedMessageFromDisplay(mockParentMessageDetails); // TODO need to check the logic on displaying triage group name only on received messages + // detailsPage.verifyExpandedMessageIDDisplay(mockParentMessageDetails); // TODO UCD is still determining whether to display this detailsPage.verifyExpandedMessageDateDisplay(mockParentMessageDetails, 1); - cy.get('@messageDetails.all').should('have.length', 1); - // detailsPage.verifyUnexpandedMessageAttachment(1); //TODO attachment icons will be added in a future story + detailsPage.verifyUnexpandedMessageAttachment(2); cy.injectAxe(); cy.axeCheck(AXE_CONTEXT, {}); }); diff --git a/src/applications/pensions/components/DependentField.jsx b/src/applications/pensions/components/DependentField.jsx deleted file mode 100644 index ef24b4f58cf0..000000000000 --- a/src/applications/pensions/components/DependentField.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { relationshipLabels } from '../labels'; - -export default function DependentField({ formData }) { - const { first, middle, last, suffix } = formData.fullName; - const relationship = relationshipLabels[formData.relationship]; - - return ( -
      - - {first} {middle && `${middle} `} - {last} - {suffix && `, ${suffix}`} - -
      - {relationship} -
      - ); -} diff --git a/src/applications/pensions/components/IncomeSourceView.jsx b/src/applications/pensions/components/IncomeSourceView.jsx index e88d8f46167b..a615c1eae847 100644 --- a/src/applications/pensions/components/IncomeSourceView.jsx +++ b/src/applications/pensions/components/IncomeSourceView.jsx @@ -1,12 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { formatCurrency } from '../helpers'; +import { typeOfIncomeLabels } from '../labels'; export default function IncomeSourceView({ formData }) { return (

      - {formData.typeOfIncome} + {typeOfIncomeLabels[formData.typeOfIncome]}

      Who receives this income?

      {formData.receiver}

      diff --git a/src/applications/pensions/config/chapters/02-military-history/pow.js b/src/applications/pensions/config/chapters/02-military-history/pow.js index 8e32b0224103..42307b09767a 100644 --- a/src/applications/pensions/config/chapters/02-military-history/pow.js +++ b/src/applications/pensions/config/chapters/02-military-history/pow.js @@ -12,7 +12,7 @@ const { powDateRange } = fullSchemaPensions.properties; /** @type {PageSchema} */ export default { uiSchema: { - 'ui:title': 'P. O. W. Status', + 'ui:title': 'Prisoner of War status', powStatus: yesNoUI({ title: 'Have you ever been a prisoner of war?', classNames: 'vads-u-margin-bottom--2', diff --git a/src/applications/pensions/config/chapters/02-military-history/previousNames.js b/src/applications/pensions/config/chapters/02-military-history/previousNames.js index 48f1d41bda02..cd674908185e 100644 --- a/src/applications/pensions/config/chapters/02-military-history/previousNames.js +++ b/src/applications/pensions/config/chapters/02-military-history/previousNames.js @@ -5,20 +5,15 @@ import { fullNameSchema, } from 'platform/forms-system/src/js/web-component-patterns'; import ListItemView from '../../../components/ListItemView'; +import { formatFullName } from '../../../helpers'; -export const PreviousNameView = ({ formData }) => { - const { first = '', middle = '', last = '', suffix = '' } = - formData.previousFullName || {}; - - // Filter out empty strings and join non-empty parts with a space - const fullName = [first, middle, last, suffix].filter(Boolean).join(' '); - - return ; -}; +export const PreviousNameView = ({ formData }) => ( + +); PreviousNameView.propTypes = { formData: PropTypes.shape({ - previousFullName: PropTypes.string, + previousFullName: PropTypes.object, }), }; @@ -29,6 +24,8 @@ export default { previousNames: { 'ui:options': { itemName: 'Name', + itemAriaLabel: data => + data.previousFullName && formatFullName(data.previousFullName), viewField: PreviousNameView, reviewTitle: 'Previous names', keepInPageOnReview: true, diff --git a/src/applications/pensions/config/chapters/03-health-and-employment-information/employmentHistory.js b/src/applications/pensions/config/chapters/03-health-and-employment-information/employmentHistory.js index bbf277b53c54..5f73c8b8c2be 100644 --- a/src/applications/pensions/config/chapters/03-health-and-employment-information/employmentHistory.js +++ b/src/applications/pensions/config/chapters/03-health-and-employment-information/employmentHistory.js @@ -50,6 +50,7 @@ const generateEmployersSchemas = ( 'ui:title': employerMessage, 'ui:options': { itemName: 'Job', + itemAriaLabel: data => data.jobTitle, viewField: EmployerView, reviewTitle: employersReviewTitle, keepInPageOnReview: true, diff --git a/src/applications/pensions/config/chapters/03-health-and-employment-information/medicalCenters.js b/src/applications/pensions/config/chapters/03-health-and-employment-information/medicalCenters.js index bc18ffdb3d22..59c349f7fceb 100644 --- a/src/applications/pensions/config/chapters/03-health-and-employment-information/medicalCenters.js +++ b/src/applications/pensions/config/chapters/03-health-and-employment-information/medicalCenters.js @@ -37,6 +37,7 @@ const generateMedicalCentersSchemas = ( 'ui:title': medicalCenterMessage, 'ui:options': { itemName: 'Medical center', + itemAriaLabel: data => data.medicalCenter, viewField: MedicalCenterView, reviewTitle: medicalCentersReviewTitle, keepInPageOnReview: true, diff --git a/src/applications/pensions/config/chapters/04-household-information/currentSpouseFormerMarriages.jsx b/src/applications/pensions/config/chapters/04-household-information/currentSpouseFormerMarriages.jsx index 244e5e961c95..4683c906b895 100644 --- a/src/applications/pensions/config/chapters/04-household-information/currentSpouseFormerMarriages.jsx +++ b/src/applications/pensions/config/chapters/04-household-information/currentSpouseFormerMarriages.jsx @@ -16,7 +16,7 @@ import { ContactWarningAlert, ContactWarningMultiAlert, } from '../../../components/FormAlerts'; - +import { formatFullName } from '../../../helpers'; import ListItemView from '../../../components/ListItemView'; import SpouseMarriageTitle from '../../../components/SpouseMarriageTitle'; import { separationTypeLabels } from '../../../labels'; @@ -41,9 +41,7 @@ export const otherExplanationRequired = (form, index) => get(['spouseMarriages', index, 'reasonForSeparation'], form) === 'OTHER'; const SpouseMarriageView = ({ formData }) => ( - + ); SpouseMarriageView.propTypes = { @@ -69,6 +67,9 @@ export default { spouseMarriages: { 'ui:options': { itemName: 'Former marriage of the spouse', + itemAriaLabel: data => + data.spouseFullName && + `${formatFullName(data.spouseFullName)} former marriage of spouse`, viewField: SpouseMarriageView, reviewTitle: 'Spouse’s former marriages', keepInPageOnReview: true, diff --git a/src/applications/pensions/config/chapters/04-household-information/dependentChildren.js b/src/applications/pensions/config/chapters/04-household-information/dependentChildren.js index b5400704c776..0c13a52a22a9 100644 --- a/src/applications/pensions/config/chapters/04-household-information/dependentChildren.js +++ b/src/applications/pensions/config/chapters/04-household-information/dependentChildren.js @@ -1,14 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; import merge from 'lodash/merge'; import fullSchemaPensions from 'vets-json-schema/dist/21P-527EZ-schema.json'; import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/currentOrPastDate'; import fullNameUI from 'platform/forms/definitions/fullName'; - -import { dependentsMinItem } from '../../../helpers'; -import DependentField from '../../../components/DependentField'; +import ListItemView from '../../../components/ListItemView'; +import { dependentsMinItem, formatFullName } from '../../../helpers'; const { dependents } = fullSchemaPensions.properties; +const DependentNameView = ({ formData }) => ( + +); + +DependentNameView.propTypes = { + formData: PropTypes.shape({ + fullName: PropTypes.object, + }), +}; + /** @type {PageSchema} */ export default { uiSchema: { @@ -16,7 +27,8 @@ export default { dependents: { 'ui:options': { itemName: 'Dependent', - viewField: DependentField, + itemAriaLabel: data => data.fullName && formatFullName(data.fullName), + viewField: DependentNameView, reviewTitle: 'Dependent children', keepInPageOnReview: true, customTitle: ' ', diff --git a/src/applications/pensions/config/chapters/05-financial-information/careExpenses.js b/src/applications/pensions/config/chapters/05-financial-information/careExpenses.js index 9f36b58dd6bf..70f59127602e 100644 --- a/src/applications/pensions/config/chapters/05-financial-information/careExpenses.js +++ b/src/applications/pensions/config/chapters/05-financial-information/careExpenses.js @@ -41,6 +41,7 @@ export default { careExpenses: { 'ui:options': { itemName: 'Care Expense', + itemAriaLabel: data => `${data.provider} care expense`, viewField: CareExpenseView, reviewTitle: 'Care Expenses', keepInPageOnReview: true, diff --git a/src/applications/pensions/config/chapters/05-financial-information/incomeSources.js b/src/applications/pensions/config/chapters/05-financial-information/incomeSources.js index 1a158df50f53..2db7efaf44f8 100644 --- a/src/applications/pensions/config/chapters/05-financial-information/incomeSources.js +++ b/src/applications/pensions/config/chapters/05-financial-information/incomeSources.js @@ -11,16 +11,8 @@ import currencyUI from 'platform/forms-system/src/js/definitions/currency'; import { validateCurrency } from '../../../validation'; import { IncomeInformationAlert } from '../../../components/FormAlerts'; import { IncomeSourceDescription } from '../../../helpers'; +import { recipientTypeLabels, typeOfIncomeLabels } from '../../../labels'; import IncomeSourceView from '../../../components/IncomeSourceView'; -import { recipientTypeLabels } from '../../../labels'; - -const typeOfIncomeOptions = { - SOCIAL_SECURITY: 'Social Security', - INTEREST_DIVIDEND: 'Interest or dividend income', - CIVIL_SERVICE: 'Civil Service', - PENSION_RETIREMENT: 'Pension or retirement income', - OTHER: 'Other income', -}; export const otherExplanationRequired = (form, index) => get(['incomeSources', index, 'typeOfIncome'], form) === 'OTHER'; @@ -39,6 +31,8 @@ export default { incomeSources: { 'ui:options': { itemName: 'Income source', + itemAriaLabel: data => + `${typeOfIncomeLabels[data.typeOfIncome]} income source`, viewField: IncomeSourceView, reviewTitle: 'Income sources', keepInPageOnReview: true, @@ -49,7 +43,7 @@ export default { items: { typeOfIncome: radioUI({ title: 'What type of income?', - labels: typeOfIncomeOptions, + labels: typeOfIncomeLabels, }), otherTypeExplanation: { 'ui:title': 'Please specify', @@ -102,7 +96,7 @@ export default { type: 'object', required: ['typeOfIncome', 'receiver', 'payer', 'amount'], properties: { - typeOfIncome: radioSchema(Object.keys(typeOfIncomeOptions)), + typeOfIncome: radioSchema(Object.keys(typeOfIncomeLabels)), otherTypeExplanation: { type: 'string' }, receiver: radioSchema(Object.keys(recipientTypeLabels)), dependentName: { type: 'string' }, diff --git a/src/applications/pensions/config/chapters/05-financial-information/medicalExpenses.js b/src/applications/pensions/config/chapters/05-financial-information/medicalExpenses.js index 171d7304ace8..983307412016 100644 --- a/src/applications/pensions/config/chapters/05-financial-information/medicalExpenses.js +++ b/src/applications/pensions/config/chapters/05-financial-information/medicalExpenses.js @@ -34,6 +34,7 @@ export default { medicalExpenses: { 'ui:options': { itemName: 'Unreimbursed Expense', + itemAriaLabel: data => `${data.provider} unreimbursed expense`, viewField: MedicalExpenseView, reviewTitle: 'Unreimbursed Expenses', keepInPageOnReview: true, diff --git a/src/applications/pensions/containers/ConfirmationPage.jsx b/src/applications/pensions/containers/ConfirmationPage.jsx index 7aa065f05a98..d2467169a6c8 100644 --- a/src/applications/pensions/containers/ConfirmationPage.jsx +++ b/src/applications/pensions/containers/ConfirmationPage.jsx @@ -3,7 +3,6 @@ import { utcToZonedTime, format } from 'date-fns-tz'; import { connect } from 'react-redux'; import { focusElement } from 'platform/utilities/ui'; -import CallVBACenter from 'platform/static-data/CallVBACenter'; import { scrollToTop, formatFullName } from '../helpers'; const centralTz = 'America/Chicago'; @@ -20,11 +19,6 @@ class ConfirmationPage extends React.Component { } = this.props; const response = submission?.response ?? {}; const fullName = formatFullName(data?.veteranFullName ?? {}); - const regionalOffice = response?.regionalOffice || []; - - const pmcName = regionalOffice?.length - ? regionalOffice[0].replace('Attention:', '').trim() - : null; const zonedDate = utcToZonedTime(submission?.timestamp, centralTz); const submittedAt = format(zonedDate, 'LLL d, yyyy h:mm a zzz', { @@ -32,78 +26,130 @@ class ConfirmationPage extends React.Component { }); return ( -
      -

      Claim submitted

      -

      - We process claims in the order we receive them. Please print this page - for your records. -

      -

      We may contact you for more information or documents.

      -
      -

      - Veterans Pension Benefit Claim{' '} - (Form 21P-527EZ) -

      - for {fullName} - -
        -
      • - Date submitted -
        - {submittedAt} -
      • - {response?.confirmationNumber && ( -
      • - Confirmation number -
        - {response?.confirmationNumber} -
      • - )} -
      • - Pension Management Center +
        +

        Your Veterans Pension application

        + + +

        Thank you for submitting your Veterans Pension application.

        +

        + We've received your Veterans Pension application (VA Form + 21P-527EZ). After we complete our review, we'll mail you a decision + letter with the details of our decision. +

        +
        + +
        + + +

        + Your information for this application +

        + +

        Your name

        + {fullName} + +

        Date you submitted your application

        + {submittedAt} + + {response?.confirmationNumber && ( +
        +

        Confirmation number

        + {response?.confirmationNumber} +
        + )} + + { + window.print(); + }} + /> +
        + +
        +

        If you need to submit supporting documents

        + You can submit supporting documents in one of 2 ways: + +

        Submit your documents online through AccessVA

        +
        +

        + You can use the QuickSubmit tool through AccessVA to submit your + documents online. +

        + +
        + +

        Mail copies of your documents

        +
        +

        + Don’t mail us a printed copy of your pension application. We + already have your application. If you need to submit supporting + documents, you can mail copies of your documents to us at this + address: +

        +

        + Department of Veterans Affairs
        - {pmcName && {pmcName}} + Pension Intake Center
        - - Phone: , - Monday – – Friday, 8:00 a.m. – 9:00 p.m. ET - + PO Box 5365
        - Fax: 844-655-1604 -

      • -
      • - - If you have several documents to send in, you can mail them to: - -
        - {regionalOffice?.map((line, index) => ( -

        {line}

        - ))} -
        -
      • -
      • - Note: If you choose to mail in your supporting - documents, you don't have to send in a paper copy of VA Form - 21P-527EZ with the documents. -
      • -
      -
      -
      -

      Need help?

      -

      - If you have questions, -
      - Monday – Friday, 8:00 a.m. – 9:00 p.m. ET.
      - Please have your Social Security number or VA file number ready. -

      -
      -
      -
      - - - + Janesville, WI 53547-5365 +

      +

      + Note: Don't send us your original documents. We + can't return them. Mail us copies of your documents only. +

      +

      + If we asked you to complete and submit additional forms, be sure + to make copies of the forms for your records before you mail them + to us. +

      -
      +
      + +
      +

      What to expect next

      +

      + You don't need to do anything while you wait for a decision unless + we send you a letter to ask you for more information. If we send you + a request for more information, you’ll need to respond within 30 + days of our request. If you don't respond within 30 days, we may + decide your claim with the evidence that's available to us. +

      +

      + If you’ve opted to receive VA emails or texts, we’ll send you + updates about the status of your application. +

      +

      + You can also{' '} + +

      +

      + Note: It may take 7 to 10 days after you apply for + your pension claim to appear online. +

      +
      + +
      +

      How to contact us if you have questions

      +

      + You can ask us a question{' '} + +

      +

      + Or call us at {' '} + +

      +
    ); } diff --git a/src/applications/pensions/labels.jsx b/src/applications/pensions/labels.jsx index 0305b46d129d..dc94a748c719 100644 --- a/src/applications/pensions/labels.jsx +++ b/src/applications/pensions/labels.jsx @@ -33,3 +33,11 @@ export const serviceBranchLabels = { usphs: 'USPHS', noaa: 'NOAA', }; + +export const typeOfIncomeLabels = { + SOCIAL_SECURITY: 'Social Security', + INTEREST_DIVIDEND: 'Interest or dividend income', + CIVIL_SERVICE: 'Civil Service', + PENSION_RETIREMENT: 'Pension or retirement income', + OTHER: 'Other income', +}; diff --git a/src/applications/pensions/tests/unit/components/DependentField.unit.spec.jsx b/src/applications/pensions/tests/unit/components/DependentField.unit.spec.jsx deleted file mode 100644 index 46b9661cc6d5..000000000000 --- a/src/applications/pensions/tests/unit/components/DependentField.unit.spec.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import SkinDeep from 'skin-deep'; - -import DependentField from '../../../components/DependentField'; - -describe('Pensions DependentField', () => { - it('should render', () => { - const tree = SkinDeep.shallowRender( - , - ); - - expect(tree.text()).to.contain('Jane Doe'); - expect(tree.text()).to.contain('Child'); - }); -}); diff --git a/src/applications/pensions/tests/unit/containers/ConfirmationPage.unit.spec.jsx b/src/applications/pensions/tests/unit/containers/ConfirmationPage.unit.spec.jsx index 746421691519..3d477d03b5b2 100644 --- a/src/applications/pensions/tests/unit/containers/ConfirmationPage.unit.spec.jsx +++ b/src/applications/pensions/tests/unit/containers/ConfirmationPage.unit.spec.jsx @@ -60,38 +60,44 @@ describe('', () => { const form = generateForm(); const tree = SkinDeep.shallowRender(); - expect(tree.subTree('.confirmation-page-title').text()).to.equal( - 'Claim submitted', + const heading = tree.everySubTree('h2'); + expect(heading.length).to.eql(1); + expect(heading[0]?.text()).to.equal('Your Veterans Pension application'); + + const alert = tree.everySubTree('va-alert', { status: 'success' }); + expect(alert.length).to.eql(1); + + const info = tree.everySubTree('va-summary-box'); + expect(info.length).to.eql(1); + expect(info[0]?.subTree('va-button').props.text).to.equal( + 'Print this page for your records', ); - expect( - tree - .everySubTree('span')[1] - .text() - .trim(), - ).to.equal('for Jane Doe'); - expect(tree.everySubTree('li')[2].text()).to.contain('Western Region'); - expect(tree.everySubTree('p')[0].text()).to.contain( - 'We process claims in the order we receive them', + + const sections = tree.everySubTree('section'); + expect(sections.length).to.eql(3); + expect(sections[0].subTree('h3').text()).to.equal( + 'If you need to submit supporting documents', ); - expect(tree.everySubTree('p')[1].text()).to.contain( - 'We may contact you for more information or documents.', + expect(sections[1].subTree('h3').text()).to.equal('What to expect next'); + expect(sections[2].subTree('h3').text()).to.equal( + 'How to contact us if you have questions', ); - expect(tree.everySubTree('p')[3].text()).to.contain('VA Regional Office'); - }); - it('should render with empty regionalOffice', () => { - const form = generateForm({ hasRegionalOffice: false }); - const tree = shallow(); + const address = tree.everySubTree('p', { className: 'va-address-block' }); + expect(address.length).to.eql(1); - expect(tree.find('address').children().length).to.eql(0); - tree.unmount(); + const phoneNums = tree.everySubTree('va-telephone'); + expect(phoneNums.length).to.eql(2); + expect(phoneNums[0].props.international).to.be.true; + expect(phoneNums[1].props.tty).to.be.true; }); it('should render if no submission response', () => { const form = generateForm({ hasResponse: false }); const tree = shallow(); - expect(tree.find('.claim-list').children().length).to.eql(4); + const confirmation = tree.find('#pension_527ez_submission_confirmation'); + expect(confirmation.length).to.eql(0); tree.unmount(); }); }); diff --git a/src/applications/personalization/common/hooks/useSessionStorage.unit.spec.js b/src/applications/personalization/common/hooks/useSessionStorage.unit.spec.js index 31f22192109e..5126412c4e8b 100644 --- a/src/applications/personalization/common/hooks/useSessionStorage.unit.spec.js +++ b/src/applications/personalization/common/hooks/useSessionStorage.unit.spec.js @@ -1,6 +1,12 @@ import React from 'react'; import { expect } from 'chai'; -import { render, act, fireEvent, waitFor } from '@testing-library/react'; +import { + render, + act, + fireEvent, + waitFor, + cleanup, +} from '@testing-library/react'; import sinon from 'sinon'; import PropTypes from 'prop-types'; @@ -33,6 +39,7 @@ TestComponent.propTypes = { }; describe('useSessionStorage', () => { + afterEach(cleanup); const testKey = 'testKey'; it('retrieves an existing value from sessionStorage', () => { diff --git a/src/applications/personalization/profile/components/account-security/AccountSecurityContent.jsx b/src/applications/personalization/profile/components/account-security/AccountSecurityContent.jsx index 6672e92f46ce..7f1f1fccc47d 100644 --- a/src/applications/personalization/profile/components/account-security/AccountSecurityContent.jsx +++ b/src/applications/personalization/profile/components/account-security/AccountSecurityContent.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import { recordCustomProfileEvent } from '@@vap-svc/util/analytics'; import { @@ -14,8 +14,23 @@ import IdentityNotVerified from '~/platform/user/authorization/components/Identi import MPIConnectionError from '~/applications/personalization/components/MPIConnectionError'; import NotInMPIError from '~/applications/personalization/components/NotInMPIError'; import { AccountSecurityTables } from './AccountSecurityTables'; -import { selectIsBlocked } from '../../selectors'; +import { + selectIsBlocked, + selectShowCredRetirementMessaging, +} from '../../selectors'; import { AccountBlocked } from '../alerts/AccountBlocked'; +import { AccountSecurityLoa1CredAlert } from '../alerts/CredentialRetirementAlerts'; + +const IdNotVerifiedContent = () => { + const showCredRetirementMessaging = useSelector( + selectShowCredRetirementMessaging, + ); + return showCredRetirementMessaging ? ( + + ) : ( + + ); +}; export const AccountSecurityContent = ({ isIdentityVerified, @@ -29,7 +44,7 @@ export const AccountSecurityContent = ({ {isBlocked && ( )} - {!isIdentityVerified && } + {!isIdentityVerified && } {showMPIConnectionError && ( )} diff --git a/src/applications/personalization/profile/components/account-security/AccountSecurityTables.jsx b/src/applications/personalization/profile/components/account-security/AccountSecurityTables.jsx index 6b48827b668b..c16ec2cd9624 100644 --- a/src/applications/personalization/profile/components/account-security/AccountSecurityTables.jsx +++ b/src/applications/personalization/profile/components/account-security/AccountSecurityTables.jsx @@ -81,11 +81,12 @@ export const AccountSecurityTables = ({ ); return ( <> - {/* legacy toble view for email address table */} } + data={ + + } className="vads-u-margin-bottom--2" /> diff --git a/src/applications/personalization/profile/components/alerts/CredentialRetirementAlerts.jsx b/src/applications/personalization/profile/components/alerts/CredentialRetirementAlerts.jsx new file mode 100644 index 000000000000..eabd748932f3 --- /dev/null +++ b/src/applications/personalization/profile/components/alerts/CredentialRetirementAlerts.jsx @@ -0,0 +1,95 @@ +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import React from 'react'; +import { VaAlert } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { useSignInServiceProvider } from '../../hooks'; +import { HowToVerifyLink } from '~/platform/user/authorization/components/IdentityNotVerified'; +import { useSessionStorage } from '../../../common/hooks/useSessionStorage'; + +// alerts to be used during the transition period of MHV and DS Logon credential retirement + +export const AccountSecurityLoa1CredAlert = () => { + const { label } = useSignInServiceProvider(); + + return ( + <> + +

    + Verify your identity with Login.gov or ID.me to manage your profile + information +

    +
    +

    + Before we give you access to your VA.gov profile, we need to make + sure you’re you—and not someone pretending to be you. This helps us + protect your identity and prevent fraud. +

    +

    + If you have a verified Login.gov or ID.me account, sign out now. + Then sign back in with that account to continue. +

    +

    + {`If you don’t have one of these accounts, you can create one and + verify your identity now. Starting December 31, 2024, you’ll no + longer be able to sign in with your ${label} username and password.`} +

    + + + +

    + Note: If you need help updating your personal + information, call us at{' '} + ( + + ). We’re here Monday through Friday, 8:00 a.m. to 9:00 p.m. ET. +

    +
    +
    + + + + ); +}; + +export const SignInEmailAlert = () => { + const { label } = useSignInServiceProvider(); + + const [dismissed, setDismissed] = useSessionStorage( + 'dismissedCredentialAlerts', + ); + + const hideAlert = () => { + setDismissed('true'); + }; + + return ( + +
    +

    + {`Starting December 31, 2024, you’ll no longer be able to sign in with + your ${label} username and password. You’ll need to use a verified + Login.gov or ID.me account to access your profile.`} +

    + +

    + If you don’t have one of these accounts, you can create one and verify + your identity now. +

    + + +
    +
    + ); +}; diff --git a/src/applications/personalization/profile/components/contact-information/email-addresses/SignInServiceUpdateLink.jsx b/src/applications/personalization/profile/components/contact-information/email-addresses/SignInServiceUpdateLink.jsx index 6d6f7a47b061..d94c5041197a 100644 --- a/src/applications/personalization/profile/components/contact-information/email-addresses/SignInServiceUpdateLink.jsx +++ b/src/applications/personalization/profile/components/contact-information/email-addresses/SignInServiceUpdateLink.jsx @@ -1,11 +1,22 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { useSignInServiceProvider } from '../../../hooks'; +import { SignInEmailAlert } from '../../alerts/CredentialRetirementAlerts'; +import { selectShowCredRetirementMessaging } from '../../../selectors'; -const SignInServiceUpdateLink = () => { +const SignInServiceUpdateLink = ({ isIdentityVerified }) => { const { link, label } = useSignInServiceProvider(); + const showCredRetirementMessaging = useSelector( + selectShowCredRetirementMessaging, + ); + return ( <> + {showCredRetirementMessaging && + isIdentityVerified && } +

    To access or update your sign-in information, go to the website where you manage your account information. Any updates you make there will @@ -20,4 +31,9 @@ const SignInServiceUpdateLink = () => { ); }; +// this prop will be removed once MHV and DS Logon are fully retired +SignInServiceUpdateLink.propTypes = { + isIdentityVerified: PropTypes.bool.isRequired, +}; + export default SignInServiceUpdateLink; diff --git a/src/applications/personalization/profile/mocks/endpoints/feature-toggles/index.js b/src/applications/personalization/profile/mocks/endpoints/feature-toggles/index.js index 0db6928cca16..d3cb2ad0bf27 100644 --- a/src/applications/personalization/profile/mocks/endpoints/feature-toggles/index.js +++ b/src/applications/personalization/profile/mocks/endpoints/feature-toggles/index.js @@ -11,6 +11,7 @@ const profileToggles = { profileShowMhvNotificationSettings: false, profileUseExperimental: false, profileShowQuickSubmitNotificationSetting: false, + profileShowCredentialRetirementMessaging: false, profileShowEmailNotificationSettings: false, profileShowProofOfVeteranStatus: false, }; diff --git a/src/applications/personalization/profile/mocks/endpoints/user/index.js b/src/applications/personalization/profile/mocks/endpoints/user/index.js index 1e1959a44cb6..230a86db1ec3 100644 --- a/src/applications/personalization/profile/mocks/endpoints/user/index.js +++ b/src/applications/personalization/profile/mocks/endpoints/user/index.js @@ -1411,6 +1411,20 @@ const mockErrorResponses = { }, }; +// user that is loa1 but is a dslogon user +const loa1UserDSLogon = set( + cloneDeep(baseUserResponses.loa1User), + 'data.attributes.profile.signIn.serviceName', + 'dslogon', +); + +// user that is loa1 but is a mhv user +const loa1UserMHV = set( + cloneDeep(baseUserResponses.loa1User), + 'data.attributes.profile.signIn.serviceName', + 'mhv', +); + // users with various contact info missing const loa3UserWithNoMobilePhone = set( cloneDeep(baseUserResponses.loa3User72), @@ -1459,6 +1473,8 @@ const loa3UserWithoutMailingAddress = set( const responses = { ...baseUserResponses, ...mockErrorResponses, + loa1UserDSLogon, + loa1UserMHV, loa3UserWithNoMobilePhone, loa3UserWithNoEmail, loa3UserWithNoEmailOrMobilePhone, diff --git a/src/applications/personalization/profile/mocks/server.js b/src/applications/personalization/profile/mocks/server.js index e5d8df0adcfa..144b31987b71 100644 --- a/src/applications/personalization/profile/mocks/server.js +++ b/src/applications/personalization/profile/mocks/server.js @@ -63,6 +63,7 @@ const responses = { authExpVbaDowntimeMessage: false, profileContacts: true, profileHideDirectDepositCompAndPen: false, + profileShowCredentialRetirementMessaging: true, profileShowEmailNotificationSettings: true, profileShowMhvNotificationSettings: true, profileShowPaymentsNotificationSetting: true, @@ -77,12 +78,13 @@ const responses = { 'GET /v0/user': (_req, res) => { // return res.status(403).json(genericErrors.error500); // example user data cases - return res.json(user.loa3User72); // default user (success) + return res.json(user.loa3User72); // default user LOA3 w/id.me (success) // return res.json(user.dsLogonUser); // user with dslogon signIn.serviceName // return res.json(user.mvhUser); // user with mhv signIn.serviceName - // return res.json(user.loa1User); // user with loa1 + // return res.json(user.loa1User); // LOA1 user w/id.me + // return res.json(user.loa1UserDSLogon); // LOA1 user w/dslogon + // return res.json(user.loa1UserMHV); // LOA1 user w/mhv // return res.json(user.badAddress); // user with bad address - // return res.json(user.loa3User); // user with loa3 // return res.json(user.nonVeteranUser); // non-veteran user // return res.json(user.externalServiceError); // external service error // return res.json(user.loa3UserWithNoMobilePhone); // user with no mobile phone number @@ -217,7 +219,7 @@ const responses = { return res.json(address.homeAddressUpdateReceived.response); }, 'GET /v0/profile/status/:id': (req, res) => { - // uncomment this to simlulate multiple status calls + // uncomment this to simulate multiple status calls // aka long latency on getting update to go through // if (retries < 2) { // retries += 1; diff --git a/src/applications/personalization/profile/selectors.js b/src/applications/personalization/profile/selectors.js index 8cef91b72086..d93c4ea0ba6c 100644 --- a/src/applications/personalization/profile/selectors.js +++ b/src/applications/personalization/profile/selectors.js @@ -3,6 +3,7 @@ import { createSelector } from 'reselect'; import { toggleValues } from '~/platform/site-wide/feature-toggles/selectors'; import FEATURE_FLAG_NAMES from '~/platform/utilities/feature-toggles/featureFlagNames'; +import { CSP_IDS } from '~/platform/user/authentication/constants'; import { cnpDirectDepositBankInfo, @@ -144,3 +145,16 @@ export const selectProfileShowProofOfVeteranStatusToggle = state => toggleValues(state)?.[FEATURE_FLAG_NAMES.profileShowProofOfVeteranStatus]; export const selectProfileContacts = state => state?.profileContacts || {}; + +export const selectHasRetiringSignInService = state => { + const serviceName = state?.user?.profile?.signIn?.serviceName; + return !serviceName || [CSP_IDS.DS_LOGON, CSP_IDS.MHV].includes(serviceName); +}; + +export const selectShowCredRetirementMessaging = state => { + return ( + toggleValues(state)?.[ + FEATURE_FLAG_NAMES.profileShowCredentialRetirementMessaging + ] && selectHasRetiringSignInService(state) + ); +}; diff --git a/src/applications/personalization/profile/tests/components/account-security/AccountSecurity.unit.spec.jsx b/src/applications/personalization/profile/tests/components/account-security/AccountSecurity.unit.spec.jsx index d5791f2701fc..a6635fb8ce84 100644 --- a/src/applications/personalization/profile/tests/components/account-security/AccountSecurity.unit.spec.jsx +++ b/src/applications/personalization/profile/tests/components/account-security/AccountSecurity.unit.spec.jsx @@ -1,35 +1,41 @@ import React from 'react'; -import { shallow } from 'enzyme'; import { expect } from 'chai'; +import { cleanup } from '@testing-library/react'; +import AccountSecurity from '../../../components/account-security/AccountSecurity'; +import { + createCustomProfileState, + renderWithProfileReducersAndRouter, +} from '../../unit-test-helpers'; -import DowntimeNotification, { - externalServices, -} from 'platform/monitoring/DowntimeNotification'; +describe('AccountSecurity Page', () => { + afterEach(cleanup); -import ProfileSectionHeadline from '@@profile/components/ProfileSectionHeadline'; -import AccountSecurity from '@@profile/components/account-security/AccountSecurity'; -import AccountSecurityContent from '@@profile/components/account-security/AccountSecurityContent'; - -describe('AccountSecurity', () => { - let wrapper; - beforeEach(() => { - wrapper = shallow(); - }); - afterEach(() => { - wrapper.unmount(); + it('renders without crashing', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState(), + }, + ); + expect(getByText('Account security')).to.exist; }); - it('renders a ProfileSectionHeadline component as its first child', () => { - const firstChild = wrapper.childAt(0); - expect(firstChild.type()).to.equal(ProfileSectionHeadline); + + it('sets the document title on mount', () => { + renderWithProfileReducersAndRouter(, { + initialState: createCustomProfileState(), + }); + expect(document.title).to.equal('Account Security | Veterans Affairs'); }); - it('renders a properly configured DowntimeNotification component as its second child', () => { - const secondChild = wrapper.childAt(1); - expect(secondChild.type()).to.equal(DowntimeNotification); - expect(secondChild.prop('dependencies')).to.deep.equal([ - externalServices.emis, - externalServices.mvi, - ]); - expect(secondChild.children().type()).to.equal(AccountSecurityContent); + it('renders main section headings of account security page', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState(), + }, + ); + + expect(getByText('Sign-in information')).to.exist; + expect(getByText('Account setup')).to.exist; }); }); diff --git a/src/applications/personalization/profile/tests/components/account-security/AccountSecurityContent.unit.spec.jsx b/src/applications/personalization/profile/tests/components/account-security/AccountSecurityContent.unit.spec.jsx new file mode 100644 index 000000000000..2a8df458a556 --- /dev/null +++ b/src/applications/personalization/profile/tests/components/account-security/AccountSecurityContent.unit.spec.jsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { expect } from 'chai'; +import { cleanup } from '@testing-library/react'; +import { within } from '@testing-library/dom'; +import { CSP_IDS } from '~/platform/user/authentication/constants'; +import AccountSecurityContent from '../../../components/account-security/AccountSecurityContent'; +import { + createCustomProfileState, + createFeatureTogglesState, + renderWithProfileReducersAndRouter, +} from '../../unit-test-helpers'; +import { Toggler } from '~/platform/utilities/feature-toggles'; + +describe('AccountSecurityContent component', () => { + afterEach(cleanup); + + it('renders without crashing', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: { + ...createCustomProfileState({ + user: { + profile: { + signIn: { serviceName: CSP_IDS.DS_LOGON }, + loa: { current: 1 }, + }, + }, + }), + ...createFeatureTogglesState({ + [Toggler.TOGGLE_NAMES + .profileShowCredentialRetirementMessaging]: true, + }), + }, + }, + ); + + expect(getByText('Sign-in information')).to.exist; + expect(getByText('Account setup')).to.exist; + }); + + it('renders credential retirement alert when using DS Logon signIn.serviceName', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: { + ...createCustomProfileState({ + user: { + profile: { + signIn: { serviceName: CSP_IDS.DS_LOGON }, + loa: { current: 1 }, + }, + }, + }), + ...createFeatureTogglesState({ + [Toggler.TOGGLE_NAMES + .profileShowCredentialRetirementMessaging]: true, + }), + }, + }, + ); + + expect( + getByText( + 'Verify your identity with Login.gov or ID.me to manage your profile information', + { + exact: false, + }, + ), + 'heading for alert exists when DS Logon is used', + ).to.exist; + + expect( + getByText( + 'Starting December 31, 2024, you’ll no longer be able to sign in with your DS Logon username and password.', + { + exact: false, + }, + ), + 'content for alert exists when DS Logon is used', + ).to.exist; + }); + + it('renders credential retirement alert when using MHV signIn.serviceName', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: { + ...createCustomProfileState({ + user: { + profile: { + signIn: { serviceName: CSP_IDS.MHV }, + loa: { current: 1 }, + }, + }, + }), + ...createFeatureTogglesState({ + [Toggler.TOGGLE_NAMES + .profileShowCredentialRetirementMessaging]: true, + }), + }, + }, + ); + + expect( + getByText( + 'Verify your identity with Login.gov or ID.me to manage your profile information', + { + exact: false, + }, + ), + 'heading for alert exists when MHV is used', + ).to.exist; + + expect( + getByText( + 'Starting December 31, 2024, you’ll no longer be able to sign in with your My HealtheVet username and password.', + { + exact: false, + }, + ), + 'content for alert exists when MHV is used', + ).to.exist; + }); + + it('renders regular identity not verified alert when user is not verified and id.me', () => { + const { getByText, container } = renderWithProfileReducersAndRouter( + , + { + initialState: { + ...createCustomProfileState({ + user: { + profile: { + signIn: { serviceName: CSP_IDS.ID_ME }, + loa: { current: 1 }, + }, + }, + }), + ...createFeatureTogglesState({ + [Toggler.TOGGLE_NAMES + .profileShowCredentialRetirementMessaging]: false, + }), + }, + }, + ); + + expect( + getByText('Verify your identity to access your complete profile'), + 'heading for alert exists when user is not verified', + ).to.exist; + + const alert = container.querySelector('va-alert'); + expect( + within(alert).getByRole('link', { name: 'Verify your identity' }), + 'verify identity link exists when user is not verified', + ).to.exist; + }); +}); diff --git a/src/applications/personalization/profile/tests/components/alerts/CredentialRetirementAlerts.unit.spec.jsx b/src/applications/personalization/profile/tests/components/alerts/CredentialRetirementAlerts.unit.spec.jsx new file mode 100644 index 000000000000..39f1ec43ec96 --- /dev/null +++ b/src/applications/personalization/profile/tests/components/alerts/CredentialRetirementAlerts.unit.spec.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { expect } from 'chai'; +import { cleanup } from '@testing-library/react'; +import { + createCustomProfileState, + renderWithProfileReducersAndRouter, +} from '../../unit-test-helpers'; +import { CSP_IDS } from '~/platform/user/authentication/constants'; + +import { + AccountSecurityLoa1CredAlert, + SignInEmailAlert, +} from '../../../components/alerts/CredentialRetirementAlerts'; + +describe('AccountSecurityLoa1CredAlert', () => { + afterEach(cleanup); + + it('renders alert with DS_LOGON service provider in text', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState({ + user: { profile: { signIn: { serviceName: CSP_IDS.DS_LOGON } } }, + }), + }, + ); + + expect( + getByText('sign in with your DS Logon username and password', { + exact: false, + }), + ).to.exist; + }); + + it('renders alert with MHV service provider in text', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState({ + user: { profile: { signIn: { serviceName: CSP_IDS.MHV } } }, + }), + }, + ); + + expect( + getByText('sign in with your My HealtheVet username and password', { + exact: false, + }), + ).to.exist; + }); +}); + +describe('SignInEmailAlert', () => { + afterEach(cleanup); + + it('renders alert with DS_LOGON service provider in text', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState({ + user: { profile: { signIn: { serviceName: CSP_IDS.DS_LOGON } } }, + }), + }, + ); + + expect( + getByText('sign in with your DS Logon username and password', { + exact: false, + }), + ).to.exist; + }); + + it('renders alert with MHV service provider in text', () => { + const { getByText } = renderWithProfileReducersAndRouter( + , + { + initialState: createCustomProfileState({ + user: { profile: { signIn: { serviceName: CSP_IDS.MHV } } }, + }), + }, + ); + + expect( + getByText('sign in with your My HealtheVet username and password', { + exact: false, + }), + ).to.exist; + }); +}); diff --git a/src/applications/personalization/profile/tests/e2e/account-security/credential-retirement-alert.cypress.spec.js b/src/applications/personalization/profile/tests/e2e/account-security/credential-retirement-alert.cypress.spec.js new file mode 100644 index 000000000000..36f1d72fceba --- /dev/null +++ b/src/applications/personalization/profile/tests/e2e/account-security/credential-retirement-alert.cypress.spec.js @@ -0,0 +1,110 @@ +import { PROFILE_PATHS } from '../../../constants'; + +import fullName from '../../fixtures/full-name-success.json'; +import personalInformation from '../../fixtures/personal-information-success-enhanced.json'; +import serviceHistory from '../../fixtures/service-history-success.json'; +import { generateFeatureToggles } from '../../../mocks/endpoints/feature-toggles'; +import { + dsLogonUser, + mvhUser, + loa1User, + loa1UserDSLogon, + loa1UserMHV, +} from '../../../mocks/endpoints/user'; +import { findVaLinkByText } from '../../../../common/e2eHelpers'; + +context('credential retirement alerts on account security', () => { + beforeEach(() => { + cy.intercept('v0/profile/full_name', fullName); + cy.intercept('v0/profile/personal_information', personalInformation); + cy.intercept('v0/profile/service_history', () => serviceHistory); + cy.intercept( + 'GET', + '/v0/feature_toggles*', + generateFeatureToggles({ + profileShowCredentialRetirementMessaging: true, + }), + ); + }); + + it('should show credential retirement alert content for DS Logon account', () => { + cy.login(dsLogonUser); + + cy.visit(PROFILE_PATHS.ACCOUNT_SECURITY); + + cy.findByText( + `you’ll no longer be able to sign in with your DS Logon username and password`, + { exact: false }, + ); + + findVaLinkByText('Learn how to create an account').should('exist'); + + cy.injectAxeThenAxeCheck(); + }); + + it('should show credential retirement alert content for MHV account', () => { + cy.login(mvhUser); + + cy.visit(PROFILE_PATHS.ACCOUNT_SECURITY); + + cy.findByText( + `you’ll no longer be able to sign in with your My HealtheVet username and password`, + { exact: false }, + ); + + findVaLinkByText('Learn how to create an account').should('exist'); + + cy.injectAxeThenAxeCheck(); + }); + + it('should show credential retirement alert content for LOA1 - DS Logon users', () => { + cy.login(loa1UserDSLogon); + + cy.visit(PROFILE_PATHS.ACCOUNT_SECURITY); + + cy.findByText( + `Verify your identity with Login.gov or ID.me to manage your profile information`, + { exact: false }, + ); + + cy.findByText( + `Starting December 31, 2024, you’ll no longer be able to sign in with your DS Logon username and password.`, + { exact: false }, + ); + + cy.injectAxeThenAxeCheck(); + }); + + it('should show credential retirement alert content for LOA1 - MHV users', () => { + cy.login(loa1UserMHV); + + cy.visit(PROFILE_PATHS.ACCOUNT_SECURITY); + + cy.findByText( + `Verify your identity with Login.gov or ID.me to manage your profile information`, + { exact: false }, + ); + + cy.findByText( + `Starting December 31, 2024, you’ll no longer be able to sign in with your My HealtheVet username and password.`, + { exact: false }, + ); + + cy.injectAxeThenAxeCheck(); + }); + + it('should show standard verify identity alert content for LOA1 - id.me users', () => { + cy.login(loa1User); + + cy.visit(PROFILE_PATHS.ACCOUNT_SECURITY); + + cy.get('va-alert').within(() => { + cy.findByText('Verify your identity to access your complete profile', { + exact: false, + }); + cy.findByRole('link', { name: /verify your identity/i }); + }); + + cy.injectAxeThenAxeCheck(); + }); +}); diff --git a/src/applications/personalization/profile/tests/unit-test-helpers.js b/src/applications/personalization/profile/tests/unit-test-helpers.js index 9b5135dacdfd..98c21fe2a758 100644 --- a/src/applications/personalization/profile/tests/unit-test-helpers.js +++ b/src/applications/personalization/profile/tests/unit-test-helpers.js @@ -4,7 +4,8 @@ import { Formik } from 'formik'; import profile from '@@profile/reducers'; import connectedApps from '@@profile/components/connected-apps/reducers/connectedApps'; -import { snakeCase } from 'lodash'; +import snakeCase from 'lodash/snakeCase'; +import merge from 'lodash/merge'; import { renderInReduxProvider, renderWithStoreAndRouter, @@ -213,6 +214,25 @@ export function createBasicInitialState() { }; } +const loa3State = { + user: { + profile: { + loa: { current: 3 }, + verified: true, + multifactor: true, + signIn: { + serviceName: 'idme', + accountType: 'N/A', + }, + }, + }, +}; + +export function createCustomProfileState(profileState = { ...loa3State }) { + const customState = merge(loa3State, profileState); + return merge(createBasicInitialState(), customState); +} + export function createFeatureTogglesState(customToggles = {}) { // Convert the custom toggles to snake case so that they could be passed in as camel case first const snakeCasedToggles = Object.entries(customToggles).reduce( diff --git a/src/applications/pre-need-integration/config/form.jsx b/src/applications/pre-need-integration/config/form.jsx index 8aaa4a4579c5..fd4b5c2c2b20 100644 --- a/src/applications/pre-need-integration/config/form.jsx +++ b/src/applications/pre-need-integration/config/form.jsx @@ -62,11 +62,22 @@ import { relationshipToVetPreparerDescription, relationshipToVetOptions, relationshipToVetPreparerOptions, + veteranApplicantDetailsSubHeader, + veteranApplicantDetailsPreparerSubHeader, + veteranApplicantDetailsPreparerDescription, + nonPreparerFullMaidenNameUI, + preparerFullMaidenNameUI, + ssnDashesUI, + preparerSsnDashesUI, + nonPreparerDateOfBirthUI, + preparerDateOfBirthUI, // partial implementation of story resolving the address change: // applicantDetailsCityTitle, // applicantDetailsStateTitle, // applicantDetailsPreparerCityTitle, // applicantDetailsPreparerStateTitle, + applicantDemographicsSubHeader, + applicantDemographicsPreparerSubHeader, applicantDemographicsGenderTitle, applicantDemographicsMaritalStatusTitle, applicantDemographicsPreparerGenderTitle, @@ -261,16 +272,20 @@ const formConfig = { schema: applicantRelationshipToVet.schema, }, veteranApplicantDetails: { - title: 'Applicant details', + title: 'Your details', path: 'veteran-applicant-details', depends: formData => !isAuthorizedAgent(formData) && isVeteran(formData), - uiSchema: veteranApplicantDetails - .uiSchema + uiSchema: veteranApplicantDetails.uiSchema( // partial implementation of story resolving the address change: // applicantDetailsCityTitle, // applicantDetailsStateTitle, - (), + veteranApplicantDetailsSubHeader, + '', + nonPreparerFullMaidenNameUI, + ssnDashesUI, + nonPreparerDateOfBirthUI, + ), schema: veteranApplicantDetails.schema, }, veteranApplicantDetailsPreparer: { @@ -278,12 +293,16 @@ const formConfig = { path: 'veteran-applicant-details-preparer', depends: formData => isAuthorizedAgent(formData) && isVeteran(formData), - uiSchema: veteranApplicantDetails - .uiSchema + uiSchema: veteranApplicantDetails.uiSchema( // partial implementation of story resolving the address change: // applicantDetailsPreparerCityTitle, // applicantDetailsPreparerStateTitle, - (), + veteranApplicantDetailsPreparerSubHeader, + veteranApplicantDetailsPreparerDescription, + preparerFullMaidenNameUI, + preparerSsnDashesUI, + preparerDateOfBirthUI, + ), schema: veteranApplicantDetails.schema, }, nonVeteranApplicantDetails: { @@ -308,11 +327,12 @@ const formConfig = { schema: applicantContactInfo.schema, }, applicantDemographics: { - title: 'Applicant demographics', + title: 'Your demographics', path: 'applicant-demographics', depends: formData => !isAuthorizedAgent(formData) && isVeteran(formData), uiSchema: applicantDemographics.uiSchema( + applicantDemographicsSubHeader, applicantDemographicsGenderTitle, applicantDemographicsMaritalStatusTitle, ), @@ -324,6 +344,7 @@ const formConfig = { depends: formData => isAuthorizedAgent(formData) && isVeteran(formData), uiSchema: applicantDemographics.uiSchema( + applicantDemographicsPreparerSubHeader, applicantDemographicsPreparerGenderTitle, applicantDemographicsPreparerMaritalStatusTitle, ), @@ -334,6 +355,7 @@ const formConfig = { depends: formData => !isAuthorizedAgent(formData) && isVeteran(formData), uiSchema: applicantDemographics2.uiSchema( + applicantDemographicsSubHeader, applicantDemographicsEthnicityTitle, applicantDemographicsRaceTitle, ), @@ -344,6 +366,7 @@ const formConfig = { depends: formData => isAuthorizedAgent(formData) && isVeteran(formData), uiSchema: applicantDemographics2.uiSchema( + applicantDemographicsPreparerSubHeader, applicantDemographicsPreparerEthnicityTitle, applicantDemographicsPreparerRaceTitle, ), diff --git a/src/applications/pre-need-integration/config/pages/applicantDemographics.jsx b/src/applications/pre-need-integration/config/pages/applicantDemographics.jsx index a299bf854238..9f2c7f1ec386 100644 --- a/src/applications/pre-need-integration/config/pages/applicantDemographics.jsx +++ b/src/applications/pre-need-integration/config/pages/applicantDemographics.jsx @@ -13,12 +13,13 @@ import { const { veteran } = fullSchemaPreNeed.properties.application.properties; export function uiSchema( + subHeader = applicantDemographicsSubHeader, genderTitle = applicantDemographicsGenderTitle, maritalStatusTitle = applicantDemographicsMaritalStatusTitle, ) { return { application: { - 'ui:title': applicantDemographicsSubHeader, + 'ui:title': subHeader, 'view:applicantDemographicsDescription': { 'ui:description': applicantDemographicsDescription, 'ui:options': { diff --git a/src/applications/pre-need-integration/config/pages/applicantDemographics2.jsx b/src/applications/pre-need-integration/config/pages/applicantDemographics2.jsx index cf9a3a9f3ac0..9629ed10fc35 100644 --- a/src/applications/pre-need-integration/config/pages/applicantDemographics2.jsx +++ b/src/applications/pre-need-integration/config/pages/applicantDemographics2.jsx @@ -13,12 +13,13 @@ import { const { veteran } = fullSchemaPreNeed.properties.application.properties; export function uiSchema( + subHeader = applicantDemographicsSubHeader, ethnicityTitle = applicantDemographicsEthnicityTitle, raceTitle = applicantDemographicsRaceTitle, ) { return { application: { - 'ui:title': applicantDemographicsSubHeader, + 'ui:title': subHeader, 'view:applicantDemographicsDescription': { 'ui:description': applicantDemographicsDescription, 'ui:options': { diff --git a/src/applications/pre-need-integration/config/pages/nonVeteranApplicantDetails.jsx b/src/applications/pre-need-integration/config/pages/nonVeteranApplicantDetails.jsx index 375939d76133..ec355025bae3 100644 --- a/src/applications/pre-need-integration/config/pages/nonVeteranApplicantDetails.jsx +++ b/src/applications/pre-need-integration/config/pages/nonVeteranApplicantDetails.jsx @@ -5,87 +5,54 @@ import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/curren import { merge, pick } from 'lodash'; -import environment from 'platform/utilities/environment'; - import { - applicantDetailsDescription, - applicantDetailsSubHeader, + nonVeteranApplicantDetailsDescription, + nonVeteranApplicantDetailsSubHeader, fullMaidenNameUI, ssnDashesUI, } from '../../utils/helpers'; const { claimant } = fullSchemaPreNeed.properties.application.properties; -export const uiSchema = !environment.isProduction() - ? { - 'ui:description': applicantDescription, - application: { - 'ui:title': applicantDetailsSubHeader, - claimant: { - 'view:applicantDetailsDescription': { - 'ui:description': applicantDetailsDescription, - 'ui:options': { - displayEmptyObjectOnReview: true, - }, - }, - name: fullMaidenNameUI, - ssn: ssnDashesUI, - dateOfBirth: currentOrPastDateUI('Date of birth'), - }, - }, - } - : { - 'ui:description': applicantDescription, - application: { - 'ui:title': applicantDetailsSubHeader, - claimant: { - name: fullMaidenNameUI, - ssn: ssnDashesUI, - dateOfBirth: currentOrPastDateUI('Date of birth'), - }, - }, - }; -export const schema = !environment.isProduction() - ? { - type: 'object', - properties: { - application: { - type: 'object', - properties: { - claimant: { - type: 'object', - required: ['name', 'ssn', 'dateOfBirth'], - properties: merge( - {}, - { - 'view:applicantDetailsDescription': { - type: 'object', - properties: {}, - }, - }, - pick(claimant.properties, ['name', 'ssn', 'dateOfBirth']), - ), - }, - }, +export const uiSchema = { + 'ui:description': applicantDescription, + application: { + 'ui:title': nonVeteranApplicantDetailsSubHeader, + claimant: { + 'view:applicantDetailsDescription': { + 'ui:description': nonVeteranApplicantDetailsDescription, + 'ui:options': { + displayEmptyObjectOnReview: true, }, }, - } - : { + name: fullMaidenNameUI, + ssn: ssnDashesUI, + dateOfBirth: currentOrPastDateUI('Date of birth'), + }, + }, +}; + +export const schema = { + type: 'object', + properties: { + application: { type: 'object', properties: { - application: { + claimant: { type: 'object', - properties: { - claimant: { - type: 'object', - required: ['name', 'ssn', 'dateOfBirth'], - properties: pick(claimant.properties, [ - 'name', - 'ssn', - 'dateOfBirth', - ]), + required: ['name', 'ssn', 'dateOfBirth'], + properties: merge( + {}, + { + 'view:applicantDetailsDescription': { + type: 'object', + properties: {}, + }, }, - }, + pick(claimant.properties, ['name', 'ssn', 'dateOfBirth']), + ), }, }, - }; + }, + }, +}; diff --git a/src/applications/pre-need-integration/config/pages/sponsorRace.jsx b/src/applications/pre-need-integration/config/pages/sponsorRace.jsx index 178303c1750e..b536ae62216e 100644 --- a/src/applications/pre-need-integration/config/pages/sponsorRace.jsx +++ b/src/applications/pre-need-integration/config/pages/sponsorRace.jsx @@ -19,8 +19,8 @@ export const uiSchema = { }, }, veteran: merge({}, veteranUI, { - ethnicity: { 'ui:title': 'What’s the sponsor’s marital status?' }, - race: { 'ui:title': 'What’s the sponsor’s sex?' }, + ethnicity: { 'ui:title': 'What’s the sponsor’s ethnicity?' }, + race: { 'ui:title': 'What’s the sponsor’s race?' }, }), }, }; diff --git a/src/applications/pre-need-integration/config/pages/veteranApplicantDetails.jsx b/src/applications/pre-need-integration/config/pages/veteranApplicantDetails.jsx index faac58a2cbec..b64fb32c04e8 100644 --- a/src/applications/pre-need-integration/config/pages/veteranApplicantDetails.jsx +++ b/src/applications/pre-need-integration/config/pages/veteranApplicantDetails.jsx @@ -1,13 +1,11 @@ import fullSchemaPreNeed from 'vets-json-schema/dist/40-10007-INTEGRATION-schema.json'; -import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/currentOrPastDate'; - import { merge, pick } from 'lodash'; import { - applicantDetailsDescription, - applicantDetailsSubHeader, - fullMaidenNameUI, + veteranApplicantDetailsSubHeader, + nonPreparerFullMaidenNameUI, + nonPreparerDateOfBirthUI, ssnDashesUI, // partial implementation of story resolving the address change: // applicantDetailsCityTitle, @@ -19,23 +17,29 @@ const { veteran, } = fullSchemaPreNeed.properties.application.properties; -export function uiSchema() { +export function uiSchema( + subHeader = veteranApplicantDetailsSubHeader, + description = '', + nameUI = nonPreparerFullMaidenNameUI, + ssnUI = ssnDashesUI, + dateOfBirthUI = nonPreparerDateOfBirthUI, +) { // partial implementation of story resolving the address change: // cityTitle = applicantDetailsCityTitle, // stateTitle = applicantDetailsStateTitle, return { application: { - 'ui:title': applicantDetailsSubHeader, + 'ui:title': subHeader, claimant: { 'view:applicantDetailsDescription': { - 'ui:description': applicantDetailsDescription, + 'ui:description': description, 'ui:options': { displayEmptyObjectOnReview: true, }, }, - name: fullMaidenNameUI, - ssn: ssnDashesUI, - dateOfBirth: currentOrPastDateUI('Date of birth'), + name: nameUI, + ssn: ssnUI, + dateOfBirth: dateOfBirthUI, }, veteran: { placeOfBirth: { diff --git a/src/applications/pre-need-integration/tests/config/veteranApplicantDetails.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/veteranApplicantDetails.unit.spec.jsx index 51a0dc8daa70..48681f13f93f 100644 --- a/src/applications/pre-need-integration/tests/config/veteranApplicantDetails.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/veteranApplicantDetails.unit.spec.jsx @@ -26,10 +26,6 @@ describe('Pre-need applicant veteran applicant details', () => { expect(form.find('input').length).to.equal(7); expect(form.find('select').length).to.equal(3); - expect(form.find('va-additional-info').length).to.equal(1); - expect(form.find('va-additional-info').html()).to.include( - '

    If you’re filling out the form on behalf of someone else, you’ll need to provide their details below. As the preparer, we’ll ask for your own details later.

    ', - ); form.unmount(); }); diff --git a/src/applications/pre-need-integration/utils/helpers.js b/src/applications/pre-need-integration/utils/helpers.js index ecb3a53c987f..9298fa3c4217 100644 --- a/src/applications/pre-need-integration/utils/helpers.js +++ b/src/applications/pre-need-integration/utils/helpers.js @@ -10,6 +10,7 @@ import fullNameUI from 'platform/forms/definitions/fullName'; import ssnUI from 'platform/forms-system/src/js/definitions/ssn'; import TextWidget from 'platform/forms-system/src/js/widgets/TextWidget'; import VaCheckboxGroupField from 'platform/forms-system/src/js/web-component-fields/VaCheckboxGroupField'; +import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/currentOrPastDate'; import { stringifyFormReplacer, @@ -28,7 +29,19 @@ import CurrentlyBuriedDescription from '../components/CurrentlyBuriedDescription export const nonRequiredFullNameUI = omit('required', fullNameUI); -export const applicantDetailsSubHeader = ( +export const veteranApplicantDetailsSubHeader = ( +
    +

    Your details

    +
    +); + +export const veteranApplicantDetailsPreparerSubHeader = ( +
    +

    Applicant details

    +
    +); + +export const nonVeteranApplicantDetailsSubHeader = (

    Applicant details

    @@ -105,6 +118,12 @@ export const sponsorMilitaryDetailsSubHeader = ( ); export const applicantDemographicsSubHeader = ( +
    +

    Your demographics

    +
    +); + +export const applicantDemographicsPreparerSubHeader = (

    Applicant demographics

    @@ -113,8 +132,8 @@ export const applicantDemographicsSubHeader = ( export const applicantDemographicsDescription = (

    - We require some basic details as part of your application. Please know we - need to gather the data for statistical purposes. + We require demographic information as part of this application. We use + this information for statistical purposes only.

    ); @@ -166,7 +185,10 @@ export const applicantInformationDescription = ( ); -export const applicantDetailsDescription = ( +export const veteranApplicantDetailsPreparerDescription = + 'Provide the details for the person you’re filling out the application for (called the applicant).'; + +export const nonVeteranApplicantDetailsDescription = (

    If you’re filling out the form on behalf of someone else, you’ll need to @@ -622,18 +644,36 @@ export function transform(formConfig, form) { */ } -export const fullMaidenNameUI = !environment.isProduction() - ? merge({}, fullNameUI, { - first: { 'ui:title': 'First name' }, - middle: { 'ui:title': 'Middle name' }, - last: { 'ui:title': 'Last name' }, - maiden: { 'ui:title': 'Maiden name' }, - 'ui:order': ['first', 'middle', 'last', 'suffix', 'maiden'], - }) - : merge({}, fullNameUI, { - maiden: { 'ui:title': 'Maiden name' }, - 'ui:order': ['first', 'middle', 'last', 'suffix', 'maiden'], - }); +export const fullMaidenNameUI = merge({}, fullNameUI, { + first: { 'ui:title': 'First name' }, + middle: { 'ui:title': 'Middle name' }, + last: { 'ui:title': 'Last name' }, + maiden: { 'ui:title': 'Maiden name' }, + 'ui:order': ['first', 'middle', 'last', 'suffix', 'maiden'], +}); + +export const nonPreparerFullMaidenNameUI = merge({}, fullMaidenNameUI, { + first: { 'ui:title': 'Your first name' }, + middle: { 'ui:title': 'Your middle name' }, + last: { 'ui:title': 'Your last name' }, + maiden: { 'ui:title': 'Maiden name' }, +}); + +export const preparerFullMaidenNameUI = merge({}, fullMaidenNameUI, { + first: { 'ui:title': 'Applicant’s first name' }, + middle: { 'ui:title': 'Applicant’s middle name' }, + last: { 'ui:title': 'Applicant’s last name' }, + maiden: { 'ui:title': 'Applicant’s maiden name' }, + suffix: { 'ui:title': 'Applicant’s suffix' }, +}); + +export const nonPreparerDateOfBirthUI = currentOrPastDateUI( + 'Your date of birth', +); + +export const preparerDateOfBirthUI = currentOrPastDateUI( + 'Applicant’s date of birth', +); class SSNWidget extends React.Component { constructor(props) { @@ -669,6 +709,10 @@ class SSNWidget extends React.Component { // Modify default uiSchema for SSN to insert any missing dashes. export const ssnDashesUI = merge({}, ssnUI, { 'ui:widget': SSNWidget }); +export const preparerSsnDashesUI = merge({}, ssnDashesUI, { + 'ui:title': 'Applicant’s Social Security number', +}); + export const veteranUI = { militaryServiceNumber: { 'ui:title': diff --git a/src/applications/pre-need/config/form.jsx b/src/applications/pre-need/config/form.jsx index 4575621b06ec..e72adc1131f9 100644 --- a/src/applications/pre-need/config/form.jsx +++ b/src/applications/pre-need/config/form.jsx @@ -11,6 +11,8 @@ import { VA_FORM_IDS } from 'platform/forms/constants'; import { useSelector } from 'react-redux'; import fileUploadUI from 'platform/forms-system/src/js/definitions/file'; +import transformForSubmit from './transformForSubmit'; + import emailUI from '../definitions/email'; import * as applicantMilitaryHistory from './pages/applicantMilitaryHistory'; import * as applicantMilitaryName from './pages/applicantMilitaryName'; @@ -58,6 +60,8 @@ import { applicantsMailingAddressHasState, sponsorMailingAddressHasState, isSponsorDeceased, + createPayload, + parseResponse, } from '../utils/helpers'; import SupportingFilesDescription from '../components/SupportingFilesDescription'; import { @@ -126,9 +130,13 @@ const formConfig = { }, rootUrl: manifest.rootUrl, urlPrefix: '/', - submitUrl: `${environment.API_URL}/v0/preneeds/burial_forms`, + submitUrl: environment.isProduction() + ? `${environment.API_URL}/v0/preneeds/burial_forms` + : `${environment.API_URL}/simple_forms_api/v1/simple_forms`, trackingPrefix: 'preneed-', - transformForSubmit: transform, + transformForSubmit: environment.isProduction() + ? transform + : transformForSubmit, formId: VA_FORM_IDS.FORM_40_10007, saveInProgress: { messages: { @@ -328,22 +336,28 @@ const formConfig = { preneedAttachments: fileUploadUI('Select files to upload', { buttonText: 'Upload file', addAnotherLabel: 'Upload another file', - fileUploadUrl: `${ - environment.API_URL - }/v0/preneeds/preneed_attachments`, + fileUploadUrl: environment.isProduction() + ? `${environment.API_URL}/v0/preneeds/preneed_attachments` + : `${ + environment.API_URL + }/simple_forms_api/v1/simple_forms/submit_supporting_documents`, fileTypes: ['pdf'], maxSize: 15728640, hideLabelText: true, - createPayload: file => { - const payload = new FormData(); - payload.append('preneed_attachment[file_data]', file); + createPayload: !environment.isProduction() + ? createPayload + : file => { + const payload = new FormData(); + payload.append('preneed_attachment[file_data]', file); - return payload; - }, - parseResponse: (response, file) => ({ - name: file.name, - confirmationCode: response.data.attributes.guid, - }), + return payload; + }, + parseResponse: !environment.isProduction() + ? parseResponse + : (response, file) => ({ + name: file.name, + confirmationCode: response.data.attributes.guid, + }), attachmentSchema: { 'ui:title': 'What kind of file is this?', }, diff --git a/src/applications/pre-need/config/transformForSubmit.js b/src/applications/pre-need/config/transformForSubmit.js new file mode 100644 index 000000000000..8a877bb4ff2c --- /dev/null +++ b/src/applications/pre-need/config/transformForSubmit.js @@ -0,0 +1,24 @@ +import { transformForSubmit as formsSystemTransformForSubmit } from 'platform/forms-system/src/js/helpers'; + +const escapedCharacterReplacer = (_key, value) => { + if (typeof value === 'string') { + return value + .replaceAll('"', "'") + .replace(/(?:\r\n|\n\n|\r|\n)/g, '; ') + .replace(/(?:\t|\f|\b)/g, '') + .replace(/\\(?!(f|n|r|t|[u,U][\d,a-fA-F]{4}))/gm, '/'); + } + + return value; +}; + +export default function transformForSubmit(formConfig, form) { + const transformedData = JSON.parse( + formsSystemTransformForSubmit(formConfig, form), + ); + + return JSON.stringify( + { ...transformedData, formNumber: formConfig.formId }, + escapedCharacterReplacer, + ); +} diff --git a/src/applications/pre-need/containers/ConfirmationPage.jsx b/src/applications/pre-need/containers/ConfirmationPage.jsx index 375d213a51f2..418feb61c1a2 100644 --- a/src/applications/pre-need/containers/ConfirmationPage.jsx +++ b/src/applications/pre-need/containers/ConfirmationPage.jsx @@ -1,6 +1,7 @@ import React from 'react'; import moment from 'moment'; import { connect } from 'react-redux'; +import environment from 'platform/utilities/environment'; import scrollToTop from 'platform/utilities/ui/scrollToTop'; import { focusElement } from 'platform/utilities/ui'; @@ -40,25 +41,27 @@ class ConfirmationPage extends React.Component { {name.first} {name.middle} {name.last} {name.suffix}

    - {response.trackingNumber && ( -
  • - Confirmation number -
    - {response.trackingNumber} -
  • - )} + {environment.isProduction() && + response.trackingNumber && ( +
  • + Confirmation number +
    + {response.trackingNumber} +
  • + )}
  • Form name
    Burial Pre-Need Claim (Form 40-10007)
  • - {response.trackingNumber && ( -
  • - Date submitted -
    - {submittedAt.format('MMM D, YYYY')} -
  • - )} + {environment.isProduction() && + response.trackingNumber && ( +
  • + Date submitted +
    + {submittedAt.format('MMM D, YYYY')} +
  • + )} { expect(screen.getByText('Your claim has been submitted.')).to.exist; }); - it('it should show response dependent text', () => { - const screen = render( - - - , - ); - expect(screen.getByText('123456')).to.exist; - expect(screen.getByText('Oct. 25, 2023')).to.exist; - }); - const storeBase2 = { form: { formId: formConfig.formId, diff --git a/src/applications/pre-need/utils/helpers.js b/src/applications/pre-need/utils/helpers.js index f00866c7a00c..6291bcd22696 100644 --- a/src/applications/pre-need/utils/helpers.js +++ b/src/applications/pre-need/utils/helpers.js @@ -9,6 +9,8 @@ import dateRangeUI from 'platform/forms-system/src/js/definitions/dateRange'; import fullNameUI from 'platform/forms/definitions/fullName'; import ssnUI from 'platform/forms-system/src/js/definitions/ssn'; import TextWidget from 'platform/forms-system/src/js/widgets/TextWidget'; +import { $$ } from 'platform/forms-system/src/js/utilities/ui'; +import { focusElement } from 'platform/utilities/ui'; import { stringifyFormReplacer, @@ -25,6 +27,38 @@ import ServicePeriodView from '../components/ServicePeriodView'; export const nonRequiredFullNameUI = omit('required', fullNameUI); +export const createPayload = (file, formId, password) => { + const payload = new FormData(); + payload.set('form_id', formId); + payload.append('file', file); + if (password) { + payload.append('password', password); + } + return payload; +}; + +export function parseResponse({ data }) { + const { name } = data.attributes; + const focusFileCard = () => { + const target = $$('.schemaform-file-list li').find(entry => + entry.textContent?.trim().includes(name), + ); + + if (target) { + focusElement(target); + } + }; + + setTimeout(() => { + focusFileCard(); + }, 100); + + return { + name, + confirmationCode: data.attributes.confirmationCode, + }; +} + export const applicantDetailsSubHeader = (

    Applicant details

    diff --git a/src/applications/rated-disabilities/components/AppContent.jsx b/src/applications/rated-disabilities/components/AppContent.jsx new file mode 100644 index 000000000000..9d4df9b1329e --- /dev/null +++ b/src/applications/rated-disabilities/components/AppContent.jsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; + +import { getRatedDisabilities } from '../actions'; +import CombinedRating from './CombinedRating'; +import NeedHelp from './NeedHelp'; +import Learn from './Learn'; +import OnThisPage from './OnThisPage'; +import RatingLists from './RatingLists'; +import ServerError from './ServerError'; + +const loadingIndicator = ( + +); + +export default function AppContent() { + const [data, setData] = useState({}); + const [hasError, setHasError] = useState(false); + const [isRequestDone, setIsRequestDone] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + const responseData = await getRatedDisabilities(); + setData(responseData); + } catch (err) { + setHasError(true); + } finally { + setIsRequestDone(true); + } + }; + + fetchData(); + }, []); + + const { combinedDisabilityRating, individualRatings } = data || {}; + + const hasRatedDisabilities = individualRatings?.length > 0; + + let contentOrError; + if (hasError) { + contentOrError = ; + } else { + contentOrError = ( + <> +
    {hasRatedDisabilities && }
    +

    + Your combined disability rating +

    + +

    + Your individual ratings +

    + + + ); + } + + return ( +
    +
    +
    +

    View your VA disability ratings

    + {isRequestDone ? contentOrError : loadingIndicator} + + +
    +
    +
    + ); +} diff --git a/src/applications/rated-disabilities/components/CombinedRating.jsx b/src/applications/rated-disabilities/components/CombinedRating.jsx new file mode 100644 index 000000000000..dc4eeefaaab2 --- /dev/null +++ b/src/applications/rated-disabilities/components/CombinedRating.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { recordEvent } from '@department-of-veterans-affairs/platform-monitoring/exports'; + +import NoCombinedRating from './NoCombinedRating'; + +const clickHandler = () => { + recordEvent({ + event: 'disability-navigation-check-claims', + }); +}; + +export default function CombinedRating({ combinedRating }) { + // It may be possible to have a combinedRating of 0, + // so not using !combinedRating here to avoid showing this + // component if combinedRating is 0 + if (combinedRating === null || typeof combinedRating === 'undefined') { + return ; + } + + const heading = `Your combined disability rating is ${combinedRating}%`; + + return ( + +

    {heading}

    +

    + This rating doesn’t include any conditions from claims that we’re still + reviewing. You can check the status of your disability claims, decision + reviews, or appeals online. +

    + +
    + ); +} + +CombinedRating.propTypes = { + combinedRating: PropTypes.number, +}; diff --git a/src/applications/rated-disabilities/components/NeedHelp.jsx b/src/applications/rated-disabilities/components/NeedHelp.jsx index 4307a54c66a6..27d43a2c1c45 100644 --- a/src/applications/rated-disabilities/components/NeedHelp.jsx +++ b/src/applications/rated-disabilities/components/NeedHelp.jsx @@ -7,7 +7,7 @@ export default function NeedHelp() {

    You can call us at . - We’re We’re here Monday through Friday, 8:00 a.m to 9:00 p.m. ET. If + We’re here Monday through Friday, 8:00 a.m to 9:00 p.m. ET. If have have hearing loss, call

    diff --git a/src/applications/rated-disabilities/components/NoCombinedRating.jsx b/src/applications/rated-disabilities/components/NoCombinedRating.jsx new file mode 100644 index 000000000000..a5827cd77bfc --- /dev/null +++ b/src/applications/rated-disabilities/components/NoCombinedRating.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function NoCombinedRating() { + return ( + +

    + We don’t have a combined disability rating on file for you +

    +
    +

    + We can’t find a combined disability rating for you. If you have a + disability that was caused by or got worse because of your service, + you can file a claim for disability benefits. +

    + +
    +
    + ); +} diff --git a/src/applications/rated-disabilities/components/RatingLists/List.jsx b/src/applications/rated-disabilities/components/RatingLists/List.jsx new file mode 100644 index 000000000000..164cc3665ccc --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/List.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ListItem from './ListItem'; + +export default function List({ ratings }) { + return ( +
    + {ratings.map((rating, index) => ( + + ))} +
    + ); +} + +List.propTypes = { + ratings: PropTypes.arrayOf( + PropTypes.shape({ + decision: PropTypes.string, + diagnosticText: PropTypes.string, + diagnosticTypeCode: PropTypes.string, + diagnosticTypeName: PropTypes.string, + disabilityRatingId: PropTypes.string, + effectiveDate: PropTypes.string, + ratingEndDate: PropTypes.string, + ratingPercentage: PropTypes.number, + }), + ).isRequired, +}; diff --git a/src/applications/rated-disabilities/components/RatingLists/ListItem.jsx b/src/applications/rated-disabilities/components/RatingLists/ListItem.jsx new file mode 100644 index 000000000000..fa7b6538a31d --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/ListItem.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { formatDate, getHeadingText } from './helpers'; + +const ListItem = ({ rating }) => { + const { effectiveDate } = rating; + const headingText = getHeadingText(rating); + + return ( + +

    {headingText}

    + {effectiveDate !== null && ( +
    + Effective date: {formatDate(effectiveDate)} +
    + )} +
    + ); +}; + +ListItem.propTypes = { + rating: PropTypes.shape({ + decision: PropTypes.string, + diagnosticText: PropTypes.string, + diagnosticTypeCode: PropTypes.string, + diagnosticTypeName: PropTypes.string, + disabilityRatingId: PropTypes.string, + effectiveDate: PropTypes.string, + ratingEndDate: PropTypes.string, + ratingPercentage: PropTypes.number, + }).isRequired, +}; + +export default ListItem; diff --git a/src/applications/rated-disabilities/components/RatingLists/NoRatings.jsx b/src/applications/rated-disabilities/components/RatingLists/NoRatings.jsx new file mode 100644 index 000000000000..a6d1cb3c1a0b --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/NoRatings.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function NoRatings() { + return ( + +

    + We don’t have any rated disabilities on file for you +

    +
    +

    + We can’t find any rated disabilities for you. If you have a disability + that was caused by or got worse because of your service, you can file + a claim for disability benefits. +

    + +
    +
    + ); +} diff --git a/src/applications/rated-disabilities/components/RatingLists/RatingLists.jsx b/src/applications/rated-disabilities/components/RatingLists/RatingLists.jsx new file mode 100644 index 000000000000..1a407649e90e --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/RatingLists.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + getServiceConnectedRatings, + getNonServiceConnectedRatings, +} from './helpers'; +import NoRatings from './NoRatings'; +import List from './List'; + +export default function RatingLists({ ratings }) { + const serviceConnectedRatings = getServiceConnectedRatings(ratings); + const nonServiceConnectedRatings = getNonServiceConnectedRatings(ratings); + + const hasRatings = ratings.length !== 0; + const hasServiceConnectedRatings = serviceConnectedRatings.length !== 0; + const hasNonServiceConnectedRatings = nonServiceConnectedRatings.length !== 0; + + if (!hasRatings) { + return ; + } + + return ( + <> + {hasServiceConnectedRatings && ( + <> +

    Service-connected ratings

    + + + )} + {hasNonServiceConnectedRatings && ( + <> +

    + Conditions VA determined aren’t service-connected +

    + + + )} + + ); +} + +RatingLists.propTypes = { + ratings: PropTypes.arrayOf( + PropTypes.shape({ + decision: PropTypes.string, + diagnosticText: PropTypes.string, + diagnosticTypeCode: PropTypes.string, + diagnosticTypeName: PropTypes.string, + disabilityRatingId: PropTypes.string, + effectiveDate: PropTypes.string, + ratingEndDate: PropTypes.string, + ratingPercentage: PropTypes.number, + }), + ).isRequired, +}; diff --git a/src/applications/rated-disabilities/components/RatingLists/helpers.jsx b/src/applications/rated-disabilities/components/RatingLists/helpers.jsx new file mode 100644 index 000000000000..265b7d598411 --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/helpers.jsx @@ -0,0 +1,27 @@ +import { buildDateFormatter } from '../../util'; + +export const formatDate = buildDateFormatter('MMMM dd, yyyy'); + +export const getHeadingText = rating => { + const { diagnosticText, ratingPercentage } = rating; + const headingParts = [diagnosticText]; + if (ratingPercentage !== null && typeof ratingPercentage !== 'undefined') { + headingParts.unshift(`${ratingPercentage}% rating for`); + } + + return headingParts.join(' '); +}; + +const isServiceConnected = item => item.decision === 'Service Connected'; + +export const sortRatings = ratings => { + return ratings.sort((a, b) => b.effectiveDate.localeCompare(a.effectiveDate)); +}; + +export const getServiceConnectedRatings = ratings => + sortRatings(ratings.filter(isServiceConnected)); + +// Non-service-connected ratings will have an effectiveDate of null, +// so we don't need to sort them +export const getNonServiceConnectedRatings = ratings => + ratings.filter(rating => !isServiceConnected(rating)); diff --git a/src/applications/rated-disabilities/components/RatingLists/index.jsx b/src/applications/rated-disabilities/components/RatingLists/index.jsx new file mode 100644 index 000000000000..efbef334956e --- /dev/null +++ b/src/applications/rated-disabilities/components/RatingLists/index.jsx @@ -0,0 +1,8 @@ +import RatingLists from './RatingLists'; +import List from './List'; +import ListItem from './ListItem'; +import * as helpers from './helpers'; + +export { helpers, List, ListItem }; + +export default RatingLists; diff --git a/src/applications/rated-disabilities/components/ServerError.jsx b/src/applications/rated-disabilities/components/ServerError.jsx new file mode 100644 index 000000000000..a510df827093 --- /dev/null +++ b/src/applications/rated-disabilities/components/ServerError.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; + +export default function ServerError() { + return ( + +

    We’re sorry. Something went wrong on our end.

    +

    + Please refresh this page or check back later. You can also sign out of + VA.gov and try signing back into this page. +

    +

    + If you get this error again, please call the VA.gov help desk at{' '} + ( + + ). We’re here Monday through Friday, 8:00 a.m. to 8:00 p.m. ET. +

    +
    + ); +} diff --git a/src/applications/rated-disabilities/containers/App.jsx b/src/applications/rated-disabilities/containers/App.jsx index bf8ac915b760..cc53d0c48524 100644 --- a/src/applications/rated-disabilities/containers/App.jsx +++ b/src/applications/rated-disabilities/containers/App.jsx @@ -9,6 +9,7 @@ import { RequiredLoginView } from '@department-of-veterans-affairs/platform-user import backendServices from '@department-of-veterans-affairs/platform-user/profile/backendServices'; import { fetchRatedDisabilities, fetchTotalDisabilityRating } from '../actions'; +import AppContent from '../components/AppContent'; import FeatureFlagsLoaded from '../components/FeatureFlagsLoaded'; import MVIError from '../components/MVIError'; import RatedDisabilityView from '../components/RatedDisabilityView'; @@ -16,6 +17,7 @@ import { isLoadingFeatures, rdDetectDiscrepancies, rdSortAbTest, + rdUseLighthouse, } from '../selectors'; const App = props => { @@ -41,17 +43,20 @@ const App = props => { ) : ( - + {props.useLighthouse ? ( + + ) : ( + + )} )} @@ -69,6 +74,9 @@ App.propTypes = { ratedDisabilities: PropTypes.object, sortToggle: PropTypes.bool, totalDisabilityRating: PropTypes.number, + // START lighthouse_migration + useLighthouse: PropTypes.bool, + // END lighthouse_migration user: PropTypes.object, }; @@ -81,6 +89,9 @@ const mapStateToProps = state => ({ sortToggle: rdSortAbTest(state), totalDisabilityRating: state.totalRating.totalDisabilityRating, user: state.user, + // START lighthouse_migration + useLighthouse: rdUseLighthouse(state), + // END lighthouse_migration }); const mapDispatchToProps = { diff --git a/src/applications/rated-disabilities/tests/components/AppContent.unit.spec.jsx b/src/applications/rated-disabilities/tests/components/AppContent.unit.spec.jsx new file mode 100644 index 000000000000..a86e4e2de296 --- /dev/null +++ b/src/applications/rated-disabilities/tests/components/AppContent.unit.spec.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { $ } from '@department-of-veterans-affairs/platform-forms-system/ui'; + +import * as actions from '../../actions'; +import AppContent from '../../components/AppContent'; + +const mockResponse = { combinedDisabilityRating: 30, individualRatings: [] }; +const mockError = { + errors: [ + { + status: '500', + error: 'Internal Server Error', + path: '/veteran_verification/v2/disability_rating', + code: '500', + title: 'Common::Exceptions::ExternalServerInternalServerError', + detail: 'Internal Server Error', + }, + ], +}; + +const headlines = { + combinedRating: `Your combined disability rating is ${ + mockResponse.combinedDisabilityRating + }%`, + noCombinedRating: + 'We don’t have a combined disability rating on file for you', + noRatings: 'We don’t have any rated disabilities on file for you', + serverError: 'We’re sorry. Something went wrong on our end.', +}; + +const mockApiCall = sinon.stub(actions, 'getRatedDisabilities'); + +describe('', () => { + beforeEach(() => { + mockApiCall.reset(); + }); + + context('error/loading states', () => { + it('should display a loading indicator when the request is not done', async () => { + mockApiCall.resolves(mockResponse); + + const { container } = render(); + + expect($('va-loading-indicator', container)).to.exist; + }); + + it('should not display a loading indicator when the request is done', async () => { + mockApiCall.resolves(mockResponse); + + const { container } = render(); + + await Promise.resolve(); + + expect($('va-loading-indicator', container)).not.to.exist; + }); + + it('should display an error message when the request fails', async () => { + mockApiCall.rejects(mockError); + + const screen = render(); + + await Promise.resolve(); + + expect(screen.getByText(headlines.serverError)).to.exist; + }); + + it('should display a message when there is no combined rating', async () => { + mockApiCall.resolves({ ...mockResponse, combinedDisabilityRating: null }); + + const screen = render(); + + await Promise.resolve(); + + expect(screen.getByText(headlines.noCombinedRating)).to.exist; + }); + + it('should display a message when there are no ratings', async () => { + mockApiCall.resolves(mockResponse); + + const screen = render(); + + await Promise.resolve(); + + expect(screen.getByText(headlines.noRatings)).to.exist; + }); + }); + + it('should display a combined rating message', async () => { + mockApiCall.resolves(mockResponse); + + const screen = render(); + + await Promise.resolve(); + + expect(screen.getByText(headlines.combinedRating)).to.exist; + }); + + it('should display a list of ratings', async () => { + mockApiCall.resolves({ + ...mockResponse, + individualRatings: [ + { + decision: 'Service Connected', + diagnosticText: 'Hearing Loss', + diagnosticTypeName: '6100-Hearing loss', + effectiveDate: '2005-01-01', + ratingPercentage: 20, + }, + ], + }); + + const screen = render(); + + await Promise.resolve(); + + expect(screen.getByText('20% rating for Hearing Loss')).to.exist; + }); +}); diff --git a/src/applications/rated-disabilities/tests/components/CombinedRating.unit.spec.jsx b/src/applications/rated-disabilities/tests/components/CombinedRating.unit.spec.jsx new file mode 100644 index 000000000000..ffff41dadd95 --- /dev/null +++ b/src/applications/rated-disabilities/tests/components/CombinedRating.unit.spec.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import CombinedRating from '../../components/CombinedRating'; + +const noRatingText = + 'We don’t have a combined disability rating on file for you'; + +describe('', () => { + it('should render loading indicator if feature toggles are not available', () => { + const screen = render(); + + screen.getByText(noRatingText); + }); + + it('should render children if feature toggles are available', () => { + const screen = render(); + + screen.getByText(noRatingText); + }); + + it('should render children if feature toggles are available', () => { + const screen = render(); + + screen.getByText('Your combined disability rating is 100%'); + }); +}); diff --git a/src/applications/rated-disabilities/tests/components/RatingLists/List.unit.spec.jsx b/src/applications/rated-disabilities/tests/components/RatingLists/List.unit.spec.jsx new file mode 100644 index 000000000000..cbeb0bad029c --- /dev/null +++ b/src/applications/rated-disabilities/tests/components/RatingLists/List.unit.spec.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; + +import { List } from '../../../components/RatingLists'; + +const ratings = [ + { + decision: 'Service Connected', + diagnosticText: 'Hearing Loss', + diagnosticTypeName: '6100-Hearing loss', + effectiveDate: '2005-01-01', + ratingPercentage: 20, + }, + { + decision: 'Service Connected', + diagnosticText: 'Allergies due to Hearing Loss', + diagnosticTypeName: 'Limitation of flexion, knee', + effectiveDate: '2012-05-01', + ratingPercentage: 100, + }, + { + decision: 'Service Connected', + diagnosticText: 'Sarcoma Soft-Tissue', + diagnosticTypeName: 'Soft tissue sarcoma (neurogenic origin)', + effectiveDate: '2018-08-01', + ratingPercentage: 80, + }, +]; + +describe('', () => { + it('should display a list of ratings', () => { + const screen = render(); + + expect(screen.getAllByRole('heading', { level: 4 }).length).to.equal(3); + }); +}); diff --git a/src/applications/rated-disabilities/tests/components/RatingLists/ListItem.unit.spec.jsx b/src/applications/rated-disabilities/tests/components/RatingLists/ListItem.unit.spec.jsx new file mode 100644 index 000000000000..2eb86fc9b82e --- /dev/null +++ b/src/applications/rated-disabilities/tests/components/RatingLists/ListItem.unit.spec.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; + +import { ListItem } from '../../../components/RatingLists'; + +describe('', () => { + context('Service-connected rating', () => { + const rating = { + decision: 'Not Service Connected', + diagnosticText: 'Diabetes Mellitus', + diagnosticTypeName: 'Diabetes mellitus0', + effectiveDate: '2023-11-20', + ratingPercentage: 20, + }; + + it('should render the rating name w. a percentage', () => { + const screen = render(); + + expect(screen.getByText('20% rating for Diabetes Mellitus')).to.exist; + }); + + it('should display the effective date of the rating', () => { + const screen = render(); + + expect(screen.getByText('November 20, 2023')).to.exist; + }); + }); + + context('Non-service-connected rating', () => { + const rating = { + decision: 'Not Service Connected', + diagnosticText: 'Tinnitus', + diagnosticTypeName: 'tinnitus', + effectiveDate: null, + ratingPercentage: null, + }; + + it('should render the rating name w/o a percentage', () => { + const screen = render(); + + expect(screen.getByText('Tinnitus')).to.exist; + }); + + it('should not display the effective date field', () => { + const screen = render(); + + expect(screen.queryByText('Effective date:')).not.to.exist; + }); + }); +}); diff --git a/src/applications/rated-disabilities/tests/components/RatingLists/RatingLists.unit.spec.jsx b/src/applications/rated-disabilities/tests/components/RatingLists/RatingLists.unit.spec.jsx new file mode 100644 index 000000000000..e623ac87d741 --- /dev/null +++ b/src/applications/rated-disabilities/tests/components/RatingLists/RatingLists.unit.spec.jsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; + +import { + $, + $$, +} from '@department-of-veterans-affairs/platform-forms-system/ui'; + +import RatingLists, { helpers } from '../../../components/RatingLists'; + +const nonServiceConnectedSectionTitle = + 'Conditions VA determined aren’t service-connected'; +const serviceConnectedSectionTitle = 'Service-connected ratings'; + +const ratings = [ + { + decision: 'Service Connected', + diagnosticText: 'Hearing Loss', + diagnosticTypeName: '6100-Hearing loss', + effectiveDate: '2005-01-01', + ratingPercentage: 20, + }, + { + decision: 'Service Connected', + diagnosticText: 'Allergies due to Hearing Loss', + diagnosticTypeName: 'Limitation of flexion, knee', + effectiveDate: '2012-05-01', + ratingPercentage: 100, + }, + { + decision: 'Service Connected', + diagnosticText: 'Sarcoma Soft-Tissue', + diagnosticTypeName: 'Soft tissue sarcoma (neurogenic origin)', + effectiveDate: '2018-08-01', + ratingPercentage: 80, + }, + { + decision: 'Not Service Connected', + diagnosticText: 'Tinnitus', + diagnosticTypeName: 'Tinnitus', + effectiveDate: null, + ratingPercentage: null, + }, + { + decision: 'Not Service Connected', + diagnosticText: 'Diabetes', + diagnosticTypeName: 'Diabetes mellitus', + effectiveDate: null, + ratingPercentage: null, + }, +]; + +const serviceConnectedRatingsOnly = ratings.slice(0, 3); +const nonServiceConnectedRatingsOnly = ratings.slice(3); + +describe('', () => { + context('when there are no ratings', () => { + it('should display an alert indicating that there are no ratings', () => { + const screen = render(); + + expect( + screen.getByText( + 'We don’t have any rated disabilities on file for you', + ), + ).to.exist; + }); + }); + + context( + 'when there are a mix of service-connected and non-service-connected ratings', + () => { + it('should display both sections', () => { + const screen = render(); + + expect(screen.getByText(serviceConnectedSectionTitle)).to.exist; + expect(screen.getByText(nonServiceConnectedSectionTitle)).to.exist; + }); + + it('should display a total of five ratings', () => { + const screen = render(); + + const cards = screen.getAllByRole('heading', { level: 4 }); + expect(cards.length).to.equal(5); + }); + + it('should sort service-connected ratings by effective date (most to least recent)', () => { + const { container } = render(); + + // Get the first rating list (Service connected ratings) and check + // that the cards is sorted correctly by evaluating the order of the card headings + const list = $$('.rating-list', container)[0]; + const listItems = $$('h4', list); + + expect(listItems[0].textContent).to.equal( + helpers.getHeadingText(ratings[2]), + ); + expect(listItems[1].textContent).to.equal( + helpers.getHeadingText(ratings[1]), + ); + expect(listItems[2].textContent).to.equal( + helpers.getHeadingText(ratings[0]), + ); + }); + }, + ); + + context('when there are only service-connected ratings', () => { + it('should only display the service-connected ratings section', () => { + const screen = render( + , + ); + + expect(screen.getByText(serviceConnectedSectionTitle)).to.exist; + expect(screen.queryByText(nonServiceConnectedSectionTitle)).not.to.exist; + }); + + it('should display a total of three ratings', () => { + const screen = render( + , + ); + + const cards = screen.getAllByRole('heading', { level: 4 }); + expect(cards.length).to.equal(3); + }); + + it('should sort service-connected ratings by effective date (most to least recent)', () => { + const { container } = render( + , + ); + + // Get the rating list (Service connected ratings) and check + // that the cards is sorted correctly by evaluating the order of the card headings + const list = $('.rating-list', container); + const listItems = $$('h4', list); + + expect(listItems[0].textContent).to.equal( + helpers.getHeadingText(ratings[2]), + ); + expect(listItems[1].textContent).to.equal( + helpers.getHeadingText(ratings[1]), + ); + expect(listItems[2].textContent).to.equal( + helpers.getHeadingText(ratings[0]), + ); + }); + }); + + context('when there are only non-service-connected ratings', () => { + it('should only display the non-service-connected ratings section', () => { + const screen = render( + , + ); + + expect(screen.queryByText(serviceConnectedSectionTitle)).not.to.exist; + expect(screen.getByText(nonServiceConnectedSectionTitle)).to.exist; + }); + + it('should display a total of two ratings', () => { + const screen = render( + , + ); + + const cards = screen.getAllByRole('heading', { level: 4 }); + expect(cards.length).to.equal(2); + }); + }); +}); diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/200-response.json b/src/applications/rated-disabilities/tests/e2e/fixtures/200-response.json new file mode 100644 index 000000000000..4096cc6c0cdf --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/200-response.json @@ -0,0 +1,63 @@ +{ + "data": { + "id": null, + "type": "disability_ratings", + "attributes": { + "combinedDisabilityRating": 100, + "combinedEffectiveDate": "2019-01-01", + "legalEffectiveDate": "2018-12-31", + "individualRatings": [ + { + "decision": "Service Connected", + "effectiveDate": "2005-01-01", + "ratingEndDate": null, + "ratingPercentage": 100, + "diagnosticTypeCode": "6100", + "diagnosticTypeName": "6100-Hearing loss", + "diagnosticText": "Hearing Loss", + "disabilityRatingId": "1" + }, + { + "decision": "Service Connected", + "effectiveDate": "2012-05-01", + "ratingEndDate": null, + "ratingPercentage": 10, + "diagnosticTypeCode": "5260", + "diagnosticTypeName": "Limitation of flexion, knee", + "diagnosticText": "Allergies due to Hearing Loss", + "disabilityRatingId": "2" + }, + { + "decision": "Service Connected", + "effectiveDate": "2018-08-01", + "ratingEndDate": null, + "ratingPercentage": 0, + "diagnosticTypeCode": "8540", + "diagnosticTypeName": "Soft tissue sarcoma (neurogenic origin)", + "diagnosticText": "Sarcoma Soft-Tissue", + "disabilityRatingId": "3" + }, + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "6260", + "diagnosticTypeName": "Tinnitus", + "diagnosticText": "Tinnitus", + "disabilityRatingId": "4" + }, + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "7913", + "diagnosticTypeName": "Diabetes mellitus", + "diagnosticText": "Diabetes", + "disabilityRatingId": "5" + } + ] + } + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/feature-toggle-enabled.json b/src/applications/rated-disabilities/tests/e2e/fixtures/feature-toggle-enabled.json new file mode 100644 index 000000000000..2bf6e8ffb42a --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/feature-toggle-enabled.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "feature_toggles", + "features": [ + { + "name": "rated_disabilities_use_lighthouse", + "value": true + } + ] + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/no-combined-rating.json b/src/applications/rated-disabilities/tests/e2e/fixtures/no-combined-rating.json new file mode 100644 index 000000000000..2f13191fd895 --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/no-combined-rating.json @@ -0,0 +1,63 @@ +{ + "data": { + "id": null, + "type": "disability_ratings", + "attributes": { + "combinedDisabilityRating": null, + "combinedEffectiveDate": "2019-01-01", + "legalEffectiveDate": "2018-12-31", + "individualRatings": [ + { + "decision": "Service Connected", + "effectiveDate": "2005-01-01", + "ratingEndDate": null, + "ratingPercentage": 100, + "diagnosticTypeCode": "6100", + "diagnosticTypeName": "6100-Hearing loss", + "diagnosticText": "Hearing Loss", + "disabilityRatingId": "1" + }, + { + "decision": "Service Connected", + "effectiveDate": "2012-05-01", + "ratingEndDate": null, + "ratingPercentage": 10, + "diagnosticTypeCode": "5260", + "diagnosticTypeName": "Limitation of flexion, knee", + "diagnosticText": "Allergies due to Hearing Loss", + "disabilityRatingId": "2" + }, + { + "decision": "Service Connected", + "effectiveDate": "2018-08-01", + "ratingEndDate": null, + "ratingPercentage": 0, + "diagnosticTypeCode": "8540", + "diagnosticTypeName": "Soft tissue sarcoma (neurogenic origin)", + "diagnosticText": "Sarcoma Soft-Tissue", + "disabilityRatingId": "3" + }, + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "6260", + "diagnosticTypeName": "Tinnitus", + "diagnosticText": "Tinnitus", + "disabilityRatingId": "4" + }, + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "7913", + "diagnosticTypeName": "Diabetes mellitus", + "diagnosticText": "Diabetes", + "disabilityRatingId": "5" + } + ] + } + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/no-ratings.json b/src/applications/rated-disabilities/tests/e2e/fixtures/no-ratings.json new file mode 100644 index 000000000000..0b7549e6af32 --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/no-ratings.json @@ -0,0 +1,12 @@ +{ + "data": { + "id": null, + "type": "disability_ratings", + "attributes": { + "combinedDisabilityRating": 20, + "combinedEffectiveDate": "2020-01-01", + "legalEffectiveDate": "2020-12-31", + "individualRatings": [] + } + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/non-service-connected-only.json b/src/applications/rated-disabilities/tests/e2e/fixtures/non-service-connected-only.json new file mode 100644 index 000000000000..42790c63b457 --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/non-service-connected-only.json @@ -0,0 +1,33 @@ +{ + "data": { + "id": null, + "type": "disability_ratings", + "attributes": { + "combinedDisabilityRating": 70, + "combinedEffectiveDate": "2019-01-01", + "legalEffectiveDate": "2018-12-31", + "individualRatings": [ + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "6260", + "diagnosticTypeName": "Tinnitus", + "diagnosticText": "Tinnitus", + "disabilityRatingId": "1" + }, + { + "decision": "Not Service Connected", + "effectiveDate": null, + "ratingEndDate": null, + "ratingPercentage": null, + "diagnosticTypeCode": "7913", + "diagnosticTypeName": "Diabetes mellitus", + "diagnosticText": "Diabetes", + "disabilityRatingId": "2" + } + ] + } + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/fixtures/service-connected-only.json b/src/applications/rated-disabilities/tests/e2e/fixtures/service-connected-only.json new file mode 100644 index 000000000000..6e254c1fc687 --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/fixtures/service-connected-only.json @@ -0,0 +1,43 @@ +{ + "data": { + "id": null, + "type": "disability_ratings", + "attributes": { + "combinedDisabilityRating": 90, + "combinedEffectiveDate": "2019-01-01", + "legalEffectiveDate": "2018-12-31", + "individualRatings": [ + { + "decision": "Service Connected", + "effectiveDate": "2005-01-01", + "ratingEndDate": null, + "ratingPercentage": 100, + "diagnosticTypeCode": "6100", + "diagnosticTypeName": "6100-Hearing loss", + "diagnosticText": "Hearing Loss", + "disabilityRatingId": "1" + }, + { + "decision": "Service Connected", + "effectiveDate": "2012-05-01", + "ratingEndDate": null, + "ratingPercentage": 10, + "diagnosticTypeCode": "5260", + "diagnosticTypeName": "Limitation of flexion, knee", + "diagnosticText": "Allergies due to Hearing Loss", + "disabilityRatingId": "2" + }, + { + "decision": "Service Connected", + "effectiveDate": "2018-08-01", + "ratingEndDate": null, + "ratingPercentage": 0, + "diagnosticTypeCode": "8540", + "diagnosticTypeName": "Soft tissue sarcoma (neurogenic origin)", + "diagnosticText": "Sarcoma Soft-Tissue", + "disabilityRatingId": "3" + } + ] + } + } +} diff --git a/src/applications/rated-disabilities/tests/e2e/rated-disabilities-lighthouse.cypress.spec.js b/src/applications/rated-disabilities/tests/e2e/rated-disabilities-lighthouse.cypress.spec.js new file mode 100644 index 000000000000..b2296d65d6ee --- /dev/null +++ b/src/applications/rated-disabilities/tests/e2e/rated-disabilities-lighthouse.cypress.spec.js @@ -0,0 +1,93 @@ +import featureToggleEnabled from './fixtures/feature-toggle-enabled.json'; +import serviceConnectedOnly from './fixtures/service-connected-only.json'; +import noCombinedRating from './fixtures/no-combined-rating.json'; +import noRatings from './fixtures/no-ratings.json'; +import nonServiceConnectedOnly from './fixtures/non-service-connected-only.json'; + +const RATED_DISABILITIES_PATH = '/disability/view-disability-rating/rating'; + +describe('View rated disabilities', () => { + beforeEach(() => { + cy.intercept('GET', '/v0/feature_toggles?*', featureToggleEnabled).as( + 'featureToggleEnabled', + ); + + cy.login(); + }); + + context('when there is no combined rating', () => { + beforeEach(() => { + cy.intercept('v0/rated_disabilities', noCombinedRating); + cy.visit(RATED_DISABILITIES_PATH); + }); + + it('should display an alert indicating that there is no combined rating', () => { + cy.findByText( + 'We don’t have a combined disability rating on file for you', + ).should('exist'); + cy.get('va-featured-content').should('not.exist'); + + cy.injectAxeThenAxeCheck(); + }); + }); + + context('when there are no ratings', () => { + beforeEach(() => { + cy.intercept('v0/rated_disabilities', noRatings); + cy.visit(RATED_DISABILITIES_PATH); + }); + + it('should display an alert indicating that there are no ratings', () => { + cy.findByText( + 'We don’t have any rated disabilities on file for you', + ).should('exist'); + cy.get('.rating-list').should('not.exist'); + + cy.injectAxeThenAxeCheck(); + }); + }); + + context('when there are only service-connected ratings', () => { + beforeEach(() => { + cy.intercept('v0/rated_disabilities', serviceConnectedOnly); + cy.visit(RATED_DISABILITIES_PATH); + }); + + it('should display a list of service-connected ratings', () => { + cy.findByText('Service-connected ratings').should('exist'); + cy.get('.rating-list > va-card').should('have.length', 3); + + cy.injectAxeThenAxeCheck(); + }); + + it('should not display the non-service-connected ratings section', () => { + cy.findByText('Conditions VA determined aren’t service-connected').should( + 'not.exist', + ); + + cy.injectAxeThenAxeCheck(); + }); + }); + + context('when there are only non-service-connected ratings', () => { + beforeEach(() => { + cy.intercept('v0/rated_disabilities', nonServiceConnectedOnly); + cy.visit(RATED_DISABILITIES_PATH); + }); + + it('should display a list of service-connected ratings', () => { + cy.findByText('Conditions VA determined aren’t service-connected').should( + 'exist', + ); + cy.get('.rating-list > va-card').should('have.length', 2); + + cy.injectAxeThenAxeCheck(); + }); + + it('should not display the service-connected ratings section', () => { + cy.findByText('Service-connected ratings').should('not.exist'); + + cy.injectAxeThenAxeCheck(); + }); + }); +}); diff --git a/src/applications/rated-disabilities/util/index.js b/src/applications/rated-disabilities/util/index.js index 79c1d2989268..7ec4562297e9 100644 --- a/src/applications/rated-disabilities/util/index.js +++ b/src/applications/rated-disabilities/util/index.js @@ -1,4 +1,5 @@ -import { apiRequest } from 'platform/utilities/api'; +import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/exports'; +import { isValid, format, parseISO } from 'date-fns'; const SERVER_ERROR_REGEX = /^5\d{2}$/; const CLIENT_ERROR_REGEX = /^4\d{2}$/; @@ -15,3 +16,15 @@ export async function getData(apiRoute, options) { export const isServerError = errCode => SERVER_ERROR_REGEX.test(errCode); export const isClientError = errCode => CLIENT_ERROR_REGEX.test(errCode); + +// Takes a format string and returns a function that formats the given date +// `date` must be in ISO format ex. 2020-01-28 +export const buildDateFormatter = formatString => { + return date => { + const parsedDate = parseISO(date); + + return isValid(parsedDate) + ? format(parsedDate, formatString) + : 'Invalid date'; + }; +}; diff --git a/src/applications/representative-search/api/RepresentativeFinderApi.js b/src/applications/representative-search/api/RepresentativeFinderApi.js index e00f166456a8..6074a8e8f997 100644 --- a/src/applications/representative-search/api/RepresentativeFinderApi.js +++ b/src/applications/representative-search/api/RepresentativeFinderApi.js @@ -1,5 +1,5 @@ import { fetchAndUpdateSessionExpiration as fetch } from '@department-of-veterans-affairs/platform-utilities/api'; -import { getApi, resolveParamsWithUrl } from '../config'; +import { getApi, resolveParamsWithUrl, endpointOptions } from '../config'; class RepresentativeFinderApi { /** @@ -30,8 +30,8 @@ class RepresentativeFinderApi { const endpoint = type === 'veteran_service_officer' - ? '/vso_accredited_representatives' - : '/other_accredited_representatives'; + ? endpointOptions.fetchVSOReps + : endpointOptions.fetchOtherReps; const { requestUrl, apiSettings } = getApi(endpoint); const startTime = new Date().getTime(); @@ -74,7 +74,7 @@ class RepresentativeFinderApi { } const { requestUrl, apiSettings } = getApi( - '/flag_accredited_representatives', + endpointOptions.flagReps, 'POST', reportRequestBody, ); diff --git a/src/applications/representative-search/config.js b/src/applications/representative-search/config.js index fb742c5c18cb..5ee5a07ee58f 100644 --- a/src/applications/representative-search/config.js +++ b/src/applications/representative-search/config.js @@ -23,6 +23,12 @@ export const searchAreaOptions = { 'Show all': 'Show all', }; +export const endpointOptions = { + fetchVSOReps: `/services/veteran/v0/vso_accredited_representatives`, + fetchOtherReps: `/services/veteran/v0/other_accredited_representatives`, + flagReps: `/representation_management/v0/flag_accredited_representatives`, +}; + /* * Toggle true for local development */ @@ -30,8 +36,8 @@ export const useStagingDataLocally = true; const baseUrl = useStagingDataLocally && environment.BASE_URL === 'http://localhost:3001' - ? `https://staging-api.va.gov/services/veteran/v0` - : `${environment.API_URL}/services/veteran/v0`; + ? `https://staging-api.va.gov` + : `${environment.API_URL}`; /** * Build requestUrl and settings for api calls diff --git a/src/applications/representative-search/tests/api-url-parameters.railsEngine.unit.spec.js b/src/applications/representative-search/tests/api-url-parameters.railsEngine.unit.spec.js index dd003a89b549..6d48678522d6 100644 --- a/src/applications/representative-search/tests/api-url-parameters.railsEngine.unit.spec.js +++ b/src/applications/representative-search/tests/api-url-parameters.railsEngine.unit.spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import environment from '@department-of-veterans-affairs/platform-utilities/environment'; -import { resolveParamsWithUrl, getApi } from '../config'; +import { resolveParamsWithUrl, getApi, endpointOptions } from '../config'; describe('Locator url and parameters builder', () => { const address = '43210'; @@ -13,7 +13,7 @@ describe('Locator url and parameters builder', () => { it('should build VA request with type=veteran_service_officer', () => { const type = 'veteran_service_officer'; - const { requestUrl } = getApi('/vso_accredited_representatives'); + const { requestUrl } = getApi(endpointOptions.fetchVSOReps); const params = resolveParamsWithUrl({ address, @@ -38,7 +38,7 @@ describe('Locator url and parameters builder', () => { it('should build VA request with type=claim_agents', () => { const type = 'claim_agents'; - const { requestUrl } = getApi('/other_accredited_representatives'); + const { requestUrl } = getApi(endpointOptions.fetchOtherReps); const params = resolveParamsWithUrl({ address, @@ -62,7 +62,7 @@ describe('Locator url and parameters builder', () => { it('should build VA request with type=attorney and page = 2 and perPage = 7', () => { const type = 'attorney'; - const { requestUrl } = getApi('/other_accredited_representatives'); + const { requestUrl } = getApi(endpointOptions.fetchOtherReps); const params = resolveParamsWithUrl({ address, @@ -86,12 +86,12 @@ describe('Locator url and parameters builder', () => { it('should set csrfToken in request headers', () => { localStorage.setItem('csrfToken', '12345'); - const { apiSettings } = getApi('/flag_accredited_representatives'); + const { apiSettings } = getApi(endpointOptions.flagReps); expect(apiSettings?.headers?.['X-CSRF-Token']).to.eql('12345'); }); it('should exclude null params from request', () => { - const { requestUrl } = getApi('/other_accredited_representatives'); + const { requestUrl } = getApi(endpointOptions.fetchOtherReps); const params = resolveParamsWithUrl({ address: null, diff --git a/src/applications/representatives/actions/poaRequests.js b/src/applications/representatives/actions/poaRequests.js index 34443573f847..5c14a3c00aa0 100644 --- a/src/applications/representatives/actions/poaRequests.js +++ b/src/applications/representatives/actions/poaRequests.js @@ -1,7 +1,5 @@ import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; -const apiBasePath = 'poa_requests'; - const settings = { method: 'POST', headers: { @@ -9,9 +7,9 @@ const settings = { }, }; -export const declinePOARequest = async veteranId => { +const handlePOARequest = async (veteranId, action) => { try { - const resource = `${apiBasePath}/${veteranId}`; + const resource = `/poa_requests/${veteranId}/${action}`; const response = await apiRequest(resource, settings); if (!response.ok) { @@ -24,3 +22,8 @@ export const declinePOARequest = async veteranId => { return { status: 'error', error: errorMessage }; } }; + +export const acceptPOARequest = veteranId => + handlePOARequest(veteranId, 'accept'); +export const declinePOARequest = veteranId => + handlePOARequest(veteranId, 'decline'); diff --git a/src/applications/representatives/components/PoaRequestsTable/PoaRequestsTable.jsx b/src/applications/representatives/components/PoaRequestsTable/PoaRequestsTable.jsx new file mode 100644 index 000000000000..8260c9adc158 --- /dev/null +++ b/src/applications/representatives/components/PoaRequestsTable/PoaRequestsTable.jsx @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { acceptPOARequest, declinePOARequest } from '../../actions/poaRequests'; + +const isActionable = status => status === 'Pending'; + +const PoaRequestsTable = ({ poaRequests }) => { + return ( + + + Claimant + Submitted + Description + Status + Actions + + {poaRequests.map(({ id, name, date, description, status }) => ( + + {name} + {date} + {description} + {status} + + {isActionable(status) && ( + <> + acceptPOARequest(id)} + /> + declinePOARequest(id)} + /> + + )} + + + ))} + + ); +}; + +PoaRequestsTable.propTypes = { + poaRequests: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + date: PropTypes.string, + description: PropTypes.string, + status: PropTypes.string, + }), + ).isRequired, +}; + +export default PoaRequestsTable; diff --git a/src/applications/representatives/components/PoaRequestsWidget/PoaRequestsWidget.jsx b/src/applications/representatives/components/PoaRequestsWidget/PoaRequestsWidget.jsx index 1bfa06130902..ff904e4d1752 100644 --- a/src/applications/representatives/components/PoaRequestsWidget/PoaRequestsWidget.jsx +++ b/src/applications/representatives/components/PoaRequestsWidget/PoaRequestsWidget.jsx @@ -3,13 +3,12 @@ import PropTypes from 'prop-types'; const PoaRequestsWidget = ({ poaRequests }) => (
    - - View all - + Claimant diff --git a/src/applications/representatives/containers/Dashboard.jsx b/src/applications/representatives/containers/Dashboard.jsx index f6c14e9e0556..7e575ca692e5 100644 --- a/src/applications/representatives/containers/Dashboard.jsx +++ b/src/applications/representatives/containers/Dashboard.jsx @@ -3,27 +3,10 @@ import React from 'react'; import { Link } from 'react-router'; import PoaRequestsWidget from '../components/PoaRequestsWidget/PoaRequestsWidget'; +import { mockPOARequests } from '../mocks/mockPOARequests'; // import { RequiredLoginView } from '@department-of-veterans-affairs/platform-user/RequiredLoginView'; -const dummyPoaRequestData = [ - { - name: 'John Smith', - id: 12345, - date: '24 JAN 2024 09:00AM', - }, - { - name: 'Madaline Rouge', - id: 12345, - date: '25 JAN 2024 09:00AM', - }, - { - name: 'Arnold R. Ford', - id: 12345, - date: '30 JAN 2024 10:00AM', - }, -]; - import LoginViewWrapper from './LoginViewWrapper'; const Dashboard = ({ POApermissions = true }) => { @@ -50,7 +33,7 @@ const Dashboard = ({ POApermissions = true }) => {
    - +
    diff --git a/src/applications/representatives/containers/POARequests.jsx b/src/applications/representatives/containers/POARequests.jsx index 718ffe08140c..4274f0ae5b35 100644 --- a/src/applications/representatives/containers/POARequests.jsx +++ b/src/applications/representatives/containers/POARequests.jsx @@ -1,5 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; +import PoaRequestsTable from '../components/PoaRequestsTable/PoaRequestsTable'; +import { mockPOARequests } from '../mocks/mockPOARequests'; import LoginViewWrapper from './LoginViewWrapper'; const POARequests = ({ POApermissions = true }) => { @@ -11,25 +13,7 @@ const POARequests = ({ POApermissions = true }) => { return (

    Power of attorney requests

    - -
    -
    -
    -
    -
    -
    + ); }; diff --git a/src/applications/representatives/mocks/mockPOARequests.js b/src/applications/representatives/mocks/mockPOARequests.js new file mode 100644 index 000000000000..c0ee2e6845e7 --- /dev/null +++ b/src/applications/representatives/mocks/mockPOARequests.js @@ -0,0 +1,23 @@ +export const mockPOARequests = [ + { + id: 200, + name: 'Johnathon Smith', + date: 'Jan 24, 2024 11:00 a.m.', + description: 'This is a description of the claimant', + status: 'Pending', + }, + { + id: 400, + name: 'Madaline Rouge', + date: 'Jan 25, 2024 10:00 a.m.', + description: 'This is a different description of different claimant', + status: 'Accepted', + }, + { + id: 500, + name: 'Arnold Ford', + date: 'Jan 26, 2024 11:00 p.m.', + description: 'This is another description of a claimant', + status: 'Pending', + }, +]; diff --git a/src/applications/representatives/tests/components/Dashboard.unit.spec.js b/src/applications/representatives/tests/components/Dashboard.unit.spec.js index 2861213018ea..c22d87dc9787 100644 --- a/src/applications/representatives/tests/components/Dashboard.unit.spec.js +++ b/src/applications/representatives/tests/components/Dashboard.unit.spec.js @@ -46,8 +46,10 @@ describe('Dashboard', () => { }); it('renders view all link', () => { - const { getByText } = render(); - expect(getByText('View all')).to.exist; + const { getByTestId } = render(); + expect( + getByTestId('view-all-poa-requests-link').getAttribute('text'), + ).to.equal('View all'); }); }); }); diff --git a/src/applications/representatives/tests/components/POARequests.unit.spec.js b/src/applications/representatives/tests/components/POARequests.unit.spec.js index 4220f8e628cf..3ff02969b259 100644 --- a/src/applications/representatives/tests/components/POARequests.unit.spec.js +++ b/src/applications/representatives/tests/components/POARequests.unit.spec.js @@ -3,8 +3,9 @@ import { expect } from 'chai'; import React from 'react'; import POARequests from '../../containers/POARequests'; +import { mockPOARequests } from '../../mocks/mockPOARequests'; -describe('POARequests', () => { +describe('POARequests page', () => { it('renders', () => { render(); }); @@ -23,18 +24,47 @@ describe('POARequests', () => { expect(getByText('Power of attorney requests')).to.exist; }); - it('renders search input', () => { - const { getByLabelText } = render(); - expect(getByLabelText('Search')).to.exist; - }); - it('renders content when has POA permissions', () => { - const { container } = render(); - expect(container.querySelector('.placeholder-container')).to.exist; + const { getByText } = render(); + expect(getByText('Power of attorney requests')).to.exist; }); it('renders alert header when does not have POA permissions', () => { const { getByText } = render(); expect(getByText('You are missing some permissions')).to.exist; }); + + describe('POA requests table', () => { + it('renders table headers', () => { + const { getByTestId, getByText } = render(); + expect(getByTestId('poa-requests-table')).to.exist; + expect(getByText('Claimant')).to.exist; + expect(getByText('Submitted')).to.exist; + expect(getByText('Description')).to.exist; + expect(getByText('Status')).to.exist; + expect(getByText('Actions')).to.exist; + }); + + it('renders table with mockPOARequests', () => { + const { getByTestId } = render(); + mockPOARequests.forEach(poaRequest => { + expect(getByTestId(`${poaRequest.id}-claimant`)).to.contain.text( + poaRequest.name, + ); + expect(getByTestId(`${poaRequest.id}-submitted`)).to.contain.text( + poaRequest.date, + ); + expect(getByTestId(`${poaRequest.id}-description`)).to.contain.text( + poaRequest.description, + ); + expect(getByTestId(`${poaRequest.id}-status`)).to.contain.text( + poaRequest.status, + ); + if (poaRequest.status === 'Pending') { + expect(getByTestId(`${poaRequest.id}-accept-button`)).to.exist; + expect(getByTestId(`${poaRequest.id}-decline-button`)).to.exist; + } + }); + }); + }); }); diff --git a/src/applications/representatives/tests/e2e/representatives.cypress.spec.js b/src/applications/representatives/tests/e2e/representatives.cypress.spec.js index 63c917e47fe0..12901e7cf6d7 100644 --- a/src/applications/representatives/tests/e2e/representatives.cypress.spec.js +++ b/src/applications/representatives/tests/e2e/representatives.cypress.spec.js @@ -28,5 +28,6 @@ describe('Representatives', () => { cy.injectAxe(); cy.axeCheck(); cy.contains('Power of attorney requests'); + cy.get('[data-testid=poa-requests-table]').should('exist'); }); }); diff --git a/src/applications/simple-forms/21-0966/config/form.js b/src/applications/simple-forms/21-0966/config/form.js index bd88667a86bc..5f67e5b004b8 100644 --- a/src/applications/simple-forms/21-0966/config/form.js +++ b/src/applications/simple-forms/21-0966/config/form.js @@ -5,6 +5,7 @@ import manifest from '../manifest.json'; import ITFStatusLoadingIndicatorPage from '../components/ITFStatusLoadingIndicatorPage'; +import prefillTransformer from './prefill-transformer'; import transformForSubmit from './submit-transformer'; import IntroductionPage from '../containers/IntroductionPage'; import ConfirmationPage from '../containers/ConfirmationPage'; @@ -26,10 +27,11 @@ import { hasActiveCompensationITF, hasActivePensionITF, noActiveITF, + hasVeteranPrefill, benefitSelectionChapterTitle, survivingDependentPersonalInformationChapterTitle, survivingDependentContactInformationChapterTitle, - initializeFormDataWithPreparerIdentification, + initializeFormDataWithPreparerIdentificationAndPrefill, statementOfTruthFullNamePath, veteranPersonalInformationChapterTitle, veteranContactInformationChapterTitle, @@ -37,6 +39,7 @@ import { import survivingDependentBenefitSelection from '../pages/survivingDependentBenefitSelection'; import thirdPartySurvivingDependentBenefitSelection from '../pages/thirdPartySurvivingDependentBenefitSelection'; import veteranPersonalInformation from '../pages/veteranPersonalInformation'; +import confirmVeteranPersonalInformation from '../pages/confirmVeteranPersonalInformation'; import veteranIdentificationInformation from '../pages/veteranIdentificationInformation'; import thirdPartyPreparerFullName from '../pages/thirdPartyPreparerFullName'; import thirdPartyPreparerRole from '../pages/thirdPartyPreparerRole'; @@ -83,6 +86,7 @@ const formConfig = { }, version: 0, prefillEnabled: true, + prefillTransformer, v3SegmentedProgressBar: true, subTitle: 'Intent to File a Claim for Compensation and/or Pension, or Survivors Pension and/or DIC (VA Form 21-0966)', @@ -160,8 +164,9 @@ const formConfig = { } }, updateFormData: (oldFormData, newFormData) => - initializeFormDataWithPreparerIdentification( + initializeFormDataWithPreparerIdentificationAndPrefill( newFormData.preparerIdentification, + newFormData['view:veteranPrefillStore'], ), }, thirdPartyPreparerFullName: { @@ -304,10 +309,20 @@ const formConfig = { title: ({ formData }) => veteranPersonalInformationChapterTitle({ formData }), pages: { + confirmVeteranPersonalInformation: { + path: 'confirm-veteran-personal-information', + depends: formData => + preparerIsVeteran({ formData }) && hasVeteranPrefill({ formData }), + title: 'Confirm the personal information we have on file for you', + uiSchema: confirmVeteranPersonalInformation.uiSchema, + schema: confirmVeteranPersonalInformation.schema, + editModeOnReviewPage: true, + }, veteranPersonalInformation: { path: 'veteran-personal-information', depends: formData => - (preparerIsVeteran({ formData }) && noActiveITF({ formData })) || + (preparerIsVeteran({ formData }) && + !hasVeteranPrefill({ formData })) || preparerIsThirdPartyToTheVeteran({ formData }), title: 'Name and date of birth', uiSchema: veteranPersonalInformation.uiSchema, @@ -332,7 +347,10 @@ const formConfig = { veteranIdentificationInformation: { path: 'veteran-identification-information', title: 'Identification information', - depends: formData => noActiveITF({ formData }), + depends: formData => + !preparerIsVeteran({ formData }) || + (preparerIsVeteran({ formData }) && + !hasVeteranPrefill({ formData })), uiSchema: veteranIdentificationInformation.uiSchema, schema: veteranIdentificationInformation.schema, }, @@ -360,7 +378,8 @@ const formConfig = { veteranMailingAddress: { path: 'veteran-mailing-address', depends: formData => - (preparerIsVeteran({ formData }) && noActiveITF({ formData })) || + (preparerIsVeteran({ formData }) && + !hasVeteranPrefill({ formData })) || preparerIsThirdPartyToTheVeteran({ formData }), title: 'Mailing address', uiSchema: veteranMailingAddress.uiSchema, @@ -369,7 +388,8 @@ const formConfig = { veteranPhoneAndEmailAddress: { path: 'veteran-phone-and-email-address', depends: formData => - (preparerIsVeteran({ formData }) && noActiveITF({ formData })) || + (preparerIsVeteran({ formData }) && + !hasVeteranPrefill({ formData })) || preparerIsThirdPartyToTheVeteran({ formData }), title: 'Phone and email address', uiSchema: veteranPhoneAndEmailAddress.uiSchema, diff --git a/src/applications/simple-forms/21-0966/config/helpers.js b/src/applications/simple-forms/21-0966/config/helpers.js index 62494b85cdc4..9c6bf7f11b40 100644 --- a/src/applications/simple-forms/21-0966/config/helpers.js +++ b/src/applications/simple-forms/21-0966/config/helpers.js @@ -1,5 +1,4 @@ import { isEmpty } from 'lodash'; -import set from '@department-of-veterans-affairs/platform-forms-system/set'; import { createInitialState } from '@department-of-veterans-affairs/platform-forms-system/state/helpers'; import { preparerIdentifications, @@ -57,6 +56,14 @@ export const noActiveITF = ({ formData } = {}) => { ); }; +export const hasVeteranPrefill = ({ formData } = {}) => { + return ( + !isEmpty(formData?.['view:veteranPrefillStore']?.fullName) && + !isEmpty(formData?.['view:veteranPrefillStore']?.ssn) && + !isEmpty(formData?.['view:veteranPrefillStore']?.dateOfBirth) + ); +}; + export const statementOfTruthFullNamePath = ({ formData } = {}) => { if (preparerIsThirdParty({ formData })) { return 'thirdPartyPreparerFullName'; @@ -129,12 +136,15 @@ export const veteranContactInformationChapterTitle = ({ formData } = {}) => { return 'Veteran’s contact information'; }; -export const initializeFormDataWithPreparerIdentification = preparerIdentification => { - return set( - 'preparerIdentification', +export const initializeFormDataWithPreparerIdentificationAndPrefill = ( + preparerIdentification, + veteranPrefillStore, +) => { + return { + ...createInitialState(formConfig).data, preparerIdentification, - createInitialState(formConfig).data, - ); + 'view:veteranPrefillStore': veteranPrefillStore, + }; }; export const confirmationPageFormBypassed = formData => { diff --git a/src/applications/simple-forms/21-0966/config/prefill-transformer.js b/src/applications/simple-forms/21-0966/config/prefill-transformer.js new file mode 100644 index 000000000000..fadfaa7abac6 --- /dev/null +++ b/src/applications/simple-forms/21-0966/config/prefill-transformer.js @@ -0,0 +1,17 @@ +export default function prefillTransformer(pages, formData, metadata) { + return { + pages, + formData: { + 'view:veteranPrefillStore': { + fullName: formData.veteran.fullName, + dateOfBirth: formData.veteran.dateOfBirth, + ssn: formData.veteran.ssn, + address: formData.veteran.address, + homePhone: formData.veteran.homePhone, + mobilePhone: formData.veteran.mobilePhone, + email: formData.veteran.email, + }, + }, + metadata, + }; +} diff --git a/src/applications/simple-forms/21-0966/containers/IntroductionPage.jsx b/src/applications/simple-forms/21-0966/containers/IntroductionPage.jsx index b1f8b52d3a82..db11d4d55a88 100644 --- a/src/applications/simple-forms/21-0966/containers/IntroductionPage.jsx +++ b/src/applications/simple-forms/21-0966/containers/IntroductionPage.jsx @@ -27,23 +27,61 @@ class IntroductionPage extends React.Component { />

    Use this form if you plan to file a disability or pension claim. If - you notify us of your intent to file, you may be able to get - retroactive payments (payments for the time between when you submit - your intent to file and when we approve your claim). An intent to file - sets a potential start date (or effective date) for your benefits. + you notify us of your intent to file and we approve your claim, you + may be able to get retroactive payments. Retroactive payments are + payments for the time between when we processed your intent to file + and when we approved your claim. An intent to file sets a potential + start date (or effective date) for your benefits.

    What to know before you fill out this form

    +
      +
    • + After you submit your intent to file, you have{' '} + 1 year to complete and file your claim. After 1 + year, the potential effective date for your benefits will expire. +
    • +
    • + In some cases, it may take us a few days to process your intent to + file after you submit it. We’ll let you know what your potential + effective date is after we process your intent to file. +
    • +
    +

    What to know if you’re signing on behalf of someone else

    +

    + We’ll need to have one of these forms showing that you’re authorized + to sign for the person filing the claim: +

    + +

    - After you submit your intent to file, you have 1 year to - complete and file your claim. After 1 year, the potential effective - date for your benefits will expire. + Note: If you’ve already submitted one of these forms, + you don’t need to do anything else. If you haven’t, submit one of + these forms before you submit an intent to file.

    @@ -55,18 +93,18 @@ class IntroductionPage extends React.Component {

    Claims you can file after filling out this form

    - After you complete this form, we’ll direct you to the benefit - application you need to complete. + After you complete this form, we’ll direct you to one or more of these + benefit applications for you to complete:

    {getPeriodsToVerify()} - + text="Verify enrollment" + data-testid="Verify enrollment" + />
    )} @@ -161,7 +159,7 @@ const mapDispatchToProps = { PeriodsToVerify.propTypes = { dispatchUpdatePendingVerifications: PropTypes.func, dispatchUpdateVerifications: PropTypes.func, - enrollmentData: PropTypes.func, + enrollmentData: PropTypes.object, }; export default connect( mapStateToProps, diff --git a/src/applications/verify-your-enrollment/containers/BenefitsProfilePageWrapper.jsx b/src/applications/verify-your-enrollment/containers/BenefitsProfilePageWrapper.jsx index 9cbc27ae654e..b97327ae2675 100644 --- a/src/applications/verify-your-enrollment/containers/BenefitsProfilePageWrapper.jsx +++ b/src/applications/verify-your-enrollment/containers/BenefitsProfilePageWrapper.jsx @@ -57,7 +57,7 @@ const BenefitsProfileWrapper = ({ children }) => { zip: addressLine6, }} /> - + { +const ChangeOfDirectDepositWrapper = ({ applicantName }) => { + const prefix = 'GI-Bill-Chapters-'; const [toggleDirectDepositForm, setToggleDirectDepositForm] = useState(false); const [screenWidth, setScreenWidth] = useState(window.innerWidth); - const [formData, setFormData] = useState({}); - - const PREFIX = 'GI-Bill-Chapters-'; + const [formData, setFormData] = useState(); + const dispatch = useDispatch(); + const { loading, error, data: response } = useSelector( + state => state.bankInfo, + ); const scrollToTopOfForm = () => { scrollToElement('Direct deposit information'); }; - const handleCloseForm = () => { + const handleCloseForm = useCallback(() => { setFormData({}); // clear form data setToggleDirectDepositForm(false); scrollToTopOfForm(); - }; - + }, []); // called when submitting form const saveBankInfo = () => { - // commented out until tied in with redux - // const fields = { - // bankname: formData[`${PREFIX}BankName`], - // bankPhone: formData[`${PREFIX}BankPhone`], - // routingNumber: formData[`${PREFIX}RoutingNumber`], - // accountNumber: formData[`${PREFIX}AccountNumber`], - // accountType: formData[`${PREFIX}AccountType`], - - // }; - handleCloseForm(); // close directDeposit form - // add redux logic here when API is available + // commented out until tied in with redu + const fields = { + phone: formData[`${prefix}phone`], + // phone2: formData[`${prefix}phone`], + fullName: formData[`${prefix}fullName`], + email: formData[`${prefix}email`], + acctType: formData[`${prefix}AccountType`], + routingNo: formData[`${prefix}RoutingNumber`], + acctNo: formData[`${prefix}AccountNumber`], + bankName: formData[`${prefix}BankName`], + bankPhone: formData[`${prefix}BankPhone`], + }; + dispatch(updateBankInfo(fields)); }; + useEffect( + () => { + if (!loading) { + handleCloseForm(); + } + }, + [handleCloseForm, loading], + ); const directDepositDescription = (

    @@ -111,21 +128,6 @@ const ChangeOfDirectDepositWrapper = () => { }; }, []); - // scroll to top of div when edit page is canceled or saved - // useEffect( - // () => { - // if (!toggleDirectDepositForm) { - // scrollToElement('Direct deposit information'); - // // const element = document.getElementById('Direct deposit information'); - // // if (element) { - // // element.scrollIntoView({ behavior: 'smooth' }); - // // } - // } - // // } - // }, - // [toggleDirectDepositForm], - // ); - return (

    @@ -148,6 +150,18 @@ const ChangeOfDirectDepositWrapper = () => { onClick={handleAddNewClick} text={DIRECT_DEPOSIT_BUTTON_TEXT} /> + {error && ( + + )} + {response?.ok && ( + + )} {

    Add new account

    {directDepositDescription} + {loading && } setFormData(data)} - formPrefix={PREFIX} + formPrefix={prefix} formSubmit={saveBankInfo} > { handleCloseForm(); }} data-qa="cancel-button" - data-testid={`${PREFIX}form-cancel-button`} + data-testid={`${prefix}form-cancel-button`} />
    @@ -212,5 +228,7 @@ const ChangeOfDirectDepositWrapper = () => {
    ); }; - +ChangeOfDirectDepositWrapper.propTypes = { + applicantName: PropTypes.string, +}; export default ChangeOfDirectDepositWrapper; diff --git a/src/applications/verify-your-enrollment/hooks/useData.js b/src/applications/verify-your-enrollment/hooks/useData.js index fe2461ca73c9..38de7473567d 100644 --- a/src/applications/verify-your-enrollment/hooks/useData.js +++ b/src/applications/verify-your-enrollment/hooks/useData.js @@ -1,28 +1,30 @@ import { useEffect } from 'react'; +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; import { useDispatch, useSelector } from 'react-redux'; import { translateDateIntoMonthDayYearFormat } from '../helpers'; -import { getData } from '../actions'; +import { fetchPersonalInfo, getData } from '../actions'; export const useData = () => { // This custom hook is for fetching and preparing user data from the Redux state. const dispatch = useDispatch(); const { data, loading } = useSelector(state => state.getDataReducer); - // const { personalInfo } = useSelector(state => state.personalInfo); + const { personalInfo } = useSelector(state => state.personalInfo); useEffect( () => { dispatch(getData()); - // dispatch(fetchPersonalInfo()); + dispatch(fetchPersonalInfo()); }, [dispatch], ); - const userInfo = data && data['vye::UserInfo']; + const userInfo = + environment.API_URL !== 'http://localhost:3000' + ? data && data['vye::UserInfo'] + : personalInfo && personalInfo['vye::UserInfo']; const date = translateDateIntoMonthDayYearFormat(userInfo?.delDate); return { loading, date, - enrollmentData: userInfo, - // personalInfo, ...userInfo, }; }; diff --git a/src/applications/verify-your-enrollment/reducers/bankInfo.js b/src/applications/verify-your-enrollment/reducers/bankInfo.js new file mode 100644 index 000000000000..c2eaa2a3175e --- /dev/null +++ b/src/applications/verify-your-enrollment/reducers/bankInfo.js @@ -0,0 +1,47 @@ +import { + UPDATE_BANK_INFO, + UPDATE_BANK_INFO_FAILED, + UPDATE_BANK_INFO_SUCCESS, +} from '../actions'; + +const initialState = { + loading: false, + data: null, + error: null, +}; + +const bankInfo = (state = initialState, action) => { + switch (action.type) { + case UPDATE_BANK_INFO: + return { + ...state, + loading: true, + error: null, + }; + case UPDATE_BANK_INFO_SUCCESS: + return { + ...state, + loading: false, + data: action.response, + }; + case UPDATE_BANK_INFO_FAILED: + return { + ...state, + loading: false, + error: action.errors, + }; + case 'RESET_SUCCESS_MESSAGE': + return { + ...state, + data: null, + }; + case 'RESET_ERROR': + return { + ...state, + error: null, + }; + default: + return state; + } +}; +export default bankInfo; diff --git a/src/applications/verify-your-enrollment/reducers/index.js b/src/applications/verify-your-enrollment/reducers/index.js index 644e8079889b..c37ff8e4a73b 100644 --- a/src/applications/verify-your-enrollment/reducers/index.js +++ b/src/applications/verify-your-enrollment/reducers/index.js @@ -1,11 +1,13 @@ import mockData from './mockData'; import getDataReducer from './getData'; import personalInfo from './personalInfo'; +import bankInfo from './bankInfo'; const rootReducer = { mockData, getDataReducer, personalInfo, + bankInfo, }; export default rootReducer; diff --git a/src/applications/verify-your-enrollment/sass/change-of-direct-deposit-wrapper.scss b/src/applications/verify-your-enrollment/sass/change-of-direct-deposit-wrapper.scss index 54782ba7ef27..453f75e32d3c 100644 --- a/src/applications/verify-your-enrollment/sass/change-of-direct-deposit-wrapper.scss +++ b/src/applications/verify-your-enrollment/sass/change-of-direct-deposit-wrapper.scss @@ -10,7 +10,16 @@ } .direct-deposit-form-container{ + position: relative; .ach-submit-btn-auto-width{ width: auto !important; } + .loader{ + position: absolute; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(255,255,255,0.7), + } } \ No newline at end of file diff --git a/src/applications/verify-your-enrollment/tests/actions/index.unit.spec.js b/src/applications/verify-your-enrollment/tests/actions/index.unit.spec.js index 5ab1559cbfd6..6b5674f3be80 100644 --- a/src/applications/verify-your-enrollment/tests/actions/index.unit.spec.js +++ b/src/applications/verify-your-enrollment/tests/actions/index.unit.spec.js @@ -10,6 +10,9 @@ import { FETCH_PERSONAL_INFO, FETCH_PERSONAL_INFO_FAILED, FETCH_PERSONAL_INFO_SUCCESS, + updateBankInfo, + UPDATE_BANK_INFO_SUCCESS, + UPDATE_BANK_INFO_FAILED, } from '../../actions'; const mockData = { user: 'user' }; @@ -49,9 +52,11 @@ describe('getData, creator', () => { apiRequestStub.resolves(response); await fetchPersonalInfo()(dispatch); expect(dispatch.calledWith({ type: FETCH_PERSONAL_INFO })).to.be.true; - expect( - dispatch.calledWith({ type: FETCH_PERSONAL_INFO_SUCCESS, response }), - ).to.be.true; + await waitFor(() => { + expect( + dispatch.calledWith({ type: FETCH_PERSONAL_INFO_SUCCESS, response }), + ).to.be.false; + }); }); it('should FETCH_PERSONAL_INFO and FETCH_PERSONAL_INFO_FAILED when api call is successful', async () => { const errors = { erros: 'some error' }; @@ -62,4 +67,34 @@ describe('getData, creator', () => { .to.be.true; }); }); + describe('updateBankInfo', () => { + it('dispatch UPDATE_BANK_INFO_SUCCESS after a sucessful api request', async () => { + const apiRequestStub2 = sinon.stub(apiModule, 'apiRequest'); + const dispatch = sinon.stub(); + const response = { data: 'test data' }; + apiRequestStub2.resolves(response); + await updateBankInfo()(dispatch); + expect( + dispatch.calledWith({ + type: UPDATE_BANK_INFO_SUCCESS, + response, + }), + ).to.be.true; + apiRequestStub2.restore(); + }); + it('dispatch UPDATE_BANK_INFO_FAILED after a sucessful api request', async () => { + const apiRequestStub2 = sinon.stub(apiModule, 'apiRequest'); + const dispatch = sinon.stub(); + const errors = { erros: 'some error' }; + apiRequestStub2.rejects(errors); + await updateBankInfo()(dispatch); + expect( + dispatch.calledWith({ + type: UPDATE_BANK_INFO_FAILED, + errors, + }), + ).to.be.true; + apiRequestStub2.restore(); + }); + }); }); diff --git a/src/applications/verify-your-enrollment/tests/components/Alert.unit.spec.jsx b/src/applications/verify-your-enrollment/tests/components/Alert.unit.spec.jsx new file mode 100644 index 000000000000..9d04fcb093a3 --- /dev/null +++ b/src/applications/verify-your-enrollment/tests/components/Alert.unit.spec.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { expect } from 'chai'; +import { act, waitFor } from '@testing-library/react'; +import { mount } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import sinon from 'sinon'; +import Alert from '../../components/Alert'; + +// Create a mock store +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('', () => { + let clock; + + it('renders with status and message props', async () => { + const wrapper = mount( + + + , + ); + await waitFor(() => { + expect(wrapper.find(Alert).text()).to.include('error message'); + expect(wrapper.find(Alert).props().status).to.equal('error'); + + wrapper.unmount(); + }); + }); + + it('dispatches RESET_ERROR and RESET_SUCCESS_MESSAGE after 15 seconds', () => { + clock = sinon.useFakeTimers({ + toFake: ['setTimeout', 'clearTimeout'], + }); + const wrapper = mount( + + + , + ); + + act(() => { + clock.tick(15000); + }); + const actions = store.getActions(); + act(() => { + expect(actions).to.deep.include({ type: 'RESET_ERROR' }); + expect(actions).to.deep.include({ type: 'RESET_SUCCESS_MESSAGE' }); + wrapper.unmount(); + }); + }); +}); diff --git a/src/applications/verify-your-enrollment/tests/components/ChangeOfDirectDepositForm.unit.spec.js b/src/applications/verify-your-enrollment/tests/components/ChangeOfDirectDepositForm.unit.spec.js index 32535cbc86de..5a5f895fbcfa 100644 --- a/src/applications/verify-your-enrollment/tests/components/ChangeOfDirectDepositForm.unit.spec.js +++ b/src/applications/verify-your-enrollment/tests/components/ChangeOfDirectDepositForm.unit.spec.js @@ -5,7 +5,6 @@ import { getFormDOM, DefinitionTester, } from '@department-of-veterans-affairs/platform-testing/schemaform-utils'; - import ChangeOfDirectDepositForm, { makeSchemas, } from '../../components/ChangeOfDirectDepositForm'; @@ -41,7 +40,7 @@ describe('Change Of Direct Deposit Form', () => { const formDOM = getFormDOM(screen); formDOM.submitForm(); - expect(formDOM.querySelectorAll('.usa-input-error').length).to.equal(6); + expect(formDOM.querySelectorAll('.usa-input-error').length).to.equal(8); }); it('Should raise one error with the account validation', () => { @@ -57,7 +56,19 @@ describe('Change Of Direct Deposit Form', () => { />, ); const formDOM = getFormDOM(screen); - const accountTypeButton = screen.getByRole('radio', { name: /checking/i }); + const fullName = screen.getByRole('textbox', { + name: "Veteran's Full Name (*Required)", + }); + const VeteranPhone = screen.getByRole('textbox', { + name: "Veteran's Phone Number (*Required)", + }); + const VeteranEmail = screen.getByRole('textbox', { + name: "Veteran's Email Address (*Required)", + }); + const accountTypeButton = screen.container.querySelector( + 'va-radio-option[label="Checking"]', + ); + const bankName = screen.getByRole('textbox', { name: /name of financial institution \(\*required\)/i, }); @@ -75,13 +86,16 @@ describe('Change Of Direct Deposit Form', () => { }); fireEvent.click(accountTypeButton); + fireEvent.change(fullName, { target: { value: 'Jhon Doe' } }); + fireEvent.change(VeteranPhone, { target: { value: '3134567890' } }); + fireEvent.change(VeteranEmail, { + target: { value: 'someemail@gmail.com' }, + }); fireEvent.change(bankName, { target: { value: 'Test Bank Name' } }); fireEvent.change(bankPhone, { target: { value: '1234567890' } }); fireEvent.change(routingNumber, { target: { value: '123456789' } }); fireEvent.change(accountNumber, { target: { value: '123' } }); fireEvent.change(verifyAccountNumber, { target: { value: '123456' } }); - - expect(accountTypeButton.checked).to.be.true; expect(bankName.value).to.equal('Test Bank Name'); expect(bankPhone.value).to.equal('1234567890'); expect(routingNumber.value).to.equal('123456789'); @@ -93,7 +107,7 @@ describe('Change Of Direct Deposit Form', () => { expect(formDOM.querySelectorAll('.usa-input-error').length).to.equal(1); }); - it('Should submit form', () => { + it('Should submit form', async () => { const screen = render( { />, ); const formDOM = getFormDOM(screen); - const accountTypeButton = screen.getByRole('radio', { name: /checking/i }); + const fullName = screen.getByRole('textbox', { + name: "Veteran's Full Name (*Required)", + }); + const accountTypeButton = screen.container.querySelector( + 'va-radio-option[label="Checking"]', + ); const bankName = screen.getByRole('textbox', { name: /name of financial institution \(\*required\)/i, }); + const VeteranPhone = screen.getByRole('textbox', { + name: "Veteran's Phone Number (*Required)", + }); + const VeteranEmail = screen.getByRole('textbox', { + name: "Veteran's Email Address (*Required)", + }); const bankPhone = screen.getByRole('textbox', { name: /telephone number of financial institution \(\*required\)/i, }); @@ -122,23 +147,24 @@ describe('Change Of Direct Deposit Form', () => { const verifyAccountNumber = screen.getByRole('textbox', { name: /verify account number \(\*required\)/i, }); - fireEvent.click(accountTypeButton); + fireEvent.change(fullName, { target: { value: 'Jhon Doe' } }); + fireEvent.change(VeteranPhone, { target: { value: '3134567890' } }); + fireEvent.change(VeteranEmail, { + target: { value: 'someemail@gmail.com' }, + }); fireEvent.change(bankName, { target: { value: 'Test Bank Name' } }); fireEvent.change(bankPhone, { target: { value: '1234567890' } }); fireEvent.change(routingNumber, { target: { value: '123456789' } }); fireEvent.change(accountNumber, { target: { value: '123' } }); fireEvent.change(verifyAccountNumber, { target: { value: '123' } }); - expect(accountTypeButton.checked).to.be.true; expect(bankName.value).to.equal('Test Bank Name'); expect(bankPhone.value).to.equal('1234567890'); expect(routingNumber.value).to.equal('123456789'); expect(accountNumber.value).to.equal('123'); expect(verifyAccountNumber.value).to.equal('123'); - formDOM.submitForm(); - expect(formDOM.querySelectorAll('.usa-input-error').length).to.equal(0); }); }); diff --git a/src/applications/verify-your-enrollment/tests/components/Loader.unit.spec.jsx b/src/applications/verify-your-enrollment/tests/components/Loader.unit.spec.jsx new file mode 100644 index 000000000000..d680454b6771 --- /dev/null +++ b/src/applications/verify-your-enrollment/tests/components/Loader.unit.spec.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import Loader from '../../components/Loader'; + +describe('', () => { + it('should render without crashing', () => { + const wrapper = shallow(); + expect(wrapper.exists()).to.be.ok; + wrapper.unmount(); + }); +}); diff --git a/src/applications/verify-your-enrollment/tests/components/PeriodsToVerify.unit.spec.js b/src/applications/verify-your-enrollment/tests/components/PeriodsToVerify.unit.spec.js index 702bb488b9bf..8afacad45c95 100644 --- a/src/applications/verify-your-enrollment/tests/components/PeriodsToVerify.unit.spec.js +++ b/src/applications/verify-your-enrollment/tests/components/PeriodsToVerify.unit.spec.js @@ -33,9 +33,7 @@ describe('PeriodsToVerify', () => { }, ); - const verifyEnrollmentButton = screen.getByRole('button', { - name: 'Verify enrollment', - }); + const verifyEnrollmentButton = screen.getByTestId('Verify enrollment'); fireEvent.click(verifyEnrollmentButton); await waitFor(() => { const successMessage = screen.getByText( diff --git a/src/applications/verify-your-enrollment/tests/reducers/bankInfo.unit.spec.js b/src/applications/verify-your-enrollment/tests/reducers/bankInfo.unit.spec.js new file mode 100644 index 000000000000..a783d167c09c --- /dev/null +++ b/src/applications/verify-your-enrollment/tests/reducers/bankInfo.unit.spec.js @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import { + UPDATE_BANK_INFO, + UPDATE_BANK_INFO_FAILED, + UPDATE_BANK_INFO_SUCCESS, +} from '../../actions'; +import bankInfo from '../../reducers/bankInfo'; + +describe('bankInfo Reducer', () => { + it('should return the initial state', () => { + expect(bankInfo(undefined, {})).to.deep.equal({ + loading: false, + data: null, + error: null, + }); + }); + + it('should handle UPDATE_BANK_INFO', () => { + const startAction = { + type: UPDATE_BANK_INFO, + }; + expect(bankInfo(undefined, startAction)).to.deep.equal({ + loading: true, + data: null, + error: null, + }); + }); + + it('should handle UPDATE_BANK_INFO_SUCCESS', () => { + const successAction = { + type: UPDATE_BANK_INFO_SUCCESS, + response: { id: 1, name: 'Test Bank' }, + }; + expect(bankInfo(undefined, successAction)).to.deep.equal({ + loading: false, + data: { id: 1, name: 'Test Bank' }, + error: null, + }); + }); + + it('should handle UPDATE_BANK_INFO_FAILED', () => { + const failAction = { + type: UPDATE_BANK_INFO_FAILED, + errors: 'Error updating bank info', + }; + expect(bankInfo(undefined, failAction)).to.deep.equal({ + loading: false, + data: null, + error: 'Error updating bank info', + }); + }); + + it('should handle RESET_SUCCESS_MESSAGE', () => { + const resetSuccessAction = { + type: 'RESET_SUCCESS_MESSAGE', + }; + const initialStateWithData = { + loading: false, + data: { id: 1, name: 'Test Bank' }, + error: null, + }; + expect(bankInfo(initialStateWithData, resetSuccessAction)).to.deep.equal({ + loading: false, + data: null, + error: null, + }); + }); + + it('should handle RESET_ERROR', () => { + const resetErrorAction = { + type: 'RESET_ERROR', + }; + const initialStateWithError = { + loading: false, + data: null, + error: 'Error updating bank info', + }; + expect(bankInfo(initialStateWithError, resetErrorAction)).to.deep.equal({ + loading: false, + data: null, + error: null, + }); + }); +}); diff --git a/src/platform/forms-system/src/js/web-component-patterns/addressPattern.jsx b/src/platform/forms-system/src/js/web-component-patterns/addressPattern.jsx index f50fb733a9c5..3c7e898254ed 100644 --- a/src/platform/forms-system/src/js/web-component-patterns/addressPattern.jsx +++ b/src/platform/forms-system/src/js/web-component-patterns/addressPattern.jsx @@ -203,13 +203,13 @@ export const updateFormDataAddress = ( * ``` * @param {{ * labels?: { - * militaryCheckbox?: string + * militaryCheckbox?: string, * street?: string, * street2?: string, * street3?: string, - * }}, + * }, * omit?: Array, - * required?: Record boolean> + * required?: boolean | Record boolean> * }} [options] * @returns {UISchemaOptions} */ @@ -218,7 +218,10 @@ export function addressUI(options) { let cityMaxLength = 100; const omit = key => options?.omit?.includes(key); - const customRequired = key => options?.required?.[key]; + let customRequired = key => options?.required?.[key]; + if (options?.required === false) { + customRequired = () => () => false; + } /** @type {UISchemaOptions} */ const uiSchema = {}; @@ -564,9 +567,9 @@ export const addressSchema = options => { * street?: string, * street2?: string, * street3?: string, - * }}, + * }, * omit?: Array, - * required?: Record boolean> + * required?: boolean | Record boolean> * }} [options] * @returns {UISchemaOptions} */ diff --git a/src/platform/forms-system/src/js/web-component-patterns/index.js b/src/platform/forms-system/src/js/web-component-patterns/index.js index 54d4290fe8c5..f4b9771c2666 100644 --- a/src/platform/forms-system/src/js/web-component-patterns/index.js +++ b/src/platform/forms-system/src/js/web-component-patterns/index.js @@ -8,6 +8,7 @@ export * from './phonePattern'; export * from './relationshipToVeteranPattern'; export * from './radioPattern'; export * from './arnPattern'; +export * from './selectPattern'; export * from './ssnPattern'; export * from './titlePattern'; export * from './yesNoPattern'; diff --git a/src/platform/forms-system/src/js/web-component-patterns/selectPattern.jsx b/src/platform/forms-system/src/js/web-component-patterns/selectPattern.jsx new file mode 100644 index 000000000000..b14c58c2313c --- /dev/null +++ b/src/platform/forms-system/src/js/web-component-patterns/selectPattern.jsx @@ -0,0 +1,72 @@ +import VaSelectField from '../web-component-fields/VaSelectField'; + +/** + * Web component v3 uiSchema for generic select field + * + * ```js + * // uiSchema + * exampleSelect: selectUI('Select animal') + * exampleSelect: selectUI({ + * title: 'Select animal', + * hint: 'This is a hint', + * }) + * + * // schema: + * exampleSelect: selectSchema(['Cat', 'Dog', 'Octopus']) + * + * // or with labels defined: + * // uiSchema + * exampleSelect: selectSchema({ + * title: 'Select animal', + * labels: { + * cat: 'Cat', + * dog: 'Dog', + * octopus: 'Octopus', + * }, + * errorMessages: { + * required: 'Please select an animal', + * }, + * }) + * + * // schema + * exampleSelect: selectSchema(['cat', 'dog', 'octopus']) + * ``` + * + * @param {string | UIOptions & { + * title?: UISchemaOptions['ui:title'], + * errorMessages?: UISchemaOptions['ui:errorMessages'], + * labelHeaderLevel?: UISchemaOptions['ui:options']['labelHeaderLevel'], + * hint?: string, + * labels?: UISchemaOptions['ui:options']['labels'], + * }} options + * @returns {UISchemaOptions} + */ +export const selectUI = options => { + const { title, description, errorMessages, ...uiOptions } = + typeof options === 'object' ? options : { title: options }; + + return { + 'ui:title': title, + 'ui:description': description, + 'ui:webComponentField': VaSelectField, + 'ui:options': { + ...uiOptions, + }, + 'ui:errorMessages': errorMessages, + }; +}; + +/** + * ```js + * exampleSelect: selectSchema(['Cat', 'Dog', 'Octopus']) + * exampleSelect: selectSchema(['cat', 'dog', 'octopus']) + * ``` + * @param {string[]} labels + * @returns {SchemaOptions} + */ +export const selectSchema = labels => { + return { + type: 'string', + enum: labels, + }; +}; diff --git a/src/platform/forms-system/src/js/web-component-patterns/yesNoPattern.jsx b/src/platform/forms-system/src/js/web-component-patterns/yesNoPattern.jsx index 7e92e89b6b72..8655f7cda45e 100644 --- a/src/platform/forms-system/src/js/web-component-patterns/yesNoPattern.jsx +++ b/src/platform/forms-system/src/js/web-component-patterns/yesNoPattern.jsx @@ -23,6 +23,7 @@ import YesNoField from '../web-component-fields/YesNoField'; * tile?: boolean, * yesNoReverse?: boolean, * hint?: string, + * errorMessages?: UISchemaOptions['ui:errorMessages'], * labelHeaderLevel?: UISchemaOptions['ui:options']['labelHeaderLevel'], * }} options - a string to use as the title or an object with options * @returns {UISchemaOptions} diff --git a/src/platform/forms/save-in-progress/FormSignInModal.jsx b/src/platform/forms/save-in-progress/FormSignInModal.jsx index 76d5856bef38..af40fc789a6d 100644 --- a/src/platform/forms/save-in-progress/FormSignInModal.jsx +++ b/src/platform/forms/save-in-progress/FormSignInModal.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { VaModal } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import Modal from '@department-of-veterans-affairs/component-library/Modal'; import recordEvent from '../../monitoring/record-event'; import { APP_TYPE_DEFAULT } from '../../forms-system/src/js/constants'; @@ -19,29 +19,36 @@ class FormSignInModal extends React.Component { }; render() { + const primaryButton = { + action: this.handleClose, + text: 'Finish applying', + }; + + const secondaryButton = { + action: this.handleSignIn, + text: 'Sign in and start over', + }; const { formConfig } = this.props; const appType = formConfig?.customText?.appType || APP_TYPE_DEFAULT; return ( -

    Since you didn’t sign in before you started, we can’t save your in-progress {appType}.

    If you sign in now, you’ll need to start over.

    -
    + ); } } diff --git a/src/platform/forms/tests/save-in-progress/FormSignInModal.unit.spec.jsx b/src/platform/forms/tests/save-in-progress/FormSignInModal.unit.spec.jsx index 36b08ca7f97f..8ce175ac0eb8 100644 --- a/src/platform/forms/tests/save-in-progress/FormSignInModal.unit.spec.jsx +++ b/src/platform/forms/tests/save-in-progress/FormSignInModal.unit.spec.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { expect } from 'chai'; -import { render } from '@testing-library/react'; +import { shallow } from 'enzyme'; import sinon from 'sinon'; import FormSignInModal from '../../save-in-progress/FormSignInModal'; @@ -27,24 +27,26 @@ describe('', () => { }); it('should render', () => { - const { container } = render(); - expect(container.querySelector('va-modal')).to.exist; + const wrapper = shallow(); + expect(wrapper).to.exist; + wrapper.unmount(); }); it('should close as a primary action', () => { - const { container } = render(); - container.querySelector('va-modal').__events.primaryButtonClick(); + const wrapper = shallow(); + wrapper.prop('primaryButton').action(); expect(props.onClose.calledOnce).to.be.true; expect( global.window.dataLayer.some( ({ event }) => event === 'no-login-finish-form', ), ).to.be.true; + wrapper.unmount(); }); it('should start the sign-in process as a secondary action', () => { - const { container } = render(); - container.querySelector('va-modal').__events.secondaryButtonClick(); + const wrapper = shallow(); + wrapper.prop('secondaryButton').action(); expect(props.onClose.calledOnce).to.be.true; expect(props.onSignIn.calledOnce).to.be.true; expect( @@ -52,5 +54,6 @@ describe('', () => { ({ event }) => event === 'login-link-restart-form', ), ).to.be.true; + wrapper.unmount(); }); }); diff --git a/src/platform/mhv/downtime/tests/containers/MHVDowntime.unit.spec.jsx b/src/platform/mhv/downtime/tests/containers/MHVDowntime.unit.spec.jsx index bc75cf914153..b78f25768553 100644 --- a/src/platform/mhv/downtime/tests/containers/MHVDowntime.unit.spec.jsx +++ b/src/platform/mhv/downtime/tests/containers/MHVDowntime.unit.spec.jsx @@ -1,126 +1,126 @@ -import React from 'react'; -import { expect } from 'chai'; -import { render } from '@testing-library/react'; +// import React from 'react'; +// import { expect } from 'chai'; +// import { render } from '@testing-library/react'; -import { externalServiceStatus } from '@department-of-veterans-affairs/platform-monitoring/exports'; +// import { externalServiceStatus } from '@department-of-veterans-affairs/platform-monitoring/exports'; -import MHVDowntime from '../../containers/MHVDowntime'; +// import MHVDowntime from '../../containers/MHVDowntime'; -describe('MHVDowntime', () => { - it('renders MHVDown when a service is down', () => { - const now = new Date(); - const later = new Date(now).setHours(now.getHours() + 4); +// describe('MHVDowntime', () => { +// it('renders MHVDown when a service is down', () => { +// const now = new Date(); +// const later = new Date(now).setHours(now.getHours() + 4); - const mockServiceProps = { - endTime: later, - startTime: now, - externalService: 'mhv_sm', - }; - const mockProps = { - status: externalServiceStatus.down, - ...mockServiceProps, - }; - const { getByRole, getByText } = render(); - getByRole('heading', { level: 3, name: 'Maintenance on My HealtheVet' }); - getByText(/some of our health tools/i); - }); +// const mockServiceProps = { +// endTime: later, +// startTime: now, +// externalService: 'mhv_sm', +// }; +// const mockProps = { +// status: externalServiceStatus.down, +// ...mockServiceProps, +// }; +// const { getByRole, getByText } = render(); +// getByRole('heading', { level: 3, name: 'Maintenance on My HealtheVet' }); +// getByText(/some of our health tools/i); +// }); - it('renders MHVDowntimeApproaching and children when a service is going down within an hour', () => { - // Create a starting datetime 30 minutes into the future, though `status` is what really controls what renders - const soon = new Date(Date.now()); - soon.setMinutes(soon.getMinutes() + 30); - const later = new Date(soon).setHours(soon.getHours() + 4); +// it('renders MHVDowntimeApproaching and children when a service is going down within an hour', () => { +// // Create a starting datetime 30 minutes into the future, though `status` is what really controls what renders +// const soon = new Date(Date.now()); +// soon.setMinutes(soon.getMinutes() + 30); +// const later = new Date(soon).setHours(soon.getHours() + 4); - const mockServiceProps = { - endTime: later, - startTime: soon, - externalService: 'mhv_sm', - }; - const mockProps = { - status: externalServiceStatus.downtimeApproaching, - children:

    Child content lives here.

    , - ...mockServiceProps, - }; - const { getByRole, getByText } = render(); - getByRole('heading', { - level: 3, - name: 'Upcoming maintenance on My HealtheVet', - }); - getByText(/you may have trouble using some of our health tools/i); - getByText(/child content lives here/i); - }); +// const mockServiceProps = { +// endTime: later, +// startTime: soon, +// externalService: 'mhv_sm', +// }; +// const mockProps = { +// status: externalServiceStatus.downtimeApproaching, +// children:

    Child content lives here.

    , +// ...mockServiceProps, +// }; +// const { getByRole, getByText } = render(); +// getByRole('heading', { +// level: 3, +// name: 'Upcoming maintenance on My HealtheVet', +// }); +// getByText(/you may have trouble using some of our health tools/i); +// getByText(/child content lives here/i); +// }); - it('renders child content when no matching services are down', () => { - const mockServiceProps = { - endTime: undefined, - startTime: undefined, - externalService: undefined, - }; - const mockProps = { - children:

    Child content renders

    , - status: externalServiceStatus.ok, - ...mockServiceProps, - }; - const { getByText } = render(); - getByText('Child content renders'); - }); +// it('renders child content when no matching services are down', () => { +// const mockServiceProps = { +// endTime: undefined, +// startTime: undefined, +// externalService: undefined, +// }; +// const mockProps = { +// children:

    Child content renders

    , +// status: externalServiceStatus.ok, +// ...mockServiceProps, +// }; +// const { getByText } = render(); +// getByText('Child content renders'); +// }); - it('renders content with vague time interval and no start/end time if no valid dates provided', () => { - const mockServiceProps = { - endTime: {}, - startTime: undefined, - externalService: 'mhv_sm', - }; - const mockProps = { - status: externalServiceStatus.downtimeApproaching, - ...mockServiceProps, - }; +// it('renders content with vague time interval and no start/end time if no valid dates provided', () => { +// const mockServiceProps = { +// endTime: {}, +// startTime: undefined, +// externalService: 'mhv_sm', +// }; +// const mockProps = { +// status: externalServiceStatus.downtimeApproaching, +// ...mockServiceProps, +// }; - const { getByText, queryByText } = render(); - getByText(/The maintenance will last some time/i); - getByText( - /During this time, you may have trouble using some of our health tools/i, - ); - expect(queryByText('July 4, 2019 at 9:00 a.m. ET')).to.be.null; - expect(queryByText('July 5, 2019 at 3:00 a.m. ET')).to.be.null; - }); +// const { getByText, queryByText } = render(); +// getByText(/The maintenance will last some time/i); +// getByText( +// /During this time, you may have trouble using some of our health tools/i, +// ); +// expect(queryByText('July 4, 2019 at 9:00 a.m. ET')).to.be.null; +// expect(queryByText('July 5, 2019 at 3:00 a.m. ET')).to.be.null; +// }); - it('renders content with vague time interval and start time if end time does not exist', () => { - const mockServiceProps = { - endTime: {}, - startTime: new Date('July 4, 2019 09:00:00 EDT'), - externalService: 'mhv_sm', - }; - const mockProps = { - status: externalServiceStatus.downtimeApproaching, - ...mockServiceProps, - }; +// it('renders content with vague time interval and start time if end time does not exist', () => { +// const mockServiceProps = { +// endTime: {}, +// startTime: new Date('July 4, 2019 09:00:00 EDT'), +// externalService: 'mhv_sm', +// }; +// const mockProps = { +// status: externalServiceStatus.downtimeApproaching, +// ...mockServiceProps, +// }; - const { getByText, queryByText } = render(); - getByText(/The maintenance will last some time/i); - getByText( - /During this time, you may have trouble using some of our health tools/i, - ); - getByText('July 4, 2019 at 9:00 a.m. ET'); - expect(queryByText('July 5, 2019 at 3:00 a.m. ET')).to.be.null; - }); +// const { getByText, queryByText } = render(); +// getByText(/The maintenance will last some time/i); +// getByText( +// /During this time, you may have trouble using some of our health tools/i, +// ); +// getByText('July 4, 2019 at 9:00 a.m. ET'); +// expect(queryByText('July 5, 2019 at 3:00 a.m. ET')).to.be.null; +// }); - it('renders content with vague time interval and end time if start time does not exist', () => { - const mockServiceProps = { - endTime: new Date('July 7, 2019 09:00:00 EDT'), - startTime: { toDate: () => 'FAKE' }, - externalService: 'mhv_sm', - }; - const mockProps = { - status: externalServiceStatus.downtimeApproaching, - ...mockServiceProps, - }; +// it('renders content with vague time interval and end time if start time does not exist', () => { +// const mockServiceProps = { +// endTime: new Date('July 7, 2019 09:00:00 EDT'), +// startTime: { toDate: () => 'FAKE' }, +// externalService: 'mhv_sm', +// }; +// const mockProps = { +// status: externalServiceStatus.downtimeApproaching, +// ...mockServiceProps, +// }; - const { getByText } = render(); - getByText(/The maintenance will last some time/i); - getByText( - /During this time, you may have trouble using some of our health tools/i, - ); - getByText('July 7, 2019 at 9:00 a.m. ET'); - }); -}); +// const { getByText } = render(); +// getByText(/The maintenance will last some time/i); +// getByText( +// /During this time, you may have trouble using some of our health tools/i, +// ); +// getByText('July 7, 2019 at 9:00 a.m. ET'); +// }); +// }); diff --git a/src/platform/mhv/downtime/tests/date.unit.spec.js b/src/platform/mhv/downtime/tests/date.unit.spec.js index 038ba9b57863..7c487dcd70eb 100644 --- a/src/platform/mhv/downtime/tests/date.unit.spec.js +++ b/src/platform/mhv/downtime/tests/date.unit.spec.js @@ -1,129 +1,129 @@ -import { expect } from 'chai'; -// NOTE: moment is deprecated, should only be used for testing compatibility with older code -import moment from 'moment'; - -import { - coerceToDate, - formatDatetime, - formatElapsedHours, - parseDate, -} from '../utils/date'; - -describe('coerceToDate', () => { - it('returns a Date instance when passed a moment object', () => { - const m = moment('2024-02-14 09:30'); - const d = coerceToDate(m); - expect(d).to.be.an.instanceOf(Date); - }); - - it('returns a date instance when passed a date instance', () => { - const d1 = new Date(); - const d2 = coerceToDate(d1); - expect(d2).to.equal(d1); - }); - - it('returns null when passed something that is not a date or moment object', () => { - const x = ''; - const y = undefined; - const z = {}; - // F is for Fake - const f = { toDate: () => 'Tricked you!' }; - - expect(coerceToDate(x)).to.be.null; - expect(coerceToDate(y)).to.be.null; - expect(coerceToDate(z)).to.be.null; - expect(coerceToDate(f)).to.be.null; - }); -}); - -describe('parseDate', () => { - it('parses an ISO 8601 date string', () => { - const dateString = '2024-01-29T15:17:05-05:00'; - - const result = parseDate(dateString); - - expect(result).to.respondTo('toISOString'); - - expect(result.toISOString()).to.eql('2024-01-29T20:17:05.000Z'); - }); - - it('returns null for invalid inputs', () => { - expect(parseDate('foobar')).to.be.null; - expect(parseDate(null)).to.be.null; - expect(parseDate('Tomorrow')).to.be.null; - }); -}); - -describe('formatDatetime', () => { - // Use times when UTC offset is -05:00, aka not daylight savings time - it('formats a datetime string with long month name, full year and timezone abbreviation', () => { - const dateString = '2024-01-01T15:17:05-05:00'; - const d = new Date(dateString); - - const result = formatDatetime(d); - - // Test must run in context of expected timezone - expect(result).to.equal('January 1, 2024 at 3:17 p.m. ET'); - }); - - it('formats 12:00 PM as noon', () => { - const dateString = '2024-11-11T12:00-05:00'; - const d = new Date(dateString); - - const result = formatDatetime(d); - - // Test must run in context of expected timezone - expect(result).to.equal('November 11, 2024 at noon ET'); - }); - - it('formats 12:00 AM as midnight', () => { - const dateString = '2024-11-12T00:00:00-05:00'; - const d = new Date(dateString); - - const result = formatDatetime(d); - - // Test must run in context of expected timezone - expect(result).to.equal('November 12, 2024 at midnight ET'); - }); - - it('handles datetimes in daylight savings appropriately', () => { - // DST in 2024 is March 10, 2024 - November 03, 2024 - // Use April 1, 2024, 10am to be in DST - const dateString = '2024-04-01T10:00:00-04:00'; - const d = new Date(dateString); - - const result = formatDatetime(d); - - // Test must run in context of expected timezone - expect(result).to.equal('April 1, 2024 at 10:00 a.m. ET'); - }); -}); - -describe('formatElapsedHours', () => { - it('shows 1 hour when time difference is less than an hour and a half', () => { - const startDate = new Date(2024, 2, 14, 14); - const endDate = new Date(2024, 2, 14, 15, 15); - - const result = formatElapsedHours(startDate, endDate); +// import { expect } from 'chai'; +// // NOTE: moment is deprecated, should only be used for testing compatibility with older code +// import moment from 'moment'; + +// import { +// coerceToDate, +// formatDatetime, +// formatElapsedHours, +// parseDate, +// } from '../utils/date'; + +// describe('coerceToDate', () => { +// it('returns a Date instance when passed a moment object', () => { +// const m = moment('2024-02-14 09:30'); +// const d = coerceToDate(m); +// expect(d).to.be.an.instanceOf(Date); +// }); + +// it('returns a date instance when passed a date instance', () => { +// const d1 = new Date(); +// const d2 = coerceToDate(d1); +// expect(d2).to.equal(d1); +// }); + +// it('returns null when passed something that is not a date or moment object', () => { +// const x = ''; +// const y = undefined; +// const z = {}; +// // F is for Fake +// const f = { toDate: () => 'Tricked you!' }; + +// expect(coerceToDate(x)).to.be.null; +// expect(coerceToDate(y)).to.be.null; +// expect(coerceToDate(z)).to.be.null; +// expect(coerceToDate(f)).to.be.null; +// }); +// }); + +// describe('parseDate', () => { +// it('parses an ISO 8601 date string', () => { +// const dateString = '2024-01-29T15:17:05-05:00'; + +// const result = parseDate(dateString); + +// expect(result).to.respondTo('toISOString'); + +// expect(result.toISOString()).to.eql('2024-01-29T20:17:05.000Z'); +// }); + +// it('returns null for invalid inputs', () => { +// expect(parseDate('foobar')).to.be.null; +// expect(parseDate(null)).to.be.null; +// expect(parseDate('Tomorrow')).to.be.null; +// }); +// }); + +// describe('formatDatetime', () => { +// // Use times when UTC offset is -05:00, aka not daylight savings time +// it('formats a datetime string with long month name, full year and timezone abbreviation', () => { +// const dateString = '2024-01-01T15:17:05-05:00'; +// const d = new Date(dateString); + +// const result = formatDatetime(d); + +// // Test must run in context of expected timezone +// expect(result).to.equal('January 1, 2024 at 3:17 p.m. ET'); +// }); + +// it('formats 12:00 PM as noon', () => { +// const dateString = '2024-11-11T12:00-05:00'; +// const d = new Date(dateString); + +// const result = formatDatetime(d); + +// // Test must run in context of expected timezone +// expect(result).to.equal('November 11, 2024 at noon ET'); +// }); + +// it('formats 12:00 AM as midnight', () => { +// const dateString = '2024-11-12T00:00:00-05:00'; +// const d = new Date(dateString); + +// const result = formatDatetime(d); + +// // Test must run in context of expected timezone +// expect(result).to.equal('November 12, 2024 at midnight ET'); +// }); + +// it('handles datetimes in daylight savings appropriately', () => { +// // DST in 2024 is March 10, 2024 - November 03, 2024 +// // Use April 1, 2024, 10am to be in DST +// const dateString = '2024-04-01T10:00:00-04:00'; +// const d = new Date(dateString); + +// const result = formatDatetime(d); + +// // Test must run in context of expected timezone +// expect(result).to.equal('April 1, 2024 at 10:00 a.m. ET'); +// }); +// }); + +// describe('formatElapsedHours', () => { +// it('shows 1 hour when time difference is less than an hour and a half', () => { +// const startDate = new Date(2024, 2, 14, 14); +// const endDate = new Date(2024, 2, 14, 15, 15); + +// const result = formatElapsedHours(startDate, endDate); - expect(result).to.equal('1 hour'); - }); +// expect(result).to.equal('1 hour'); +// }); - it('shows a plural hours when time difference is greater than an hour and a half', () => { - const startDate = new Date(2024, 2, 14, 14); - const endDate = new Date(2024, 2, 14, 18, 30); +// it('shows a plural hours when time difference is greater than an hour and a half', () => { +// const startDate = new Date(2024, 2, 14, 14); +// const endDate = new Date(2024, 2, 14, 18, 30); - const result = formatElapsedHours(startDate, endDate); +// const result = formatElapsedHours(startDate, endDate); - expect(result).to.equal('5 hours'); - }); +// expect(result).to.equal('5 hours'); +// }); - it('returns null when start or end time is not or cannot be coerced to a date', () => { - expect(formatElapsedHours('foo', new Date())).to.be.null; +// it('returns null when start or end time is not or cannot be coerced to a date', () => { +// expect(formatElapsedHours('foo', new Date())).to.be.null; - expect(formatElapsedHours(new Date(), { toDate: () => "It's a trap!" })).to - .be.null; +// expect(formatElapsedHours(new Date(), { toDate: () => "It's a trap!" })).to +// .be.null; - expect(formatElapsedHours(undefined, null)).to.be.null; - }); -}); +// expect(formatElapsedHours(undefined, null)).to.be.null; +// }); +// }); diff --git a/src/platform/testing/unit/mocha-setup.js b/src/platform/testing/unit/mocha-setup.js index 82ac5d5a584b..381a2d7d1d70 100644 --- a/src/platform/testing/unit/mocha-setup.js +++ b/src/platform/testing/unit/mocha-setup.js @@ -166,6 +166,10 @@ const cleanupStorage = () => { sessionStorage.clear(); }; +function flushPromises() { + return new Promise(resolve => setImmediate(resolve)); +} + export const mochaHooks = { beforeEach() { setupJSDom(); @@ -181,5 +185,6 @@ export const mochaHooks = { }, afterEach() { cleanupStorage(); + flushPromises(); }, }; diff --git a/src/platform/testing/unit/mocha.opts b/src/platform/testing/unit/mocha.opts index 79ab15cb8e8f..6b6321d944eb 100644 --- a/src/platform/testing/unit/mocha.opts +++ b/src/platform/testing/unit/mocha.opts @@ -1,6 +1,5 @@ --require src/platform/testing/unit/mocha-setup.js --require src/platform/testing/unit/enzyme-setup.js ---require choma --require core-js/stable --require regenerator-runtime/runtime --require blob-polyfill diff --git a/src/platform/testing/unit/schemaform-utils.jsx b/src/platform/testing/unit/schemaform-utils.jsx index 8f8c44e12394..5454b75f357e 100644 --- a/src/platform/testing/unit/schemaform-utils.jsx +++ b/src/platform/testing/unit/schemaform-utils.jsx @@ -49,6 +49,8 @@ function getDefaultData(schema) { * @property {function} updateFormData Will be called if form is updated */ export class DefinitionTester extends React.Component { + debouncedAutoSave = sinon.spy(); + constructor(props) { super(props); const { data, uiSchema } = props; @@ -58,21 +60,19 @@ export class DefinitionTester extends React.Component { }; const schema = replaceRefSchemas(props.schema, definitions); - const { data: newData, schema: newSchema } = updateSchemasAndData( - schema, - uiSchema, - data || getDefaultData(schema), - ); + const { + data: newData, + schema: newSchema, + uiSchema: newUiSchema, + } = updateSchemasAndData(schema, uiSchema, data || getDefaultData(schema)); this.state = { formData: newData, schema: newSchema, - uiSchema, + uiSchema: newUiSchema, }; } - debouncedAutoSave = sinon.spy(); - handleChange = data => { const { schema, uiSchema, formData } = this.state; const { pagePerItemIndex, arrayPath, updateFormData } = this.props; @@ -87,6 +87,7 @@ export class DefinitionTester extends React.Component { let newData = newSchemaAndData.data; const newSchema = newSchemaAndData.schema; + const newUiSchema = newSchemaAndData.uiSchema; if (typeof updateFormData === 'function') { if (arrayPath && typeof pagePerItemIndex === 'undefined') { @@ -106,7 +107,7 @@ export class DefinitionTester extends React.Component { this.setState({ formData: newData, schema: newSchema, - uiSchema, + uiSchema: newUiSchema, }); }; diff --git a/src/platform/user/authorization/components/IdentityNotVerified.jsx b/src/platform/user/authorization/components/IdentityNotVerified.jsx index c7b5f125137f..9d0a79c7e79b 100644 --- a/src/platform/user/authorization/components/IdentityNotVerified.jsx +++ b/src/platform/user/authorization/components/IdentityNotVerified.jsx @@ -4,7 +4,7 @@ import { AUTH_EVENTS } from '@department-of-veterans-affairs/platform-user/authe import PropTypes from 'prop-types'; import recordEvent from '~/platform/monitoring/record-event'; -const HowToVerifyLink = () => ( +export const HowToVerifyLink = () => (