diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 574663867bc0..1225fb9b44dd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,6 +43,7 @@ src/applications/proxy-rewrite @department-of-veterans-affairs/platform-design-s src/applications/auth @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend src/applications/login @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend src/applications/sign-in-changes @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend +src/applications/static-pages/cta-widget @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend src/applications/terms-of-use @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend src/applications/verify @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend src/platform/site-wide/ebenefits/ @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/va-platform-cop-frontend @@ -134,14 +135,13 @@ src/applications/find-forms @department-of-veterans-affairs/vfs-public-websites- src/applications/discharge-wizard @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/income-limits @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/public-outreach-materials @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend -src/applications/search @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/1010-health-apps-frontend @department-of-veterans-affairs/va-platform-cop-frontend +src/applications/search @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/BTSSS-login @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend -src/applications/static-pages/cta-widget @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/events @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/homepage @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/homepage-veteran-banner @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/i18select @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend -src/applications/resources-and-support @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/1010-health-apps-frontend @department-of-veterans-affairs/va-platform-cop-frontend +src/applications/resources-and-support @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/pact-act @department-of-veterans-affairs/vfs-public-websites-frontend @department-of-veterans-affairs/va-platform-cop-frontend # Public Websites - unauthed experience only diff --git a/package.json b/package.json index 13f96b97b999..22f018d5477b 100644 --- a/package.json +++ b/package.json @@ -262,8 +262,8 @@ "@babel/runtime": "^7.15.4", "@datadog/browser-logs": "^5.8.0", "@datadog/browser-rum": "^4.49.0", - "@department-of-veterans-affairs/component-library": "^48.3.0", - "@department-of-veterans-affairs/css-library": "^0.16.1", + "@department-of-veterans-affairs/component-library": "^48.4.0", + "@department-of-veterans-affairs/css-library": "^0.17.0", "@department-of-veterans-affairs/react-jsonschema-form": "^1.2.5", "@department-of-veterans-affairs/va-forms-system-core": "1.6.1", "@department-of-veterans-affairs/vagov-platform": "^0.0.1", @@ -335,7 +335,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#35ca8ea9a798407936e20ccf76a4be0fd75a8f76", + "vets-json-schema": "https://github.com/department-of-veterans-affairs/vets-json-schema.git#75b0a0dd78b479e230867e89989bf3e8262d850c", "web-vitals": "^4.2.4" }, "resolutions": { diff --git a/src/applications/686c-674/components/IntroductionPageHeader.jsx b/src/applications/686c-674/components/IntroductionPageHeader.jsx index a2cbf3e50dcf..83eee7de2d25 100644 --- a/src/applications/686c-674/components/IntroductionPageHeader.jsx +++ b/src/applications/686c-674/components/IntroductionPageHeader.jsx @@ -5,7 +5,7 @@ import { PAGE_TITLE } from '../config/constants'; export const IntroductionPageHeader = () => ( <> -

+

VA Form 21-686c and 21-674

diff --git a/src/applications/686c-674/config/constants.js b/src/applications/686c-674/config/constants.js index ce74aca260c7..4769895b5a4d 100644 --- a/src/applications/686c-674/config/constants.js +++ b/src/applications/686c-674/config/constants.js @@ -31,3 +31,5 @@ export const NETWORTH_VALUE = '159,240'; export const FORMAT_YMD_DATE_FNS = 'yyyy-MM-dd'; export const FORMAT_COMPACT_DATE_FNS = 'MMM d, yyyy'; export const FORMAT_READABLE_DATE_FNS = 'MMMM d, yyyy'; + +export const V2_LAUNCH_DATE = 'March 27th, 2025'; // TBD - update after launch diff --git a/src/applications/686c-674/containers/ConfirmationPage.jsx b/src/applications/686c-674/containers/ConfirmationPage.jsx index 850fddf98ebf..d1c4d4bda1dc 100644 --- a/src/applications/686c-674/containers/ConfirmationPage.jsx +++ b/src/applications/686c-674/containers/ConfirmationPage.jsx @@ -13,7 +13,8 @@ export default function ConfirmationPage() { const form = useSelector(state => state?.form); const { submission, data } = form; - const formSubmissionId = submission?.response?.formSubmissionId; + const response = submission?.response ?? {}; + const confirmationNumber = response?.attributes?.confirmationNumber; const veteranFirstName = data?.veteranInformation?.fullName?.first || ''; const veteranLastName = data?.veteranInformation?.fullName?.last || ''; @@ -29,11 +30,11 @@ export default function ConfirmationPage() { return ( <> -

Form submission started on August 15, 2024

+

Form submission started on {dateSubmitted}

Your submission is in progress.

It can take up to 10 days for us to receive your form. Your - confirmation number is {formSubmissionId}. + confirmation number is {confirmationNumber}.

{ const dispatch = useDispatch(); @@ -62,7 +63,6 @@ const IntroductionPage = props => { ) : (
- { pageList={props.route.pageList} startText="Add or remove a dependent" headingLevel={2} - /> + > +

+ You should also know that we updated our online form.{' '} + + If you started applying online before {V2_LAUNCH_DATE}, + {' '} + you’ll need to review the information in your application.Select + Continue your application to use our updated form. +

+
+
diff --git a/src/applications/_mock-form-ae-design-patterns/app-entry.jsx b/src/applications/_mock-form-ae-design-patterns/app-entry.jsx index 91d73206f289..4de3896f2ac3 100644 --- a/src/applications/_mock-form-ae-design-patterns/app-entry.jsx +++ b/src/applications/_mock-form-ae-design-patterns/app-entry.jsx @@ -1,21 +1,31 @@ import 'platform/polyfills'; import './sass/_mock-form-ae-design-patterns.scss'; -import startApp from 'platform/startup'; import routes from './routes'; import reducer from './reducers'; import manifest from './manifest.json'; import coeReducer from './patterns/pattern2/TaskGray/shared/reducers'; +import { asyncStartApp } from './utils/asyncStartApp'; const combinedReducers = { ...reducer, certificateOfEligibility: coeReducer.certificateOfEligibility, }; -startApp({ +const createRoutes = initialRoutes => { + // here we can do some async stuff + // maybe we change the routes based on the state or other api call responses? + // this could be where we add or remove routes for the contact info that is missing for a user + // replace () with (store) to access the store and use it to determine the routes + return () => { + return initialRoutes; + }; +}; + +asyncStartApp({ entryName: manifest.entryName, url: manifest.rootUrl, reducer: combinedReducers, - routes, + createAsyncRoutesWithStore: createRoutes(routes), }); diff --git a/src/applications/_mock-form-ae-design-patterns/hooks/useLocalStorage.js b/src/applications/_mock-form-ae-design-patterns/hooks/useLocalStorage.js index fd86dbbfa505..2ecc282895b3 100644 --- a/src/applications/_mock-form-ae-design-patterns/hooks/useLocalStorage.js +++ b/src/applications/_mock-form-ae-design-patterns/hooks/useLocalStorage.js @@ -3,9 +3,10 @@ import { useState, useEffect } from 'react'; * useLocalStorage is a hook that provides a way to store and retrieve values from localStorage * @param {string} key - The key to store the value under * @param {any} defaultValue - The default value to use if the key does not exist + * @param {boolean} json - Whether to parse the value as JSON, this way a stringified object can be stored and retrieved as an object * @returns {array} An array with [value, setValue, clearValue] */ -export const useLocalStorage = (key, defaultValue) => { +export const useLocalStorage = (key, defaultValue, json = false) => { const [value, setValue] = useState(() => { let currentValue; @@ -13,7 +14,7 @@ export const useLocalStorage = (key, defaultValue) => { const item = localStorage.getItem(key); if (item === null) { currentValue = defaultValue; - } else if (item.startsWith('{') || item.startsWith('[')) { + } else if (json && (item.startsWith('{') || item.startsWith('['))) { currentValue = JSON.parse(item); } else { currentValue = item; @@ -29,14 +30,21 @@ export const useLocalStorage = (key, defaultValue) => { useEffect( () => { - localStorage.setItem(key, JSON.stringify(value)); + if (value === null) { + localStorage.removeItem(key); + return; + } + if (json) { + localStorage.setItem(key, JSON.stringify(value)); + return; + } + localStorage.setItem(key, value); }, - [value, key], + [value, key, json], ); const clearValue = () => { - localStorage.removeItem(key); - setValue(defaultValue); + setValue(null); }; return [value, setValue, clearValue]; diff --git a/src/applications/_mock-form-ae-design-patterns/hooks/useMockedLogin.js b/src/applications/_mock-form-ae-design-patterns/hooks/useMockedLogin.js index ec6162de8a80..07614b965724 100644 --- a/src/applications/_mock-form-ae-design-patterns/hooks/useMockedLogin.js +++ b/src/applications/_mock-form-ae-design-patterns/hooks/useMockedLogin.js @@ -2,23 +2,31 @@ import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { teardownProfileSession } from 'platform/user/profile/utilities'; import { updateLoggedInStatus } from 'platform/user/authentication/actions'; +import { refreshProfile } from 'platform/user/exportsFile'; import { initializeProfile } from 'platform/user/profile/actions'; import { useLocalStorage } from './useLocalStorage'; +// useMockedLogin is a hook that provides a way to log in and out of the application +// it also provides a way to check if the user is logged in +// used for local development export const useMockedLogin = () => { const [ localHasSession, setLocalHasSession, clearLocalHasSession, - ] = useLocalStorage('hasSession', ''); + ] = useLocalStorage('hasSession', null); const loggedInFromState = useSelector( state => state?.user?.login?.currentlyLoggedIn, ); + /** + * memoized value that is true if the local storage has a session or the redux store has a logged in status + * @returns {boolean} + */ const loggedIn = useMemo( - () => localHasSession === 'true' || loggedInFromState, + () => localHasSession === 'true' && loggedInFromState, [localHasSession, loggedInFromState], ); @@ -26,7 +34,9 @@ export const useMockedLogin = () => { const logIn = () => { setLocalHasSession('true'); - dispatch(initializeProfile()); + dispatch(updateLoggedInStatus(true)); + // get the profile right away, so that user state is updated in the redux store + dispatch(refreshProfile()); }; const logOut = () => { @@ -35,6 +45,12 @@ export const useMockedLogin = () => { clearLocalHasSession(); }; + /** + * useLoggedInQuery is a hook that checks the url query params for loggedIn=true or loggedIn=false + * and sets the local storage and redux store accordingly + * @param {*} location - the location object from react router + * @returns {void} + */ const useLoggedInQuery = location => { useEffect( () => { diff --git a/src/applications/_mock-form-ae-design-patterns/mocks/endpoints/user/index.js b/src/applications/_mock-form-ae-design-patterns/mocks/endpoints/user/index.js index 8422b0cc910c..4bfbff9fb185 100644 --- a/src/applications/_mock-form-ae-design-patterns/mocks/endpoints/user/index.js +++ b/src/applications/_mock-form-ae-design-patterns/mocks/endpoints/user/index.js @@ -275,13 +275,73 @@ const loa3UserWithUpdatedHomePhoneTimeStamp = set( ); const loa3UserWithUpdatedMailingAddress = set( + set( + cloneDeep(loa3User), + 'data.attributes.vet360ContactInformation.mailingAddress.addressLine1', + '345 Mailing Address St.', + ), + 'data.attributes.vet360ContactInformation.mailingAddress.updatedAt', + new Date().toISOString(), +); + +const loa3UserWithNoEmail = set( + cloneDeep(loa3User), + 'data.attributes.vet360ContactInformation.email', + {}, +); + +const loa3UserWithNoContactInfo = set( cloneDeep(loa3User), - 'data.attributes.vet360ContactInformation.mailingAddress.addressLine1', - '345 Mailing Address St.', + 'data.attributes.vet360ContactInformation', + { + email: { + ...loa3User.data.attributes.vet360ContactInformation.email, + emailAddress: '', + }, + homePhone: { + ...loa3User.data.attributes.vet360ContactInformation.homePhone, + phoneNumber: '', + areaCode: '', + countryCode: '', + phoneType: '', + }, + mobilePhone: { + ...loa3User.data.attributes.vet360ContactInformation.mobilePhone, + phoneNumber: '', + areaCode: '', + countryCode: '', + phoneType: '', + }, + mailingAddress: { + ...loa3User.data.attributes.vet360ContactInformation.mailingAddress, + addressLine1: '', + addressLine2: '', + addressLine3: '', + city: '', + stateCode: '', + zipCode: '', + countryCodeIso2: '', + countryCodeIso3: '', + countryCodeFips: '', + countyCode: '', + countyName: '', + createdAt: '', + effectiveEndDate: '', + effectiveStartDate: '', + geocodeDate: '', + geocodePrecision: '', + id: '', + internationalPostalCode: '', + latitude: '', + longitude: '', + }, + }, ); module.exports = { loa3User, loa3UserWithUpdatedHomePhoneTimeStamp, loa3UserWithUpdatedMailingAddress, + loa3UserWithNoEmail, + loa3UserWithNoContactInfo, }; diff --git a/src/applications/_mock-form-ae-design-patterns/mocks/script/drupal-vamc-data/mockLocalDSOT.js b/src/applications/_mock-form-ae-design-patterns/mocks/script/drupal-vamc-data/mockLocalDSOT.js index c78d14e6f479..560029507a3f 100644 --- a/src/applications/_mock-form-ae-design-patterns/mocks/script/drupal-vamc-data/mockLocalDSOT.js +++ b/src/applications/_mock-form-ae-design-patterns/mocks/script/drupal-vamc-data/mockLocalDSOT.js @@ -4,13 +4,30 @@ const dfns = require('date-fns'); const fetch = require('node-fetch'); const { warn, error, success, debug } = require('../utils'); -const cwd = process.cwd(); +const findBuildRoot = startDir => { + let currentDir = startDir; -const buildLocalhostPath = './build/localhost/data/cms/vamc-ehr.json'; + // Walk up until we find .build or hit the root + while (currentDir !== path.parse(currentDir).root) { + const buildPath = path.join(currentDir, 'build'); -const urlForStagingVamcDSOT = 'https://staging.va.gov/data/cms/vamc-ehr.json'; -const pathToSaveData = path.join(cwd, buildLocalhostPath); + if (fs.existsSync(buildPath)) { + return currentDir; + } + + currentDir = path.dirname(currentDir); + } + throw new Error('Could not find .build directory in parent directories'); +}; + +const buildRoot = findBuildRoot(__dirname); +const pathToSaveData = path.join( + buildRoot, + 'build/localhost/data/cms/vamc-ehr.json', +); + +const urlForStagingVamcDSOT = 'https://staging.va.gov/data/cms/vamc-ehr.json'; const amount = 14; const twoWeeksAgo = dfns.subDays(new Date(), amount); diff --git a/src/applications/_mock-form-ae-design-patterns/mocks/script/mem-db.js b/src/applications/_mock-form-ae-design-patterns/mocks/script/mem-db.js index 5cb8aa26b714..c7a860fc57ee 100644 --- a/src/applications/_mock-form-ae-design-patterns/mocks/script/mem-db.js +++ b/src/applications/_mock-form-ae-design-patterns/mocks/script/mem-db.js @@ -1,10 +1,15 @@ const _ = require('lodash'); -const { loa3User } = require('../endpoints/user'); +const { loa3UserWithNoContactInfo, loa3User } = require('../endpoints/user'); + +const possibleUsers = { + loa3UserWithNoContactInfo, + loa3User, +}; // in memory db const memDb = { - user: loa3User, + user: possibleUsers.loa3UserWithNoContactInfo, }; // sanitize user input @@ -28,7 +33,11 @@ const updateFields = (target, source, fields) => { } return updatedTarget; }, - { ...target, updatedAt: new Date().toISOString() }, + { + ...target, + updatedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + }, ); }; @@ -42,6 +51,12 @@ const updateConfig = { 'city', 'stateCode', 'zipCode', + 'countryCodeIso2', + 'countryCodeIso3', + 'countryCodeFips', + 'countyCode', + 'countyName', + 'addressPou', ], transactionId: 'mock-update-mailing-address-success-transaction-id', type: 'AsyncTransaction::VAProfile::AddressTransaction', @@ -55,6 +70,12 @@ const updateConfig = { 'city', 'stateCode', 'zipCode', + 'countryCodeIso2', + 'countryCodeIso3', + 'countryCodeFips', + 'countyCode', + 'countyName', + 'addressPou', ], transactionId: 'mock-update-residential-address-success-transaction-id', type: 'AsyncTransaction::VAProfile::AddressTransaction', @@ -114,7 +135,10 @@ const updateMemDb = (req, res = null) => { throw new Error('Invalid phone type sent to PUT telephones'); } - if (key === 'PUT /v0/profile/addresses') { + if ( + key === 'PUT /v0/profile/addresses' || + key === 'POST /v0/profile/addresses' + ) { const addressType = body.addressPou?.toLowerCase(); if ( addressType === 'correspondence' || diff --git a/src/applications/_mock-form-ae-design-patterns/mocks/server.js b/src/applications/_mock-form-ae-design-patterns/mocks/server.js index de7d119f863d..7fb227367657 100644 --- a/src/applications/_mock-form-ae-design-patterns/mocks/server.js +++ b/src/applications/_mock-form-ae-design-patterns/mocks/server.js @@ -48,8 +48,8 @@ const responses = { generateFeatureToggles({ aedpVADX: true, aedpPrefill: true, - coeAccess: false, - profileUseExperimental: false, + coeAccess: true, + profileUseExperimental: true, }), ), secondsOfDelay, @@ -129,7 +129,7 @@ const responses = { ); }, 'POST /v0/profile/addresses': (req, res) => { - return res.json(updateMemDb(req, address.homeAddressUpdateReceived)); + return res.json(updateMemDb(req)); }, 'DELETE /v0/profile/addresses': (_req, res) => { const secondsOfDelay = 1; diff --git a/src/applications/_mock-form-ae-design-patterns/nodemon.json b/src/applications/_mock-form-ae-design-patterns/nodemon.json new file mode 100644 index 000000000000..f820c219006c --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/nodemon.json @@ -0,0 +1,9 @@ +{ + "watch": [ + "src/applications/_mock-form-ae-design-patterns/vadx/server/**/*.js", + "src/applications/_mock-form-ae-design-patterns/mocks/**/*.js" + ], + "ignore": ["*.spec.js"], + "ext": "js,json", + "delay": "1000" +} \ No newline at end of file diff --git a/src/applications/_mock-form-ae-design-patterns/package.json b/src/applications/_mock-form-ae-design-patterns/package.json index 92d38c7c9d5b..9501a1e260b8 100644 --- a/src/applications/_mock-form-ae-design-patterns/package.json +++ b/src/applications/_mock-form-ae-design-patterns/package.json @@ -2,7 +2,9 @@ "name": "@department-of-veterans-affairs/applications-mock-form-ae-design-patterns", "version": "1.0.0", "scripts": { - "demo": "echo DEMOING $npm_package_name v $npm_package_version with WORKSPACES" + "demo": "echo DEMOING $npm_package_name v $npm_package_version with WORKSPACES", + "vadx": "node vadx/server/index.js --entry='mock-form-ae-design-patterns'", + "vadx:dev": "nodemon vadx/server/index.js" }, "licence": "MIT", "dependencies": {}, diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/contactInfo.js b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/contactInfo.js new file mode 100644 index 000000000000..f310322dfec4 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/contactInfo.js @@ -0,0 +1,24 @@ +import profileContactInfo from 'applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/profileContactInfo'; + +import { getContent } from 'platform/forms-system/src/js/utilities/data/profile'; + +const content = { + ...getContent('application'), + description: null, + title: 'Confirm the contact information we have on file for you', +}; + +export const contactInfo = profileContactInfo({ + content, + contactInfoPageKey: 'confirmContactInfo3', + contactPath: 'veteran-information', + contactInfoRequiredKeys: [ + 'mailingAddress', + 'email', + 'homePhone', + 'mobilePhone', + ], + included: ['homePhone', 'mailingAddress', 'email', 'mobilePhone'], + disableMockContactInfo: true, + prefillPatternEnabled: true, +}); diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/form.js b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/form.js index 1b26eaa0b20c..85b9d286c216 100644 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/form.js +++ b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/form.js @@ -8,8 +8,8 @@ import { taskCompletePagePattern2 } from 'applications/_mock-form-ae-design-patt // page level imports import IntroductionPage from '../IntroductionPage'; -import profileContactInfo from './profileContactInfo'; import veteranInfo from './veteranInfo'; +import { contactInfo } from './contactInfo'; const formConfig = { rootUrl: manifest.rootUrl, @@ -65,17 +65,7 @@ const formConfig = { uiSchema: veteranInfo.uiSchema, schema: veteranInfo.schema, }, - ...profileContactInfo({ - contactInfoPageKey: 'confirmContactInfo3', - contactPath: 'veteran-information', - contactInfoRequiredKeys: [ - 'mailingAddress', - 'email', - 'homePhone', - 'mobilePhone', - ], - included: ['homePhone', 'mailingAddress', 'email', 'mobilePhone'], - }), + ...contactInfo, taskCompletePagePattern2, }, }, diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/config/form.js b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/config/form.js index b926c073f0a0..04c7c78f4791 100644 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/config/form.js +++ b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/config/form.js @@ -15,13 +15,12 @@ import { VIEW_FIELD_SCHEMA } from 'applications/_mock-form-ae-design-patterns/ut import { ContactInformationInfoSection } from '../components/ContactInfo'; import VeteranProfileInformation from '../components/VeteranProfileInformation'; import IntroductionPage from '../containers/IntroductionPage'; -import manifest from '../manifest.json'; import { definitions } from './schemaImports'; import profileContactInfo from './profileContactInfo'; import ReviewPage from '../../pages/ReviewPage'; const formConfig = { - rootUrl: manifest.rootUrl, + rootUrl: '/mock-form-ae-design-patterns', urlPrefix: '/2/task-gray/', // submitUrl: `${environment.API_URL}/v0/coe/submit_coe_claim`, // transformForSubmit: customCOEsubmit, diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/manifest.json b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/manifest.json deleted file mode 100644 index 5a8bc1ff23a9..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "appName": "Apply for Certificate of Eligibility", - "entryFile": "./app-entry.jsx", - "entryName": "coe", - "rootUrl": "/mock-form-ae-design-patterns", - "productId": "56491e7e-ed71-42c1-9678-f8e0b4cacb07" -} diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/app-entry.jsx b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/app-entry.jsx deleted file mode 100644 index a1f92d3bdb3f..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/app-entry.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import 'platform/polyfills'; -import './sass/coe-status.scss'; - -import startApp from 'platform/startup'; - -import routes from './routes'; -import reducer from '../shared/reducers'; -import manifest from './manifest.json'; - -startApp({ - entryName: manifest.entryName, - url: manifest.rootUrl, - reducer, - routes, -}); diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/manifest.json b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/manifest.json deleted file mode 100644 index 7c80196ad359..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "appName": "Your VA home loan COE", - "entryFile": "./app-entry.jsx", - "entryName": "coe-status", - "rootUrl": "/housing-assistance/home-loans/check-coe-status/your-coe" -} diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskOrange/manifest.json b/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskOrange/manifest.json deleted file mode 100644 index 87e3c0694ee9..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskOrange/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "appName": "22-1990 Education benefits form", - "entryFile": "./edu-benefits-entry.jsx", - "entryName": "1990-edu-benefits", - "rootUrl": "/mock-form-ae-design-patterns", - "productId": "74dba175-91a7-4986-86cb-7070773e1abe" -} diff --git a/src/applications/_mock-form-ae-design-patterns/routes.jsx b/src/applications/_mock-form-ae-design-patterns/routes.jsx index aeda30b19659..aa3600f6c62e 100644 --- a/src/applications/_mock-form-ae-design-patterns/routes.jsx +++ b/src/applications/_mock-form-ae-design-patterns/routes.jsx @@ -28,9 +28,11 @@ const Form1990Entry = lazy(() => import { plugin } from './shared/components/VADXPlugin'; -const DevPanel = lazy(() => import('./vadx/app/pages/DevPanel')); - import { VADX } from './vadx'; +import { Debug } from './vadx/app/pages/debug/Debug'; +import { withLayout } from './vadx/app/layout/withLayout'; +import { Servers } from './vadx/app/pages/servers/Servers'; +import { FeatureToggles } from './vadx/app/pages/feature-toggles/FeatureToggles'; // Higher order component to wrap routes in the PatternConfigProvider and other common components const routeHoc = Component => props => ( @@ -118,8 +120,16 @@ const routes = [ ...pattern1Routes, ...pattern2Routes, { - path: '/dev', - component: routeHoc(DevPanel), + path: '/vadx', + component: routeHoc(withLayout(Servers)), + }, + { + path: '/vadx/debug', + component: routeHoc(withLayout(Debug)), + }, + { + path: '/vadx/feature-toggles', + component: routeHoc(withLayout(FeatureToggles)), }, { path: '*', diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfo.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfo.jsx new file mode 100644 index 000000000000..523096f3ba7f --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfo.jsx @@ -0,0 +1,532 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router'; + +import { + focusElement, + scrollTo, + scrollAndFocus, +} from '@department-of-veterans-affairs/platform-utilities/ui'; +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; + +import { + selectProfile, + isLoggedIn, +} from '@department-of-veterans-affairs/platform-user/selectors'; + +import { useFeatureToggle } from 'platform/utilities/feature-toggles'; +import { Element } from 'platform/utilities/scroll'; + +import { generateMockUser } from 'platform/site-wide/user-nav/tests/mocks/user'; + +// import { AddressView } from '@department-of-veterans-affairs/platform-user/exports'; +// import AddressView from '@@vap-svc/components/AddressField/AddressView'; +import AddressView from 'platform/user/profile/vap-svc/components/AddressField/AddressView'; + +// import FormNavButtons from '@department-of-veterans-affairs/platform-forms-system/FormNavButtons'; +// import FormNavButtons from 'platform/forms-system/src/js/components/FormNavButtons'; +import FormNavButtons from 'platform/forms-system/src/js/components/FormNavButtons'; + +import readableList from 'platform/forms-system/src/js/utilities/data/readableList'; +import { + setReturnState, + getReturnState, + clearReturnState, + renderTelephone, + getMissingInfo, + REVIEW_CONTACT, + convertNullishObjectValuesToEmptyString, + contactInfoPropTypes, +} from 'platform/forms-system/src/js/utilities/data/profile'; +import { getValidationErrors } from 'platform/forms-system/src/js/utilities/validations'; +import { VaLink } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { ContactInfoLoader } from './ContactInfoLoader'; +import { ContactInfoSuccessAlerts } from './ContactInfoSuccessAlerts'; + +/** + * Render contact info page + * @param {Object} data - full form data + * @param {Function} goBack - CustomPage param + * @param {Function} goForward - CustomPage param + * @param {Boolean} onReviewPage - CustomPage param + * @param {Function} updatePage - CustomPage param + * @param {Element} contentBeforeButtons - CustomPage param + * @param {Element} contentAfterButtons - CustomPage param + * @param {Function} setFormData - CustomPage param + * @param {Object} content - Contact info page content + * @param {String} contactPath - Contact info path; used in edit page path + * @parma {import('../utilities/data/profile').ContactInfoKeys} keys - contact info data key + * @param {String[]} requiredKeys - list of keys of required fields + * @returns + */ +const ContactInfoBase = ({ + data, + goBack, + goForward, + onReviewPage, + updatePage, + contentBeforeButtons, + contentAfterButtons, + setFormData, + content, + contactPath, + keys, + requiredKeys, + uiSchema, + testContinueAlert = false, + contactInfoPageKey, + disableMockContactInfo = false, + contactSectionHeadingLevel, + prefillPatternEnabled, + ...rest +}) => { + const { TOGGLE_NAMES, useToggleValue } = useFeatureToggle(); + const aedpPrefillToggleEnabled = useToggleValue(TOGGLE_NAMES.aedpPrefill); + + const { router } = rest; + + const { pathname } = router.location; + + const wrapRef = useRef(null); + window.sessionStorage.setItem(REVIEW_CONTACT, onReviewPage || false); + const [hasInitialized, setHasInitialized] = useState(false); + const [hadError, setHadError] = useState(false); + const [submitted, setSubmitted] = useState(false); + const [editState] = useState(getReturnState()); + + // vapContactInfo is an empty object locally, so mock it + const profile = useSelector(selectProfile) || {}; + const loggedIn = useSelector(isLoggedIn) || false; + const contactInfo = + loggedIn && environment.isLocalhost() && !disableMockContactInfo + ? generateMockUser({ authBroker: 'iam' }).data.attributes + .vet360ContactInformation + : profile.vapContactInfo || {}; + + const dataWrap = data[keys.wrapper] || {}; + const email = dataWrap[keys.email] || ''; + const homePhone = dataWrap[keys.homePhone] || {}; + const mobilePhone = dataWrap[keys.mobilePhone] || {}; + const address = dataWrap[keys.address] || {}; + + const missingInfo = getMissingInfo({ + data: dataWrap, + keys, + content, + requiredKeys, + }); + + const list = readableList(missingInfo); + const plural = missingInfo.length > 1; + + const validationErrors = uiSchema?.['ui:required']?.(data) + ? getValidationErrors(uiSchema?.['ui:validations'] || [], {}, data) + : []; + + const handlers = { + onSubmit: event => { + // This prevents this nested form submit event from passing to the + // outer form and causing a page advance + event.stopPropagation(); + }, + onGoBack: () => { + clearReturnState(); + goBack(); + }, + onGoForward: () => { + setSubmitted(true); + if (missingInfo.length || validationErrors.length) { + scrollAndFocus(wrapRef.current); + } else { + clearReturnState(); + goForward(data); + } + }, + updatePage: () => { + setSubmitted(true); + if (missingInfo.length || validationErrors.length) { + scrollAndFocus(wrapRef.current); + } else { + setReturnState('true'); + updatePage(); + } + }, + }; + + useEffect( + () => { + if ( + (keys.email && (contactInfo.email?.emailAddress || '') !== email) || + (keys.homePhone && + contactInfo.homePhone?.updatedAt !== homePhone?.updatedAt) || + (keys.mobilePhone && + contactInfo.mobilePhone?.updatedAt !== mobilePhone?.updatedAt) || + (keys.address && + contactInfo.mailingAddress?.updatedAt !== address?.updatedAt) + ) { + const wrapper = { ...data[keys.wrapper] }; + if (keys.address) { + wrapper[keys.address] = convertNullishObjectValuesToEmptyString( + contactInfo.mailingAddress, + ); + } + if (keys.homePhone) { + wrapper[keys.homePhone] = convertNullishObjectValuesToEmptyString( + contactInfo.homePhone, + ); + } + if (keys.mobilePhone) { + wrapper[keys.mobilePhone] = convertNullishObjectValuesToEmptyString( + contactInfo.mobilePhone, + ); + } + if (keys.email) { + wrapper[keys.email] = contactInfo.email?.emailAddress; + } + setFormData({ ...data, [keys.wrapper]: wrapper }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [contactInfo, setFormData, data, keys], + ); + + useEffect( + () => { + if (editState) { + const [lastEdited, returnState] = editState.split(','); + setTimeout(() => { + const target = + returnState === 'canceled' + ? `#edit-${lastEdited}` + : `#updated-${lastEdited}`; + scrollTo( + onReviewPage + ? `${contactInfoPageKey}ScrollElement` + : `header-${lastEdited}`, + ); + focusElement(onReviewPage ? `#${contactInfoPageKey}Header` : target); + }); + } + }, + [contactInfoPageKey, editState, onReviewPage], + ); + + useEffect( + () => { + if ((hasInitialized && missingInfo.length) || testContinueAlert) { + // page had an error flag, so we know when to show a success alert + setHadError(true); + } + setTimeout(() => { + setHasInitialized(true); + }); + }, + [missingInfo, hasInitialized, testContinueAlert], + ); + + const MainHeader = onReviewPage ? 'h4' : 'h3'; + const Headers = contactSectionHeadingLevel || (onReviewPage ? 'h5' : 'h4'); + const headerClassNames = [ + 'vads-u-font-size--h4', + 'vads-u-width--auto', + 'vads-u-margin-top--0', + ].join(' '); + + // keep alerts in DOM, so we don't have to delay focus; but keep the 100ms + // delay to move focus away from the h3 + const showSuccessAlertInField = (id, text) => { + if (prefillPatternEnabled && aedpPrefillToggleEnabled) { + return null; + } + return ( + + {`${text} ${content.updated}`} + + ); + }; + + // Loop to separate pages when editing + // Each Link includes an ID for focus management on the review & submit page + + const contactSection = [ + keys.address ? ( + + + + {content.mailingAddress} + + {showSuccessAlertInField('address', content.mailingAddress)} + + {loggedIn && ( +

+ { + e.preventDefault(); + router.push(`${pathname}/edit-mailing-address`); + }} + active + /> +

+ )} +
+
+ ) : null, + + keys.homePhone ? ( + + + + {content.homePhone} + + {showSuccessAlertInField('home-phone', content.homePhone)} + + {renderTelephone(dataWrap[keys.homePhone])} + + {loggedIn && ( +

+ { + e.preventDefault(); + router.push(`${pathname}/edit-home-phone`); + }} + active + /> +

+ )} +
+
+ ) : null, + + keys.mobilePhone ? ( + + + + {content.mobilePhone} + + {showSuccessAlertInField('mobile-phone', content.mobilePhone)} + + {renderTelephone(dataWrap[keys.mobilePhone])} + + {loggedIn && ( +

+ { + e.preventDefault(); + router.push(`${pathname}/edit-mobile-phone`); + }} + active + /> +

+ )} +
+
+ ) : null, + + keys.email ? ( + + + + {content.email} + + {showSuccessAlertInField('email', content.email)} + + {dataWrap[keys.email] || ''} + + {loggedIn && ( +

+ { + e.preventDefault(); + router.push(`${pathname}/edit-email-address`); + }} + active + /> +

+ )} +
+
+ ) : null, + ]; + + const navButtons = onReviewPage ? ( + + ) : ( + <> + {contentBeforeButtons} + + {contentAfterButtons} + + ); + + return ( + +
+ + +
+ + {content.title} + + {content.description} + {!loggedIn && ( + + You must be logged in to enable view and edit this page. + + )} +
+ {hadError && + missingInfo.length === 0 && + validationErrors.length === 0 && ( +
+ +
+ {content.alertContent} +
+
+
+ )} + {missingInfo.length > 0 && ( + <> +

+ Note: + {missingInfo[0].startsWith('e') ? ' An ' : ' A '} + {list} {plural ? 'are' : 'is'} required for this application. +

+ {submitted && ( +
+ +
+ We still don’t have your {list}. Please edit and update + the field. +
+
+
+ )} +
+ +
+ Your {list} {plural ? 'are' : 'is'} missing. Please edit + and update the {plural ? 'fields' : 'field'}. +
+
+
+ + )} + {submitted && + missingInfo.length === 0 && + validationErrors.length > 0 && ( +
+ +
+ {validationErrors[0]} +
+
+
+ )} +
+
+
+
+
+ {contactSection} +
+
+
+
+
+
{navButtons}
+
+
+ ); +}; + +ContactInfoBase.propTypes = { + contactInfoPageKey: contactInfoPropTypes.contactInfoPageKey, + contactPath: PropTypes.string, + contactSectionHeadingLevel: PropTypes.string, + content: contactInfoPropTypes.content, // content passed in from profileContactInfo + contentAfterButtons: PropTypes.element, + contentBeforeButtons: PropTypes.element, + data: contactInfoPropTypes.data, + disableMockContactInfo: PropTypes.bool, + goBack: PropTypes.func, + goForward: PropTypes.func, + immediateRedirect: PropTypes.bool, + keys: contactInfoPropTypes.keys, + requiredKeys: PropTypes.arrayOf(PropTypes.string), + setFormData: PropTypes.func, + testContinueAlert: PropTypes.bool, // for unit testing only + uiSchema: PropTypes.shape({ + 'ui:required': PropTypes.func, + 'ui:validations': PropTypes.array, + }), + updatePage: PropTypes.func, + onReviewPage: PropTypes.bool, + prefillPatternEnabled: PropTypes.bool, +}; + +const ContactInfo = withRouter(ContactInfoBase); + +export default ContactInfo; diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoLoader.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoLoader.jsx new file mode 100644 index 000000000000..fb723b360820 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoLoader.jsx @@ -0,0 +1,48 @@ +import { withRouter } from 'react-router'; +import React from 'react'; +import { useFeatureToggle } from 'platform/utilities/feature-toggles'; +import PropTypes from 'prop-types'; +import { useRouteMetadata } from './useRouteMetadata'; +import useContactInfo from './useContactInfo'; + +const ContactInfoLoaderBase = ({ + router, + children, + requiredKeys, + disableMockContactInfo, + contactPath, + prefillPatternEnabled, +}) => { + const { useToggleLoadingValue } = useFeatureToggle(); + const loading = useToggleLoadingValue(); + const routeMetadata = useRouteMetadata(router); + + const { missingFields } = useContactInfo({ + requiredKeys, + disableMockContactInfo, + fullContactPath: `${routeMetadata?.urlPrefix || ''}${contactPath}`, + }); + + if (loading) { + return
Loading...
; + } + + if (missingFields.length > 0 && prefillPatternEnabled) { + router.push(missingFields[0].editPath); + } + + return
{children}
; +}; + +ContactInfoLoaderBase.propTypes = { + children: PropTypes.node.isRequired, + router: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + contactPath: PropTypes.string, + disableMockContactInfo: PropTypes.bool, + prefillPatternEnabled: PropTypes.bool, + requiredKeys: PropTypes.arrayOf(PropTypes.string), +}; + +export const ContactInfoLoader = withRouter(ContactInfoLoaderBase); diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoReview.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoReview.jsx new file mode 100644 index 000000000000..0b657f4ba91c --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoReview.jsx @@ -0,0 +1,308 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; + +import { focusElement } from '@department-of-veterans-affairs/platform-utilities/ui'; + +import { Element } from 'platform/utilities/scroll'; + +// import { ADDRESS_TYPES } from '@department-of-veterans-affairs/platform-forms/exports'; +import { ADDRESS_TYPES } from 'platform/forms/address/helpers'; + +import { + renderTelephone, + contactInfoPropTypes, + validateEmail, + validatePhone, + validateZipcode, + getReturnState, + setReturnState, + clearReturnState, +} from 'platform/forms-system/src/js/utilities/data/profile'; + +/** + * Contact info fields shown on the review & submit page + * @param {Object} data - form data + * @param {function} editPage - edit page callback + * @param {ContactInfoContent} content + * @param {Object} keys - form data keys + * @returns {Element} + */ +const ContactInfoReview = ({ + data, + editPage, + content, + keys, + contactInfoPageKey, +}) => { + const editRef = useRef(null); + useEffect( + () => { + if (getReturnState() === 'true,' && editRef?.current) { + // focus on edit button _after_ editing and returning + clearReturnState(); + setTimeout( + () => focusElement('va-button', {}, editRef.current?.shadowRoot), + 0, + ); + } + }, + [editRef], + ); + + const dataWrap = data[keys.wrapper] || {}; + const emailString = dataWrap[keys.email] || ''; + const homePhoneObj = dataWrap[keys.homePhone] || {}; + const mobilePhoneObj = dataWrap[keys.mobilePhone] || {}; + const addressObj = dataWrap[keys.address] || {}; + + const isUS = addressObj.addressType !== ADDRESS_TYPES.international; + + /** + * Renders value (if it isn't all whitespace) or an error message wrapped in + * a class that makes the text bold & red + * @param {String} value - Field value to show + * @param {String} errorMessage - Error message text + * @returns {String|JSX} - value or error message + */ + const showValueOrErrorMessage = (value, errorMessage) => + (value || '').trim() || + (errorMessage && ( + {errorMessage} + )) || + ''; + + /** + * Display field label & data (or error message) on the review & submit page + * Using an array here to maintain display order + * Entry: [ Label, Value or error message ] + * - Label = Name of field (customizable in getContent) + * - Value or error message = `getValue` function that uses `keys` which is + * the data object key that contains the value; e.g. keys.homePhone matches + * this data path: + * { [wrapperKey]: { [homePhone]: { areaCode: '', phoneNumber: '' } } } + * If the value function returns an empty string, the row isn't rendered + * */ + const display = [ + [ + content.homePhone, // label + () => { + // keys.homePhone is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.homePhone) { + return ''; // Don't render row + } + // content contains the error messages, homePhoneObj is the phone object + const errorMsg = validatePhone(content, homePhoneObj); + // Pass showValueOrErrorMessage an empty string so error renders + return errorMsg + ? showValueOrErrorMessage('', errorMsg) + : renderTelephone(homePhoneObj); // va-telephone web component + }, + ], + [ + content.mobilePhone, + () => { + // keys.mobilePhone is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.mobilePhone) { + return ''; // Don't render row + } + const errorMsg = validatePhone(content, mobilePhoneObj); + // Pass showValueOrErrorMessage an empty string so error renders + return errorMsg + ? showValueOrErrorMessage('', errorMsg) + : renderTelephone(mobilePhoneObj); // va-telephone web component + }, + ], + [ + content.email, + () => { + // keys.email is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.email) { + return ''; // Don't render row + } + const errorMsg = validateEmail(content, emailString); + // Pass showValueOrErrorMessage an empty string so error renders + return errorMsg ? showValueOrErrorMessage('', errorMsg) : emailString; + }, + ], + [ + content.country, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.address) { + return ''; // Don't render row + } + // showValueOrErrorMessage will render the trimmed value or an error + return showValueOrErrorMessage( + addressObj.countryName, + content.missingCountryError, + ); + }, + ], + [ + content.address1, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.address) { + return ''; // Don't render row + } + // showValueOrErrorMessage will render the trimmed value or an error + return showValueOrErrorMessage( + addressObj.addressLine1, + content.missingStreetAddressError, + ); + }, + ], + [ + content.address2, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.address) { + return ''; // Don't render row + } + return addressObj.addressLine2; // No error because it's optional + }, + ], + [ + content.address3, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.address) { + return ''; // Don't render row + } + return addressObj.addressLine3; // No error because it's optional + }, + ], + [ + content.city, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` + if (!keys.address) { + return ''; // Don't render row + } + // showValueOrErrorMessage will render the trimmed value or an error + return showValueOrErrorMessage( + addressObj.city, + content.missingCityError, + ); + }, + ], + [ + content.state, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo`, or don't render this row for non-U.S. addresses + if (!keys.address || !isUS) { + return ''; // Don't render row + } + // showValueOrErrorMessage will render the trimmed value or an error + return showValueOrErrorMessage( + addressObj.stateCode, + content.missingStateError, + ); + }, + ], + [ + content.province, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` & don't render this row for U.S. addresses + if (!keys.address && isUS) { + return ''; // Don't render row + } + return addressObj.province; // No error because it's optional + }, + ], + [ + content.zipCode, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo`, or don't render this row for non-U.S. addresses + if (!keys.address || !isUS) { + return ''; // Don't render row + } + const { zipCode } = addressObj; + // Profile should only provide a 5-digit zipcode; evaluate for missing + // or invalid zipcode + const errorMsg = validateZipcode(content, zipCode); + // Pass showValueOrErrorMessage an empty string so error renders + return errorMsg ? showValueOrErrorMessage('', errorMsg) : zipCode; + }, + ], + [ + content.postal, + () => { + // keys.address is undefined if not in `included` option within + // `profileContactInfo` & don't render this row for U.S. addresses + if (!keys.address && isUS) { + return ''; // Don't render row + } + return addressObj.internationalPostalCode; // No error because it's optional + }, + ], + ]; + + const handlers = { + onEditPage: () => { + // maintain state using session storage + setReturnState('true'); + editPage(); + }, + }; + + // Process display list of rows to show on the review & submit page + const list = display + .map(([label, getValue], index) => { + const value = getValue() || ''; + // don't render anything if the value is falsy (getValue will always + // return a string) + return value ? ( +
+
{label}
+
+ {value} +
+
+ ) : null; + }) + .filter(Boolean); + + return ( +
+ +
+

+ {content.title} +

+ +
+ {list.length ?
{list}
: null} +
+ ); +}; + +ContactInfoReview.propTypes = { + contactInfoPageKey: contactInfoPropTypes.contactInfoPageKey, + content: contactInfoPropTypes.content, + data: contactInfoPropTypes.data, + editPage: PropTypes.func, + keys: contactInfoPropTypes.keys, +}; + +export default ContactInfoReview; diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoSuccessAlerts.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoSuccessAlerts.jsx new file mode 100644 index 000000000000..55cc6b36a57b --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/ContactInfoSuccessAlerts.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { clearReturnState } from 'platform/forms-system/src/js/utilities/data/profile'; + +export const ContactInfoSuccessAlerts = ({ + editState, + prefillPatternEnabled, +}) => { + const showSuccessAlert = + prefillPatternEnabled && editState && editState?.includes('updated'); + + if (showSuccessAlert) { + clearReturnState(); + } + + return showSuccessAlert ? ( +
+ +

+ We’ve updated your contact information. +

+
+ We’ve made these changes to this form and your VA.gov profile. +
+
+
+ ) : null; +}; + +ContactInfoSuccessAlerts.propTypes = { + editState: PropTypes.string, + prefillPatternEnabled: PropTypes.bool, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/EditContactInfo.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/EditContactInfo.jsx new file mode 100644 index 000000000000..3958b2863a66 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/EditContactInfo.jsx @@ -0,0 +1,132 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; + +import InitializeVAPServiceID from 'platform/user/profile/vap-svc/containers/InitializeVAPServiceID'; +import ProfileInformationFieldController from 'platform/user/profile/vap-svc/components/ProfileInformationFieldController'; +import { FIELD_NAMES } from 'platform/user/profile/vap-svc/constants'; + +import { focusElement } from '@department-of-veterans-affairs/platform-utilities/ui'; + +import { + REVIEW_CONTACT, + setReturnState, +} from 'platform/forms-system/src/js/utilities/data/profile'; +import { usePrevious } from 'platform/utilities/react-hooks'; +import { withRouter } from 'react-router'; +import { refreshProfile } from 'platform/user/exportsFile'; +import { useRouteMetadata } from './useRouteMetadata'; + +export const BuildPageBase = ({ + title, + field, + id, + goToPath, + contactPath, + editContactInfoHeadingLevel, + router, +}) => { + const dispatch = useDispatch(); + const Heading = editContactInfoHeadingLevel || 'h3'; + const headerRef = useRef(null); + + const modalState = useSelector(state => state?.vapService.modal); + const prevModalState = usePrevious(modalState); + + const routeMetadata = useRouteMetadata(router); + + const fullContactPath = `${routeMetadata?.urlPrefix || ''}${contactPath}`; + + useEffect( + () => { + if (headerRef?.current) { + focusElement(headerRef?.current); + } + }, + [headerRef], + ); + + useEffect( + () => { + const shouldFocusOnHeaderRef = + prevModalState === 'addressValidation' && + modalState === 'mailingAddress'; + + // we do this to make sure focus is set when cancelling out of address validation UI + if (shouldFocusOnHeaderRef) { + setTimeout(() => { + focusElement(headerRef?.current); + }, 250); + } + }, + [modalState, prevModalState], + ); + + const onReviewPage = window.sessionStorage.getItem(REVIEW_CONTACT) === 'true'; + const returnPath = onReviewPage ? '/review-and-submit' : fullContactPath; + + const handlers = { + onSubmit: event => { + // This prevents this nested form submit event from passing to the + // outer form and causing a page advance + event.stopPropagation(); + }, + cancel: () => { + setReturnState(id, 'canceled'); + goToPath(returnPath); + }, + success: async () => { + setReturnState(id, 'updated'); + await dispatch(refreshProfile); + goToPath(returnPath); + }, + }; + + return ( +
+ + + {title} + + + +
+ ); +}; + +BuildPageBase.propTypes = { + router: PropTypes.shape({ + location: PropTypes.object, + }).isRequired, + contactPath: PropTypes.string, + editContactInfoHeadingLevel: PropTypes.string, + field: PropTypes.string, + goToPath: PropTypes.func, + id: PropTypes.string, + title: PropTypes.string, +}; + +const BuildPage = withRouter(BuildPageBase); + +export const EditHomePhone = props => ( + +); + +export const EditMobilePhone = props => ( + +); + +export const EditEmail = props => ( + +); + +export const EditAddress = props => ( + +); diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/index.jsx b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/index.jsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/profileContactInfo.js b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/profileContactInfo.js similarity index 80% rename from src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/profileContactInfo.js rename to src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/profileContactInfo.js index 03189472570a..afe9a069164a 100644 --- a/src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/config/profileContactInfo.js +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/profileContactInfo.js @@ -1,4 +1,3 @@ -import ContactInfoReview from 'platform/forms-system/src/js/components/ContactInfoReview'; import React from 'react'; import { getContent, @@ -7,15 +6,20 @@ import { standardEmailSchema, profileAddressSchema, blankSchema, + getReturnState, clearReturnState, -} from '../../../../utils/data/task-blue/profile'; +} from 'platform/forms-system/src/js/utilities/data/profile'; + +import { scrollTo, focusElement } from 'platform/utilities/ui'; + import { EditAddress, EditEmail, EditHomePhone, EditMobilePhone, -} from '../EditContactInfo'; -import ContactInfo from '../ContactInfo'; +} from './EditContactInfo'; +import ContactInfo from './ContactInfo'; +import ContactInfoReview from './ContactInfoReview'; /** * Profile settings @@ -87,6 +91,10 @@ const profileContactInfo = ({ // depends callback for contact info page depends = null, contactInfoUiSchema = {}, + disableMockContactInfo = false, + contactSectionHeadingLevel = null, + editContactInfoHeadingLevel = null, + prefillPatternEnabled = false, } = {}) => { const config = {}; const wrapperProperties = {}; @@ -99,7 +107,13 @@ const profileContactInfo = ({ config[`${contactInfoPageKey}EditMailingAddress`] = { title: content.editMailingAddress, path: `${contactPath}/edit-mailing-address`, - CustomPage: props => EditAddress({ ...props, content, contactPath }), + CustomPage: props => + EditAddress({ + ...props, + content, + contactPath, + editContactInfoHeadingLevel, + }), CustomPageReview: null, // not shown on review & submit depends: () => false, // accessed from contact info page uiSchema: {}, @@ -119,6 +133,10 @@ const profileContactInfo = ({ ...props, content, contactPath, + editContactInfoHeadingLevel, + requiredKeys: contactInfoRequiredKeys, + contactInfoPageKey, + disableMockContactInfo, }), CustomPageReview: null, // not shown on review & submit depends: () => false, // accessed from contact info page @@ -134,7 +152,16 @@ const profileContactInfo = ({ config[`${contactInfoPageKey}EditMobilePhone`] = { title: content.editMobilePhone, path: `${contactPath}/edit-mobile-phone`, - CustomPage: props => EditMobilePhone({ ...props, content, contactPath }), + CustomPage: props => + EditMobilePhone({ + ...props, + content, + contactPath, + editContactInfoHeadingLevel, + requiredKeys: contactInfoRequiredKeys, + contactInfoPageKey, + disableMockContactInfo, + }), CustomPageReview: null, // not shown on review & submit depends: () => false, // accessed from contact info page uiSchema: {}, @@ -147,7 +174,16 @@ const profileContactInfo = ({ config[`${contactInfoPageKey}EditEmailAddress`] = { title: content.editEmail, path: `${contactPath}/edit-email-address`, - CustomPage: props => EditEmail({ ...props, content, contactPath }), + CustomPage: props => + EditEmail({ + ...props, + content, + contactPath, + editContactInfoHeadingLevel, + requiredKeys: contactInfoRequiredKeys, + contactInfoPageKey, + disableMockContactInfo, + }), CustomPageReview: null, // not shown on review & submit depends: () => false, // accessed from contact info page uiSchema: {}, @@ -167,6 +203,10 @@ const profileContactInfo = ({ keys={keys} requiredKeys={contactInfoRequiredKeys} contactInfoPageKey={contactInfoPageKey} + disableMockContactInfo={disableMockContactInfo} + contactSectionHeadingLevel={contactSectionHeadingLevel} + editContactInfoHeadingLevel={editContactInfoHeadingLevel} + prefillPatternEnabled={prefillPatternEnabled} /> ), CustomPageReview: props => @@ -194,6 +234,13 @@ const profileContactInfo = ({ clearReturnState(); return formData; }, + // overide scroll & focus header + scrollAndFocusTarget: () => { + if (!getReturnState()) { + scrollTo('topContentElement'); + focusElement('h3'); + } + }, }, // edit pages; only accessible via ContactInfo component links ...config, diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useContactInfo.js b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useContactInfo.js new file mode 100644 index 000000000000..ffd809123b01 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useContactInfo.js @@ -0,0 +1,123 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { isLoggedIn } from '@department-of-veterans-affairs/platform-user/selectors'; +import { generateMockUser } from 'platform/site-wide/user-nav/tests/mocks/user'; +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; + +const useContactInfo = ({ + disableMockContactInfo = false, + requiredKeys = [], + fullContactPath, +} = {}) => { + const loggedIn = useSelector(isLoggedIn); + const profile = useSelector(state => state?.user?.profile); + + const editPagePathMap = { + email: 'edit-email-address', + homePhone: 'edit-home-phone', + mobilePhone: 'edit-mobile-phone', + mailingAddress: 'edit-mailing-address', + }; + + return useMemo( + () => { + // Use mock data locally if logged in and not disabled + const vapContactInfo = + loggedIn && environment.isLocalhost() && !disableMockContactInfo + ? generateMockUser({ authBroker: 'iam' }).data.attributes + .vet360ContactInformation + : profile?.vapContactInfo || {}; + + const email = vapContactInfo?.email?.emailAddress || ''; + const homePhone = vapContactInfo?.homePhone || {}; + const mobilePhone = vapContactInfo?.mobilePhone || {}; + const mailingAddress = vapContactInfo?.mailingAddress || {}; + + // Get missing info + const missingFields = []; + if (!email && requiredKeys.includes('email')) + missingFields.push({ + field: 'email', + editPath: `${fullContactPath}/${editPagePathMap.email}`, + label: 'email address', + }); + if (!homePhone?.phoneNumber && requiredKeys.includes('homePhone')) + missingFields.push({ + field: 'homePhone', + editPath: `${fullContactPath}/${editPagePathMap.homePhone}`, + label: 'home phone', + }); + if (!mobilePhone?.phoneNumber && requiredKeys.includes('mobilePhone')) + missingFields.push({ + field: 'mobilePhone', + editPath: `${fullContactPath}/${editPagePathMap.mobilePhone}`, + label: 'mobile phone', + }); + if ( + !mailingAddress?.addressLine1 && + requiredKeys.includes('mailingAddress') + ) + missingFields.push({ + field: 'mailingAddress', + editPath: `${fullContactPath}/${editPagePathMap.mailingAddress}`, + label: 'mailing address', + }); + + return { + email: { + emailAddress: email, + id: vapContactInfo?.email?.id, + status: vapContactInfo?.email?.status, + missing: missingFields.some(field => field.field === 'email'), + required: requiredKeys.includes('email'), + }, + homePhone: { + phoneNumber: homePhone?.phoneNumber || '', + extension: homePhone?.extension || '', + id: homePhone?.id, + status: homePhone?.status, + missing: missingFields.some(field => field.field === 'homePhone'), + required: requiredKeys.includes('homePhone'), + }, + mobilePhone: { + phoneNumber: mobilePhone?.phoneNumber || '', + extension: mobilePhone?.extension || '', + id: mobilePhone?.id, + status: mobilePhone?.status, + missing: missingFields.some(field => field.field === 'mobilePhone'), + required: requiredKeys.includes('mobilePhone'), + }, + mailingAddress: { + addressLine1: mailingAddress?.addressLine1 || '', + addressLine2: mailingAddress?.addressLine2 || '', + addressLine3: mailingAddress?.addressLine3 || '', + city: mailingAddress?.city || '', + stateCode: mailingAddress?.stateCode || '', + zipCode: mailingAddress?.zipCode || '', + countryName: mailingAddress?.countryName || '', + id: mailingAddress?.id, + status: mailingAddress?.status, + missing: missingFields.some( + field => field.field === 'mailingAddress', + ), + required: requiredKeys.includes('mailingAddress'), + }, + missingFields, + isLoggedIn: loggedIn, + }; + }, + [ + loggedIn, + profile, + disableMockContactInfo, + requiredKeys, + fullContactPath, + editPagePathMap.email, + editPagePathMap.homePhone, + editPagePathMap.mailingAddress, + editPagePathMap.mobilePhone, + ], + ); +}; + +export default useContactInfo; diff --git a/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useRouteMetadata.js b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useRouteMetadata.js new file mode 100644 index 000000000000..1c3a1d6aec1f --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/shared/components/ContactInfo/useRouteMetadata.js @@ -0,0 +1,17 @@ +export const useRouteMetadata = router => { + const { pathname } = router.location; + + // check that there is a route with a urlPrefix and a path property, then combine them + const foundRoute = router.routes.find( + route => + route.urlPrefix && + route.path && + `${route.urlPrefix}${route.path}` === pathname, + ); + + if (!foundRoute) { + return null; + } + + return foundRoute; +}; diff --git a/src/applications/_mock-form-ae-design-patterns/shared/context/PatternConfigContext.jsx b/src/applications/_mock-form-ae-design-patterns/shared/context/PatternConfigContext.jsx index efcd03e61b5d..1db3267b2142 100644 --- a/src/applications/_mock-form-ae-design-patterns/shared/context/PatternConfigContext.jsx +++ b/src/applications/_mock-form-ae-design-patterns/shared/context/PatternConfigContext.jsx @@ -1,74 +1,47 @@ -import React, { createContext } from 'react'; +import React, { createContext, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; -import greenFormConfig from '../../patterns/pattern1/TaskGreen/config/form'; -import yellowFormConfig from '../../patterns/pattern1/TaskYellow/config/form'; -import purpleFormConfig from '../../patterns/pattern1/TaskPurple/config/form'; -import ezrFormConfig from '../../patterns/pattern1/ezr/config/form'; -import grayFormConfig from '../../patterns/pattern2/TaskGray/form/config/form'; -import blueFormConfig from '../../patterns/pattern2/TaskBlue/config/form'; -import { formConfigForOrangeTask } from '../../patterns/pattern2/TaskOrange/config/form'; +import { withRouter } from 'react-router'; import fallbackForm from '../config/fallbackForm'; import { TaskTabs } from '../components/TaskTabs'; import { Portal } from '../components/Portal'; import { useMockedLogin } from '../../hooks/useMockedLogin'; -export const getFormConfig = location => { - if (location.pathname.includes('/1/task-green')) { - return greenFormConfig; - } - - if (location.pathname.includes('/1/task-yellow')) { - return yellowFormConfig; - } - - if (location.pathname.includes('/1/task-purple')) { - return purpleFormConfig; - } - - if (location.pathname.includes('/1/ezr')) { - return ezrFormConfig; - } - - if (location.pathname.includes('/2/task-gray')) { - return grayFormConfig; - } - - if (location.pathname.includes('/2/task-orange')) { - return formConfigForOrangeTask; - } - - if (location.pathname.includes('/2/task-blue')) { - return blueFormConfig; - } - - return fallbackForm; -}; - export const PatternConfigContext = createContext(); -export const PatternConfigProvider = ({ location, children }) => { +export const PatternConfigProviderBase = ({ children, router }) => { const { useLoggedInQuery } = useMockedLogin(); - useLoggedInQuery(location); - const formConfig = getFormConfig(location); + useLoggedInQuery(router?.location); + const formConfig = router?.routes?.[1]?.formConfig || fallbackForm; const dispatch = useDispatch(); - dispatch({ type: 'SET_NEW_FORM_CONFIG', formConfig }); + + useEffect( + () => { + dispatch({ type: 'SET_NEW_FORM_CONFIG', formConfig }); + }, + [dispatch, formConfig], + ); // we need to get the header element to append the tabs to it const header = document.getElementById('header-default'); return formConfig ? ( - + {children} ) : null; }; -PatternConfigProvider.propTypes = { +export const PatternConfigProvider = withRouter(PatternConfigProviderBase); + +PatternConfigProviderBase.propTypes = { children: PropTypes.node.isRequired, - location: PropTypes.object.isRequired, + router: PropTypes.shape({ + location: PropTypes.object, + routes: PropTypes.array, + }).isRequired, }; diff --git a/src/applications/_mock-form-ae-design-patterns/tests/e2e/pattern2/taskBlue.cypress.spec.js b/src/applications/_mock-form-ae-design-patterns/tests/e2e/pattern2/taskBlue.cypress.spec.js index cc44c2e94802..6ef527b19b25 100644 --- a/src/applications/_mock-form-ae-design-patterns/tests/e2e/pattern2/taskBlue.cypress.spec.js +++ b/src/applications/_mock-form-ae-design-patterns/tests/e2e/pattern2/taskBlue.cypress.spec.js @@ -2,6 +2,7 @@ import manifest from '../../../manifest.json'; // eslint-disable-next-line import/no-duplicates import mockUsers from '../../../mocks/endpoints/user'; import mockPrefills from '../../../mocks/endpoints/in-progress-forms/mock-form-ae-design-patterns'; +import { generateFeatureToggles } from '../../../mocks/endpoints/feature-toggles'; // eslint-disable-next-line import/no-duplicates describe('Prefill pattern - Blue Task', () => { @@ -78,6 +79,14 @@ describe('Prefill pattern - Blue Task', () => { }, }, }); + + cy.intercept( + 'GET', + '/v0/feature_toggles*', + generateFeatureToggles({ + aedpPrefill: true, + }), + ); }); it('should show user as authenticated from the start', () => { @@ -146,8 +155,9 @@ describe('Prefill pattern - Blue Task', () => { cy.injectAxeThenAxeCheck(); + cy.get('va-link[label="Edit mailing address"]').click(); // update mailing address and save form - cy.findByLabelText('Edit mailing address').click(); + // cy.findByLabelText('Edit mailing address').click(); // need this to access the input in the web component shadow dom cy.get('va-text-input[name="root_addressLine1"]') @@ -159,12 +169,12 @@ describe('Prefill pattern - Blue Task', () => { cy.get('@addressInput').type('345 Mailing Address St.'); // confirming save to profile ques is selected yes by default - cy.contains( - 'legend', - 'Do you also want to update this information in your VA.gov profile?', - ).should('exist'); + // cy.contains( + // 'legend', + // 'Do you also want to update this information in your VA.gov profile?', + // ).should('exist'); - cy.get('#saveToProfileYes').click(); + // cy.get('#saveToProfileYes').click(); cy.findByTestId('save-edit-button').click(); @@ -172,7 +182,7 @@ describe('Prefill pattern - Blue Task', () => { // redirect to previous page and show save alert cy.url().should('contain', '/veteran-information'); - cy.findByText('We’ve updated your mailing address').should('exist'); + cy.findByText('We’ve updated your contact information.').should('exist'); cy.findByText( 'We’ve made these changes to this form and your VA.gov profile.', ).should('exist'); diff --git a/src/applications/_mock-form-ae-design-patterns/utils/asyncSetUpCommonFunctionality.js b/src/applications/_mock-form-ae-design-patterns/utils/asyncSetUpCommonFunctionality.js index cf6d494493c3..878e55166c2a 100644 --- a/src/applications/_mock-form-ae-design-patterns/utils/asyncSetUpCommonFunctionality.js +++ b/src/applications/_mock-form-ae-design-patterns/utils/asyncSetUpCommonFunctionality.js @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/browser'; import { connectFeatureToggle } from 'platform/utilities/feature-toggles'; import startSitewideComponents from 'platform/site-wide'; -import createRtkCommonStore from './rtkStore'; +import createCommonStore from 'platform/startup/store'; /** * Wrapper for creating a store and sitewide components, this async version is used @@ -28,7 +28,7 @@ export default async function asyncSetUpCommonFunctionality({ // Set the app name for use in the apiRequest helper window.appName = entryName; - const store = createRtkCommonStore(reducer, analyticsEvents); + const store = createCommonStore(reducer, analyticsEvents); await connectFeatureToggle(store.dispatch); if (url?.endsWith('/')) { diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/ApplicationSelector.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/ApplicationSelector.jsx deleted file mode 100644 index 3af64129b0ca..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/vadx/app/ApplicationSelector.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { setSelectedApps as setApps } from '../../slice'; - -export const ApplicationSelector = () => { - const [searchTerm, setSearchTerm] = useState(''); - - // a stand-in for the actual process config for now - const processConfig = { - validEntryNames: ['auth', 'profile', 'mock-form-ae-design-patterns'], - }; - - const dispatch = useDispatch(); - const setSelectedApps = payload => dispatch(setApps(payload)); - - const selectedApps = useSelector(state => state.vadx.selectedApps); - - const filteredApps = useMemo( - () => { - return processConfig.validEntryNames.filter(app => - app.toLowerCase().includes(searchTerm.toLowerCase()), - ); - }, - [searchTerm], - ); - - const handleAppSelect = e => { - const selectedApp = e.target.value; - if (selectedApp && !selectedApps.includes(selectedApp)) { - setSelectedApps([...selectedApps, selectedApp]); - } - e.target.value = ''; // Reset select after choosing - }; - - const handleRemoveApp = app => { - setSelectedApps(selectedApps.filter(a => a !== app)); - }; - - return ( -
-
- - setSearchTerm(e.target.value)} - placeholder="Type to search..." - className="vads-input" - /> -
- -
- - -
- -
-

Selected Applications:

-
    - {selectedApps.map(app => ( -
  • - -
  • - ))} -
-
- - -
- ); -}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/MainLayout.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/MainLayout.jsx new file mode 100644 index 000000000000..ea6b9ce99dac --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/MainLayout.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * @component MainLayout + * @description Layout component for VADX tools. includes left navigation and content section + */ +export const MainLayout = ({ children, navigation }) => { + return ( +
+
+
+

+ VADX tools +

+ +
+ +
{children}
+
+
+ ); +}; + +MainLayout.propTypes = { + children: PropTypes.node.isRequired, + navigation: PropTypes.node.isRequired, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/Navigation.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/Navigation.jsx new file mode 100644 index 000000000000..da7997f99cff --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/Navigation.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Link } from 'react-router'; +import { useProcessManager } from '../../context/processManager'; + +// side navigation for the vadx pages +export const Navigation = () => { + const { activeApps } = useProcessManager(); + return ( + + ); +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/withLayout.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/withLayout.jsx new file mode 100644 index 000000000000..45d143c7318a --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/layout/withLayout.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { MainLayout } from './MainLayout'; +import { Navigation } from './Navigation'; +import { ProcessManagerProvider } from '../../context/processManager'; + +// withLayout is a higher order component that wraps the component with the ProcessManagerProvider +// and MainLayout so that it can be used in the react router routes config +export const withLayout = Component => props => { + return ( + + }> + + + + ); +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/DevPanel.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/DevPanel.jsx deleted file mode 100644 index 5d7828276efb..000000000000 --- a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/DevPanel.jsx +++ /dev/null @@ -1,243 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; -import { format, parseISO } from 'date-fns'; -import { ApplicationSelector } from '../ApplicationSelector'; - -const API_BASE_URL = 'http://localhost:1337'; - -const formatDate = dateString => { - try { - const date = parseISO(dateString); - return format(date, 'HH:mm:ss:aaaaa'); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error parsing date:', error); - return dateString; // Return original string if parsing fails - } -}; - -const DevPanelLineItem = ({ line }) => { - return ( -
- {line} -
- ); -}; - -const DevPanel = () => { - const [processes, setProcesses] = useState({}); - const [output, setOutput] = useState({}); - const eventSourcesRef = useRef({}); - - const setupEventSource = processName => { - const eventSource = new EventSource( - `${API_BASE_URL}/events/${processName}`, - ); - - eventSource.onmessage = event => { - const data = JSON.parse(event.data); - setOutput(prev => ({ - ...prev, - [processName]: [ - { - id: Date.now(), - friendlyDate: formatDate(new Date().toISOString()), - ...data, - }, - ...(prev[processName] || []), - ], - })); - }; - - eventSource.onerror = error => { - // eslint-disable-next-line no-console - console.error(`EventSource failed for ${processName}:`, error); - eventSource.close(); - delete eventSourcesRef.current[processName]; - }; - - return eventSource; - }; - - const fetchStatus = async () => { - try { - const response = await fetch(`${API_BASE_URL}/status`); - const data = await response.json(); - setProcesses(data); - - // Setup or tear down event sources based on process status - Object.keys(data).forEach(processName => { - if (data[processName] && !eventSourcesRef.current[processName]) { - eventSourcesRef.current[processName] = setupEventSource(processName); - } else if (!data[processName] && eventSourcesRef.current[processName]) { - eventSourcesRef.current[processName].close(); - delete eventSourcesRef.current[processName]; - } - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching status:', error); - } - }; - - useEffect(() => { - fetchStatus(); - const interval = setInterval(fetchStatus, 5000); - const currentEventSources = eventSourcesRef.current; - return () => { - clearInterval(interval); - Object.values(currentEventSources).forEach(es => es.close()); - }; - }, []); - - const startProcess = async (processName, processConfig) => { - try { - const response = await fetch(`${API_BASE_URL}/start-${processName}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(processConfig), - }); - await response.json(); - fetchStatus(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Error starting ${processName}:`, error); - } - }; - - const stopProcess = async (processName, port) => { - try { - const response = await fetch(`${API_BASE_URL}/stop`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ port }), - }); - await response.json(); - if (eventSourcesRef.current[processName]) { - eventSourcesRef.current[processName].close(); - delete eventSourcesRef.current[processName]; - } - fetchStatus(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Error stopping process on port ${port}:`, error); - } - }; - - const renderProcessColumn = ( - processName, - displayName, - startConfig, - stopPort, - ) => { - return ( -
-
-

- {displayName} -

- {processes[processName] ? ( - <> -
- Status: Running 🍏 -
- stopProcess(processName, stopPort)} - text="stop process" - secondary - /> - - ) : ( - <> -
- Status: Stopped 🍎 -
- startProcess(processName, startConfig)} - text="start process" - secondary - /> - - )} -
- -
-

- Process Output -

-
- - {output[processName]?.flatMap((msg, index) => { - if (msg.type === 'cache') { - return msg.data.map((line, cacheIndex) => ( - - )); - } - return ( - - - - ); - })} - -
-
-
- ); - }; - - return ( -
-

VADX - Tools

- -
-
- -
- {renderProcessColumn( - 'fe-dev-server', - 'Frontend Dev Server', - { - entry: 'mock-form-ae-design-patterns', - api: 'http://localhost:3000', - }, - 3001, - )} - {renderProcessColumn( - 'mock-server', - 'Mock API Server', - { - debug: true, - responsesPath: - 'src/applications/_mock-form-ae-design-patterns/mocks/server.js', - }, - 3000, - )} -
-
- ); -}; - -export default DevPanel; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/debug/Debug.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/debug/Debug.jsx new file mode 100644 index 000000000000..c024d9c00277 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/debug/Debug.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Debug = () => { + return
Debug
; +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/feature-toggles/FeatureToggles.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/feature-toggles/FeatureToggles.jsx new file mode 100644 index 000000000000..d87874faae0b --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/feature-toggles/FeatureToggles.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const FeatureToggles = () => { + return
Feature Toggles
; +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerColumn.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerColumn.jsx new file mode 100644 index 000000000000..73134c0a80fb --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerColumn.jsx @@ -0,0 +1,66 @@ +// components/servers/FrontendServer.jsx +import React, { useState } from 'react'; +import ServerControls from './ServerControls'; +import ProcessOutput from './ProcessOutput'; +import { FrontendServerConfiguration } from './FrontendServerConfiguration'; +import { useProcessManager } from '../../../context/processManager'; +import { FRONTEND_PROCESS_NAME } from '../../../constants'; + +export const FrontendServerColumn = () => { + const { + processes, + output, + startProcess, + stopProcess, + fetchStatus, + } = useProcessManager(); + const processName = FRONTEND_PROCESS_NAME; + const [showStarter, setShowStarter] = useState(false); + const [starting, setStarting] = useState(false); + + const handleStart = () => { + setShowStarter(true); + }; + + const handleStarterClose = () => { + setShowStarter(false); + }; + + return ( +
+ + + + + { + setStarting(true); + await startProcess(processName, { + entries: manifestsToStart.map(m => m.entryName), + api: 'http://localhost:3000', + }); + fetchStatus(); + setStarting(false); + setShowStarter(false); + }} + visible={showStarter} + starting={starting} + /> +
+ ); +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerConfiguration.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerConfiguration.jsx new file mode 100644 index 000000000000..ce8972bcbf07 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerConfiguration.jsx @@ -0,0 +1,217 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { + VaModal, + VaCheckbox, + VaSearchInput, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { FRONTEND_PROCESS_NAME } from '../../../constants'; +import { useProcessManager } from '../../../context/processManager'; +import { formatDate } from '../../../utils/dates'; + +const ManifestOption = ({ manifest, selected, onToggle }) => { + return ( +
+ onToggle(manifest, e.target.checked)} + tile + /> +
+ ); +}; + +const scrollableCheckboxStyles = { + maxHeight: '60vh', + overflowY: 'auto', + padding: '0 .5rem', + border: '1px solid #e1e1e1', + borderRadius: '4px', + backgroundColor: '#f9f9f9', +}; + +export const FrontendServerConfiguration = ({ + onClose, + onStart, + visible, + starting, +}) => { + const { manifests, activeApps, setOutput, output } = useProcessManager(); + const [selectedManifests, setSelectedManifests] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [startError, setStartError] = useState(null); + const filteredManifests = useMemo( + () => { + const searchLower = searchQuery.toLowerCase(); + return manifests + .filter( + manifest => + manifest.appName.toLowerCase().includes(searchLower) || + manifest.entryName.toLowerCase().includes(searchLower) || + (manifest.rootUrl && + manifest.rootUrl.toLowerCase().includes(searchLower)), + ) + .sort((a, b) => { + const aSelected = selectedManifests.some( + m => m.entryName === a.entryName, + ); + const bSelected = selectedManifests.some( + m => m.entryName === b.entryName, + ); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return a.appName.localeCompare(b.appName); + }); + }, + [manifests, searchQuery, selectedManifests], + ); + + const handleManifestToggle = (manifest, isSelected) => { + setSelectedManifests(prev => { + if (isSelected) { + return [...prev, manifest]; + } + return prev.filter(m => m.entryName !== manifest.entryName); + }); + }; + + const handleStartServer = async () => { + if (selectedManifests.length === 0) { + setStartError('Please select at least one application'); + return; + } + try { + setOutput({ + ...output, + [FRONTEND_PROCESS_NAME]: [ + { + type: 'stdout', + data: `Starting frontend dev server for apps ${selectedManifests + .map(m => m.appName) + .join(', ')} 🚀`, + friendlyDate: formatDate(new Date().toISOString()), + }, + ], + }); + await onStart(selectedManifests); + } catch (error) { + setStartError(error.message); + } + }; + + useEffect( + () => { + if (visible && activeApps.length > 0) { + setSelectedManifests(activeApps); + } + }, + [visible, activeApps], + ); + + return visible ? ( +
+ +
+ {starting && ( + + )} + {manifests.length > 0 && + !starting && ( + <> +

+ Select the applications you want dev server to watch +

+ +
+ setSearchQuery(e.target.value)} + buttonText="Search" + name="manifest-search" + small + disableAnalytics + /> +
+ +
+ +
    + {selectedManifests.length > 0 ? ( + selectedManifests.map(manifest => ( +
  • + {manifest.appName} ({manifest.entryName}) + {manifest.rootUrl && ` - ${manifest.rootUrl}`} +
  • + )) + ) : ( +
  • No applications selected
  • + )} +
+
+
+ +
+
+ {filteredManifests.map(manifest => ( + m.entryName === manifest.entryName, + )} + onToggle={handleManifestToggle} + /> + ))} +
+
+ + {startError && ( + + {`Error Starting Server: ${startError}`} + + )} + + )} +
+
+
+ ) : null; +}; + +FrontendServerConfiguration.propTypes = { + starting: PropTypes.bool.isRequired, + visible: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onStart: PropTypes.func.isRequired, +}; + +ManifestOption.propTypes = { + manifest: PropTypes.shape({ + appName: PropTypes.string.isRequired, + entryName: PropTypes.string.isRequired, + rootUrl: PropTypes.string, + }).isRequired, + selected: PropTypes.bool.isRequired, + onToggle: PropTypes.func.isRequired, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/MockServerColumn.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/MockServerColumn.jsx new file mode 100644 index 000000000000..8831483dcee4 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/MockServerColumn.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ServerControls from './ServerControls'; +import ProcessOutput from './ProcessOutput'; +import { useProcessManager } from '../../../context/processManager'; + +export const MockServerColumn = () => { + const { processes, output, startProcess, stopProcess } = useProcessManager(); + const processName = 'mock-server'; + + return ( +
+ + +
+ ); +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/OutputLineItem.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/OutputLineItem.jsx new file mode 100644 index 000000000000..474f5ea358fb --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/OutputLineItem.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const OutputLineItem = ({ line }) => { + return ( +
+ {line} +
+ ); +}; + +OutputLineItem.propTypes = { + line: PropTypes.string.isRequired, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ProcessOutput.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ProcessOutput.jsx new file mode 100644 index 000000000000..50405bf95a30 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ProcessOutput.jsx @@ -0,0 +1,88 @@ +// components/servers/ProcessOutput.jsx +import React from 'react'; +import PropTypes from 'prop-types'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { OutputLineItem } from './OutputLineItem'; +import { FRONTEND_PROCESS_NAME } from '../../../constants'; + +const getStatusOfProcess = (output, processName) => { + if (!output) return 'unknown'; + + if (processName === FRONTEND_PROCESS_NAME) { + // get first message to check if it's finished compiling + const firstMessage = output[0]; + + if (firstMessage?.type === 'cache') { + const isFinishedCompiling = firstMessage.data.some(line => + line.toLowercase().includes('compiled successfully'), + ); + return isFinishedCompiling + ? 'finished 🎉' + : 'compiling...hang in there 🦥'; + } + + if ( + firstMessage?.data.includes('ERROR') || + firstMessage?.data.includes('error') + ) { + return 'error 🚨'; + } + + return firstMessage?.data?.includes('compiled successfully') + ? 'finished 🎉' + : 'compiling...hang in there 🦥'; + } + + return 'unknown'; +}; + +const ProcessOutput = ({ output, processName }) => { + return ( +
+

+ Status: {getStatusOfProcess(output, processName)} +

+
+ + {output?.flatMap((msg, index) => { + if (msg.type === 'cache') { + return msg.data.map((line, cacheIndex) => ( + + )); + } + return ( + + + + ); + })} + +
+
+ ); +}; + +ProcessOutput.propTypes = { + output: PropTypes.array.isRequired, + processName: PropTypes.string.isRequired, +}; + +export default ProcessOutput; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ServerControls.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ServerControls.jsx new file mode 100644 index 000000000000..25f4614154ea --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ServerControls.jsx @@ -0,0 +1,58 @@ +// components/servers/ServerControls.jsx +import React from 'react'; +import PropTypes from 'prop-types'; + +const ServerControls = ({ processName, displayName, isRunning, onStart }) => { + return ( +
+

+ {displayName} +

+ <> +
+ {isRunning ? ( + <> + Process: Running{' '} + + + + + ) : ( + <> + Status: Stopped{' '} + + + + + )} +
+ onStart(processName)} + text={`${isRunning ? 'Configure' : 'Start'} ${processName}`} + secondary + /> + +
+ ); +}; + +ServerControls.propTypes = { + displayName: PropTypes.string.isRequired, + isRunning: PropTypes.bool.isRequired, + processName: PropTypes.string.isRequired, + startConfig: PropTypes.object.isRequired, + stopPort: PropTypes.number.isRequired, + onStart: PropTypes.func.isRequired, + onStop: PropTypes.func.isRequired, +}; + +export default ServerControls; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/Servers.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/Servers.jsx new file mode 100644 index 000000000000..256528a53c7a --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/Servers.jsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { FrontendServerColumn } from './FrontendServerColumn'; +import { MockServerColumn } from './MockServerColumn'; +import { useProcessManager } from '../../../context/processManager'; + +export const Servers = () => { + const { fetchManifests, fetchStatus } = useProcessManager(); + + useEffect( + () => { + fetchManifests(); + fetchStatus(); + }, + [fetchManifests, fetchStatus], + ); + + return ( +
+
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/constants.js b/src/applications/_mock-form-ae-design-patterns/vadx/constants.js new file mode 100644 index 000000000000..15e96f2c5906 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/constants.js @@ -0,0 +1,7 @@ +const constants = { + API_BASE_URL: 'http://localhost:1337', + FRONTEND_PROCESS_NAME: 'frontend-server', + MOCK_SERVER_PROCESS_NAME: 'mock-server', +}; + +module.exports = constants; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx new file mode 100644 index 000000000000..5f9a9c87cf66 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx @@ -0,0 +1,239 @@ +import React, { + createContext, + useContext, + useState, + useRef, + useCallback, + useMemo, +} from 'react'; +import PropTypes from 'prop-types'; +import { formatDate } from '../utils/dates'; +import { API_BASE_URL } from '../constants'; + +const ProcessManagerContext = createContext(null); + +export const ProcessManagerProvider = ({ children }) => { + const [processes, setProcesses] = useState({}); + const [activeApps, setActiveApps] = useState({}); + const [output, setOutput] = useState({}); + const eventSourcesRef = useRef({}); + const [manifests, setManifests] = useState([]); + + const handleSSEMessage = useCallback((processName, event) => { + const parsedEvent = JSON.parse(event.data); + + if (parsedEvent.type === 'status') { + if (parsedEvent.data === 'stopped') { + // remove the process from the output + setOutput(prev => { + // eslint-disable-next-line no-unused-vars + const { [processName]: _, ...rest } = prev; + return rest; + }); + return; + } + setProcesses(prev => ({ + ...prev, + [processName]: { + ...prev[processName], + status: parsedEvent.data.status, + lastUpdate: parsedEvent.data.timestamp, + metadata: parsedEvent.data.metadata, + }, + })); + return; + } + + if (parsedEvent.type === 'stdout' || parsedEvent.type === 'stderr') { + setOutput(prev => ({ + ...prev, + [processName]: [ + { + id: Date.now(), + friendlyDate: formatDate(new Date().toISOString()), + ...parsedEvent, + }, + ...(prev[processName] || []), + ], + })); + } + + if (parsedEvent.type === 'cache') { + setOutput(prev => ({ + ...prev, + [processName]: [ + ...(prev[processName] || []), + ...parsedEvent.data.map((line, index) => ({ + id: `${processName}-cache-${index}`, + friendlyDate: formatDate(new Date().toISOString()), + data: line, + })), + ], + })); + } + }, []); + + // Move all the event source and process management logic here + const setupEventSource = useCallback( + processName => { + const eventSource = new EventSource( + `${API_BASE_URL}/events/${processName}`, + ); + + eventSource.onmessage = event => { + handleSSEMessage(processName, event); + }; + + eventSource.onerror = () => { + eventSource.close(); + delete eventSourcesRef.current[processName]; + }; + + return eventSource; + }, + [handleSSEMessage], + ); + + const fetchStatus = useCallback( + async () => { + try { + const response = await fetch(`${API_BASE_URL}/status`); + const { processes: processStatus, apps } = await response.json(); + setProcesses(processStatus); + setActiveApps(apps); + + // Setup or tear down event sources based on process status + Object.keys(processStatus).forEach(processName => { + if ( + processStatus[processName] && + !eventSourcesRef.current[processName] + ) { + eventSourcesRef.current[processName] = setupEventSource( + processName, + ); + } else if ( + !processStatus[processName] && + eventSourcesRef.current[processName] + ) { + eventSourcesRef.current[processName].close(); + delete eventSourcesRef.current[processName]; + } + }); + } catch (error) { + setProcesses({}); + setActiveApps([]); + } + }, + [setupEventSource], + ); // Empty dependency array since it only uses stable references + + const fetchManifests = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/manifests`); + const data = await response.json(); + setManifests(data.manifests); + return data.manifests; + } catch (error) { + return []; + } + }, []); // Empty dependency array since it only uses stable references + + const startProcess = async (processName, processConfig) => { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout + + const response = await fetch(`${API_BASE_URL}/start-${processName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(processConfig), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + delete eventSourcesRef.current[processName]; + fetchStatus(); + return true; + } + + throw new Error('Failed to start process'); + } catch (error) { + if (error.name === 'AbortError') { + // remove process from event sources + delete eventSourcesRef.current[processName]; + fetchStatus(); + return true; + } + return false; + } + }; + + const stopProcess = async (processName, port) => { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout + + const response = await fetch(`${API_BASE_URL}/stop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ port }), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + if (eventSourcesRef.current[processName]) { + eventSourcesRef.current[processName].close(); + delete eventSourcesRef.current[processName]; + } + fetchStatus(); + return true; + } + + throw new Error('Failed to stop process'); + } catch (error) { + if (error.name === 'AbortError') { + // This is expected when server restarts + fetchStatus(); + return true; + } + return false; + } + }; + + const value = { + processes, + output, + startProcess, + stopProcess, + fetchStatus, + setupEventSource, + manifests, + fetchManifests, + activeApps: useMemo(() => activeApps, [activeApps]), + setOutput, + }; + + return ( + + {children} + + ); +}; + +ProcessManagerProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useProcessManager = () => { + const context = useContext(ProcessManagerContext); + if (!context) { + throw new Error( + 'useProcessManager must be used within a ProcessManagerProvider', + ); + } + return context; +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js b/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js index 98bffcc69624..18b824fd7c37 100644 --- a/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js +++ b/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js @@ -94,6 +94,12 @@ export const VADXProvider = ({ children }) => { [broadcastChannel], ); + const createUpdateHandlerByKey = key => { + return update => { + setSyncedData({ ...preferences, [key]: update }); + }; + }; + // update the loading state for the dev tools const updateDevLoading = isLoading => { setSyncedData({ ...preferences, isDevLoading: isLoading }); @@ -117,6 +123,9 @@ export const VADXProvider = ({ children }) => { setSyncedData({ ...preferences, showVADX: show }); }; + const updateFeApi = createUpdateHandlerByKey('feApiUrl'); + const updateBeApi = createUpdateHandlerByKey('beApiUrl'); + // update local toggles const updateLocalToggles = useCallback( async toggles => { @@ -148,10 +157,12 @@ export const VADXProvider = ({ children }) => { return ( { updateShowVADX, updateLocalToggles, updateClearLocalToggles, + updateFeApi, + updateBeApi, debouncedSetSearchQuery, - togglesLoading, - togglesState, }} > {children} diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx index 6efb6c00ecd3..cf765d8cd241 100644 --- a/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx +++ b/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx @@ -1,5 +1,6 @@ import React, { Suspense } from 'react'; import PropTypes from 'prop-types'; +import { Servers } from './app/pages/servers/Servers'; import { VADXProvider } from './context/vadx'; import { VADXPanelLoader } from './panel/VADXPanelLoader'; @@ -33,3 +34,5 @@ VADX.propTypes = { featureToggleName: PropTypes.string, plugin: PropTypes.object, }; + +export { Servers as VADXServersRoute }; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx index 9830ee426e71..4634aff3f3ec 100644 --- a/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx +++ b/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link, withRouter } from 'react-router'; import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; import ChapterAnalyzer from './ChapterAnalyzer'; import { FormDataViewer } from './FormDataViewer'; @@ -69,4 +70,8 @@ const FormTabBase = props => { ); }; +FormTabBase.propTypes = { + router: PropTypes.object.isRequired, +}; + export const FormTab = withRouter(FormTabBase); diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js new file mode 100644 index 000000000000..82bfed18a4c2 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js @@ -0,0 +1,44 @@ +const MOCK_SERVER_PATHS = [ + 'src/applications/_mock-form-ae-design-patterns/mocks/server.js', + 'src/applications/appeals/shared/tests/mock-api.js', + 'src/applications/avs/api/mocks/index.js', + 'src/applications/check-in/api/local-mock-api/index.js', + 'src/applications/combined-debt-portal/combined/utils/mocks/mockServer.js', + 'src/applications/disability-benefits/2346/mocks/index.js', + 'src/applications/disability-benefits/all-claims/local-dev-mock-api/index.js', + 'src/applications/education-letters/testing/response.js', + 'src/applications/financial-status-report/mocks/responses.js', + 'src/applications/health-care-supply-reordering/mocks/index.js', + 'src/applications/ivc-champva/10-10D/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/ivc-champva/10-7959C/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/ivc-champva/10-7959f-1/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/mhv-landing-page/mocks/api/index.js', + 'src/applications/mhv-medications/mocks/api/index.js', + 'src/applications/mhv-secure-messaging/api/mocks/index.js', + 'src/applications/mhv-supply-reordering/mocks/index.js', + 'src/applications/my-education-benefits/testing/responses.js', + 'src/applications/personalization/dashboard/mocks/server.js', + 'src/applications/personalization/notification-center/mocks/server.js', + 'src/applications/personalization/profile/mocks/server.js', + 'src/applications/personalization/review-information/tests/fixtures/mocks/local-mock-responses.js', + 'src/applications/post-911-gib-status/mocks/server.js', + 'src/applications/representative-appoint/mocks/server.js', + 'src/applications/simple-forms/20-10206/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/21-0845/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/21-0966/tests/e2e/fixtures/mocks/local-mock-api-responses.js', + 'src/applications/simple-forms/21-0972/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/21-10210/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/21-4138/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/21-4142/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/40-0247/tests/e2e/fixtures/mocks/local-mock-api-reponses.js', + 'src/applications/simple-forms/form-upload/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/simple-forms/mock-simple-forms-patterns/tests/e2e/fixtures/mocks/local-mock-responses.js', + 'src/applications/travel-pay/services/mocks/index.js', + 'src/applications/vaos/services/mocks/index.js', + 'src/platform/mhv/api/mocks/index.js', + 'src/platform/mhv/downtime/mocks/api/index.js', + 'src/platform/testing/local-dev-mock-api/common.js', +]; + +module.exports = { MOCK_SERVER_PATHS }; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js new file mode 100644 index 000000000000..1c7ccebc8b5c --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js @@ -0,0 +1,34 @@ +/* eslint-disable no-console */ +const express = require('express'); + +const cors = require('./utils/cors'); +const { initializeManifests } = require('./utils/manifests'); +const { autoStartServers } = require('./utils/processes'); +const parseArgs = require('./utils/parseArgs'); + +const app = express(); +const port = 1337; +const args = parseArgs(); + +app.use(express.json()); + +// Allow CORS +app.use(cors); + +const router = express.Router(); + +router.use(require('./routes/events')); +router.use(require('./routes/manifests')); +router.use(require('./routes/output')); +router.use(require('./routes/start-mock-server')); +router.use(require('./routes/start-frontend-server')); +router.use(require('./routes/status')); +router.use(require('./routes/stop-on-port')); + +app.use(router); + +app.listen(port, async () => { + await initializeManifests(); + await autoStartServers(args); + console.log(`Process manager server listening at http://localhost:${port}`); +}); diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js new file mode 100644 index 000000000000..3dc223a0ea15 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js @@ -0,0 +1,36 @@ +const express = require('express'); +const { outputCache, clients, sendSSE } = require('../utils/processes'); + +const router = express.Router(); + +router.get('/events/:name', (req, res) => { + const { name } = req.params; + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + + // Send the current cache immediately + if (outputCache[name]) { + sendSSE(res, { type: 'cache', data: outputCache[name] }); + } + + // Add this client to the list of clients for this process + if (!clients.has(name)) { + clients.set(name, []); + } + clients.get(name).push(res); + + // Remove the client when the connection is closed + req.on('close', () => { + const clientsForProcess = clients.get(name) || []; + const index = clientsForProcess.indexOf(res); + if (index !== -1) { + clientsForProcess.splice(index, 1); + } + }); +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js new file mode 100644 index 000000000000..529a411380fe --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js @@ -0,0 +1,15 @@ +const express = require('express'); +const { getCachedManifests } = require('../utils/manifests'); + +const router = express.Router(); + +router.get('/manifests', (req, res) => { + const manifests = getCachedManifests(); + res.json({ + success: true, + count: manifests.length, + manifests, + }); +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js new file mode 100644 index 000000000000..d267b03066c5 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js @@ -0,0 +1,21 @@ +const express = require('express'); +const { outputCache } = require('../utils/processes'); + +const router = express.Router(); + +router.get('/output/:name', (req, res) => { + const { name } = req.params; + if (outputCache[name]) { + res.json(outputCache[name]); + } else { + res + .status(404) + .json({ error: `No output cache found for process ${name}` }); + } +}); + +router.get('/output', (req, res) => { + res.json({ all: outputCache }); +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js new file mode 100644 index 000000000000..b4e3f4a042fd --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js @@ -0,0 +1,85 @@ +const express = require('express'); +const { killProcessOnPort } = require('../utils/processes'); +const { startProcess } = require('../utils/processes'); +const paths = require('../utils/paths'); +const logger = require('../utils/logger'); +const { getCachedManifests } = require('../utils/manifests'); +const { FRONTEND_PROCESS_NAME } = require('../../constants'); + +const router = express.Router(); + +router.post('/start-frontend-server', async (req, res) => { + const { entries = [] } = req.body; + + if (!Array.isArray(entries) || entries.length === 0) { + return res.status(400).json({ + success: false, + message: 'Request body must include an array of entry strings', + }); + } + + try { + const manifests = getCachedManifests(); + + // Find manifests that match the requested entries + const validManifests = entries + .map(entry => manifests.find(manifest => manifest.entryName === entry)) + .filter(Boolean); + + // check if we found all requested entries + if (validManifests.length !== entries.length) { + const foundEntries = validManifests.map(m => m.entryName); + const invalidEntries = entries.filter( + entry => !foundEntries.includes(entry), + ); + + // provides the invalid entries and the available entries + // might remove the available entries in the future just to be more consistent + // but this way we can provide the user with the available entries + return res.status(400).json({ + success: false, + message: 'Invalid entry names provided', + invalidEntries, + availableEntries: manifests.map(m => m.entryName).filter(Boolean), + }); + } + + // Use only the validated entry names from the manifests + // this way user input is not used to start the server + const validatedEntries = validManifests.map(m => m.entryName); + + await killProcessOnPort('3001'); + + const result = await startProcess( + FRONTEND_PROCESS_NAME, + 'yarn', + [ + '--cwd', + paths.root, + 'watch', + '--env', + `entry=${validatedEntries.join(',')}`, + 'api=http://localhost:3000', + ], + { + forceRestart: true, + metadata: { + entries: validatedEntries, + }, + }, + ); + + logger.debug('result', result); + + return res.status(200).json(result); + } catch (error) { + logger.error('Error in /start-frontend-server:', error); + return res.status(500).json({ + success: false, + message: 'Failed to validate entries', + error: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js new file mode 100644 index 000000000000..e2db13f5c1d0 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js @@ -0,0 +1,57 @@ +const express = require('express'); +const path = require('path'); +const { killProcessOnPort, startProcess } = require('../utils/processes'); +const paths = require('../utils/paths'); +const { MOCK_SERVER_PATHS } = require('../constants/mockServerPaths'); +const { MOCK_SERVER_PROCESS_NAME } = require('../../constants'); + +const router = express.Router(); + +router.post('/start-mock-server', async (req, res) => { + const { responsesPath } = req.body; + + if (!responsesPath) { + return res.status(400).json({ + success: false, + message: 'responsesPath is required', + }); + } + + // Normalize the path for comparison just in case there are any issues with the path string + const normalizedPath = path.normalize(responsesPath).replace(/\\/g, '/'); + + const matchingPathIndex = MOCK_SERVER_PATHS.findIndex( + allowedPath => + path.normalize(allowedPath).replace(/\\/g, '/') === normalizedPath, + ); + + if (matchingPathIndex === -1) { + return res.status(403).json({ + success: false, + message: 'Invalid responses path', + allowedPaths: MOCK_SERVER_PATHS, // I might remove this in the future + }); + } + + // Use the validated path from our array + // this way we are not using user input to start the server + const validatedPath = path.join( + paths.root, + MOCK_SERVER_PATHS[matchingPathIndex], + ); + + await killProcessOnPort('3000'); + + const result = await startProcess( + MOCK_SERVER_PROCESS_NAME, + 'node', + [paths.mockApi, '--responses', validatedPath], + { + forceRestart: true, + }, + ); + + return res.json(result); +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js new file mode 100644 index 000000000000..a4f896d1df7a --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js @@ -0,0 +1,82 @@ +// server/routes/status.js +const express = require('express'); +const { processes } = require('../utils/processes'); +const { getCachedManifests } = require('../utils/manifests'); +const logger = require('../utils/logger'); +const { FRONTEND_PROCESS_NAME } = require('../../constants'); + +const router = express.Router(); + +/** + * Extracts entry names from frontend process args + * @param {string[]} args - Process spawn arguments + * @returns {string[]} Array of entry names + */ +const getEntryNamesFromArgs = args => { + const entryArg = args.find(arg => arg.startsWith('entry=')); + if (!entryArg) return []; + + // Split on comma to handle multiple entries + return entryArg.replace('entry=', '').split(','); +}; + +/** + * Gets app information from manifests for given entry names + * @param {string[]} entryNames - Array of entry names to look up + * @returns {Object[]} Array of app information objects + */ +const getAppInfo = async entryNames => { + const apps = []; + const manifestFiles = getCachedManifests(); + + entryNames.forEach(entryName => { + const manifest = manifestFiles.find(m => m.entryName === entryName); + if (manifest) { + apps.push({ + entryName, + rootUrl: manifest.rootUrl, + appName: manifest.appName || '', + }); + } + }); + + return apps; +}; + +router.get('/status', async (req, res) => { + try { + // Get basic process status info + const processStatus = Object.keys(processes).reduce((acc, name) => { + acc[name] = { + pid: processes[name].pid, + killed: processes[name].killed, + exitCode: processes[name].exitCode, + signalCode: processes[name].signalCode, + args: processes[name].spawnargs, + }; + return acc; + }, {}); + + // Get route information for frontend process if running + let apps = []; + if (processStatus[FRONTEND_PROCESS_NAME]) { + const entryNames = getEntryNamesFromArgs( + processStatus[FRONTEND_PROCESS_NAME].args, + ); + apps = await getAppInfo(entryNames); + } + + res.json({ + processes: processStatus, + apps, + }); + } catch (error) { + logger.error('Error getting status:', error); + res.status(500).json({ + error: 'Failed to get server status', + message: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js new file mode 100644 index 000000000000..836dbe6a98ca --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js @@ -0,0 +1,45 @@ +const express = require('express'); +const { killProcessOnPort } = require('../utils/processes'); +const logger = require('../utils/logger'); + +const router = express.Router(); + +router.post('/stop', async (req, res) => { + const { port: portToStop } = req.body; + + // Validate port is a number and within allowed range + // adds a bit of protection from invalid port numbers + const port = parseInt(portToStop, 10); + if (Number.isNaN(port) || port < 1024 || port > 65535) { + return res.status(400).json({ + success: false, + message: 'Invalid port number. Must be between 1024 and 65535', + }); + } + + // Only allow stopping known development ports + // if 1337 is in the list, we are stopping the whole server manager aka this server + const allowedPorts = [3000, 3001, 3002, 1337]; + if (!allowedPorts.includes(port)) { + return res.status(403).json({ + success: false, + message: 'Not allowed to stop processes on this port', + }); + } + + try { + await killProcessOnPort(port); + return res.json({ + success: true, + message: `Process on port ${port} stopped`, + }); + } catch (error) { + logger.error(`Error stopping process on port ${port}:`, error); + return res.status(500).json({ + success: false, + message: `Error stopping process on port ${port}: ${error.message}`, + }); + } +}); + +module.exports = router; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js new file mode 100644 index 000000000000..8eb7f45e6b30 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js @@ -0,0 +1,42 @@ +/** + * _LOCAL USE ONLY_ - middleware to enable cross-origin requests (CORS). + * Sets necessary CORS headers and handles preflight requests. + * + * @middleware + * @param {import('express').Request} req - Express request object + * @param {import('express').Response} res - Express response object + * @param {import('express').NextFunction} next - Express next middleware function + * @returns {void|Response} Returns 200 for preflight requests, otherwise calls next() + * + * @example + * // Apply as middleware to Express app or router + * app.use(cors); + * + * @security + * - Only for use in local server + * - Allows all origins ('*') + * - Allows all common request methods for REST + * - Allows X-Requested-With and content-type headers + * - Enables credentials + */ +const cors = (req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + ); + res.setHeader( + 'Access-Control-Allow-Headers', + 'X-Requested-With,content-type', + ); + res.setHeader('Access-Control-Allow-Credentials', true); + + // Handle preflight requests by immediately responding with 200 + if (req.method === 'OPTIONS') { + return res.sendStatus(200); + } + + return next(); +}; + +module.exports = cors; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js new file mode 100644 index 000000000000..184256ed6a1e --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js @@ -0,0 +1,73 @@ +/* eslint-disable no-console */ +const chalk = require('chalk'); + +/** + * @typedef {(name: string, type: string, message: string|Buffer) => void} ProcessFn + * Process-specific logging function + */ + +/** + * @typedef {(...args: any[]) => void} LoggerFn + * Generic logger function + */ + +/** + * @typedef {Object} Logger + * @property {LoggerFn} info - log blue message with `[INFO]` prefix + * @property {LoggerFn} success - log green message with `[SUCCESS]` prefix + * @property {LoggerFn} warn - log yellow message with `[WARN]` prefix + * @property {LoggerFn} error - log red message with `[ERROR]` prefix + * @property {LoggerFn} debug - log gray message with `[DEBUG]` prefix + * @property {ProcessFn} process - log process-specific messages with name and type + */ + +/** + * Logger for server-side logging with colored output. + * Only creates this logger when running in Node.js environment. + * Returns a no-op logger in non-Node environments to prevent accidental logging. + * + * @type {Logger} + * + * @example + * logger.info('Server started on port 3000'); + * logger.success('Database connected successfully'); + * logger.warn('Rate limit approaching'); + * logger.error('Failed to connect to database'); + * logger.debug(data); + * + * // Log process-specific messages, red for stderr type, blue for stdout or other types + * logger.process('Server', 'stdout', 'Server initialized'); + * logger.process('Worker', 'stderr', 'Memory limit exceeded'); + * + * // Handling Buffer messages + * const buf = Buffer.from('Process output'); + * logger.process('Process', 'stdout', buf); + */ +const logger = + typeof process !== 'undefined' && process.versions?.node + ? { + info: (...args) => console.log(chalk.blue('[INFO]'), ...args), + success: (...args) => console.log(chalk.green('[SUCCESS]'), ...args), + warn: (...args) => console.log(chalk.yellow('[WARN]'), ...args), + error: (...args) => console.log(chalk.red('[ERROR]'), ...args), + debug: (...args) => console.log(chalk.gray('[DEBUG]'), ...args), + /** + * Log process-specific messages + * @type {ProcessFn} + */ + process: (name, type, message) => { + const color = type === 'stderr' ? 'red' : 'blue'; + const text = Buffer.isBuffer(message) ? message.toString() : message; + console.log(chalk[color](`[${name}] ${type}:`), text); + }, + } + : { + info: () => {}, + success: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + process: () => {}, + }; + +module.exports = logger; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js new file mode 100644 index 000000000000..1ecfbb7dce02 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js @@ -0,0 +1,105 @@ +const fs = require('fs').promises; +const path = require('path'); +const logger = require('./logger'); +const paths = require('./paths'); + +/** + * @typedef {Object} ManifestFile + * @property {string} path - file path to the manifest.json + * @property {string} entryName - name of the entry file + * @property {string} rootUrl - root url of the app + * @property {string} appName - name of the app + * @property {string} [productId] - product id of the app + */ + +/** @type {ManifestFile[]} */ +let _cachedManifests = []; + +/** + * Searches a directory for manifest.json files + * @param {string} dir - Dir path to search + * @returns {Promise} Array of manifest objects + */ +async function findManifestFiles(dir) { + const manifests = []; + + /** + * Recursively searches directories for manifest files + * @param {string} currentDir - Current directory being searched + * @returns {Promise} + */ + async function searchDir(currentDir) { + try { + const files = await fs.readdir(currentDir); + const fileStats = await Promise.all( + files.map(file => { + const filePath = path.join(currentDir, file); + return fs.stat(filePath).then(stat => ({ file, filePath, stat })); + }), + ); + + const dirPromises = []; + const manifestPromises = []; + + for (const { file, filePath, stat } of fileStats) { + // there were a few files called manifest.json in the node_modules folder + // so we need to filter them out + if (file !== 'node_modules') { + if (stat.isDirectory()) { + dirPromises.push(searchDir(filePath)); + } else if (file === 'manifest.json') { + try { + manifestPromises.push( + fs.readFile(filePath, 'utf8').then(content => ({ + path: filePath, + ...JSON.parse(content), + })), + ); + } catch (err) { + logger.error(`Error reading manifest at ${filePath}:`, err); + } + } + } + } + + // using Promise.all to run all promises in parallel and not wait for each one + await Promise.all(dirPromises); + manifests.push(...(await Promise.all(manifestPromises))); + } catch (err) { + logger.error(`Error reading directory ${currentDir}:`, err); + } + } + + await searchDir(dir); + return manifests; +} + +/** + * Returns the currently cached manifests. + * Used because just accessing the `_cachedManifests` variable directly + * will only return the initial value `[]` + * @returns {ManifestFile[]} Array of cached manifest objects + */ +const getCachedManifests = () => _cachedManifests; + +/** + * Creates the manifest cache. + * Used during vadx startup + * @returns {Promise} + * @throws {Error} If there is an error reading the manifests + */ +async function initializeManifests() { + try { + logger.debug('Scanning for manifests in:', paths.applications); + _cachedManifests = await findManifestFiles(paths.applications); + logger.info(`Loaded ${_cachedManifests.length} manifests at startup`); + } catch (error) { + logger.error('Error loading manifests at startup:', error); + _cachedManifests = []; + } +} + +module.exports = { + initializeManifests, + getCachedManifests, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js new file mode 100644 index 000000000000..8a76055ca1b0 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js @@ -0,0 +1,20 @@ +const commandLineArgs = require('command-line-args'); + +const optionDefinitions = [ + { name: 'entry', type: String, defaultValue: 'mock-form-ae-design-patterns' }, + { name: 'api', type: String, defaultValue: 'http://localhost:3000' }, + { name: 'responses', type: String }, +]; + +function parseArgs() { + const options = commandLineArgs(optionDefinitions); + return { + entry: options.entry, + api: options.api, + responses: + options.responses || + `src/applications/_mock-form-ae-design-patterns/mocks/server.js`, + }; +} + +module.exports = parseArgs; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js new file mode 100644 index 000000000000..6a8ec44f452e --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js @@ -0,0 +1,40 @@ +const path = require('path'); +const fs = require('fs'); + +const findRoot = startDir => { + let currentDir = startDir; + + // Walk up the directory tree until we find package.json or hit the root + while (currentDir !== path.parse(currentDir).root) { + const pkgPath = path.join(currentDir, 'package.json'); + + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + // Verify this is the right package.json + if (pkg.name === 'vets-website') { + return currentDir; + } + } catch (e) { + // Continue if package.json is invalid + } + } + + currentDir = path.dirname(currentDir); + } + + throw new Error('Could not find vets-website root directory'); +}; + +// Get absolute paths that can be used anywhere +const paths = { + root: findRoot(__dirname), + get applications() { + return path.join(this.root, 'src/applications'); + }, + get mockApi() { + return path.join(this.root, 'src/platform/testing/e2e/mockapi.js'); + }, +}; + +module.exports = paths; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js new file mode 100644 index 000000000000..ee2c9750b6a0 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js @@ -0,0 +1,201 @@ +const { spawn, exec } = require('child_process'); +const { stripAnsi } = require('./strings'); +const logger = require('./logger'); +const paths = require('./paths'); +const { FRONTEND_PROCESS_NAME } = require('../../constants'); + +const processes = {}; +const outputCache = {}; +const MAX_CACHE_LINES = 100; +const clients = new Map(); + +function killProcessOnPort(portToKill) { + return new Promise((resolve, reject) => { + const isWin = process.platform === 'win32'; + + let command; + if (isWin) { + command = `FOR /F "tokens=5" %a in ('netstat -aon ^| find ":${portToKill}" ^| find "LISTENING"') do taskkill /F /PID %a`; + } else { + command = `lsof -ti :${portToKill} | xargs kill -9`; + } + + exec(command, error => { + if (error) { + logger.error(`Error killing process on port ${portToKill}: ${error}`); + reject(error); + } else { + logger.debug(`Process on port ${portToKill} killed`); + resolve(); + } + }); + }); +} + +function sendSSE(res, data) { + res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +function addToCache(name, type, data) { + if (!outputCache[name]) { + outputCache[name] = []; + } + + const strippedData = stripAnsi(data.toString().trim()); + + // Send SSE to all connected clients for this process + const clientsForProcess = clients.get(name) || []; + clientsForProcess.forEach(client => { + sendSSE(client, { type, data: strippedData }); + }); + + if (type === 'status') { + return; + } + + outputCache[name].unshift(strippedData); + if (outputCache[name].length > MAX_CACHE_LINES) { + outputCache[name].pop(); + } +} + +// Modify setupProcessHandlers to include status events +function setupProcessHandlers(childProcess, procName, metadata) { + addToCache(procName, 'status', { + status: 'started', + timestamp: Date.now(), + metadata, + }); + + const statusInterval = setInterval(() => { + if (processes[procName]) { + const clientsForProcess = clients.get(procName) || []; + clientsForProcess.forEach(client => { + sendSSE(client, { + type: 'status', + data: { status: 'running', metadata }, + }); + }); + } else { + clearInterval(statusInterval); + } + }, 5000); + + childProcess.stdout.on('data', data => { + logger.process(procName, 'stdout', data); + addToCache(procName, 'stdout', data); + }); + + childProcess.stderr.on('data', data => { + logger.process(procName, 'stderr', data); + addToCache(procName, 'stderr', data); + }); + + childProcess.on('close', code => { + logger.process(procName, 'close', code); + addToCache(procName, 'status', 'stopped'); + clearInterval(statusInterval); + delete processes[procName]; + }); +} + +function startProcess(procName, command, args, options = {}) { + const { forceRestart = false } = options; + + const { metadata } = options; + + logger.debug(`Starting process: ${procName}`); + logger.debug(`Command: ${command}`); + logger.debug(`Args: ${args}`); + logger.debug({ metadata }); + return new Promise(resolve => { + if (processes[procName]) { + if (!forceRestart) { + return resolve({ + success: false, + message: `Process ${procName} is already running`, + }); + } + + logger.debug(`Force stopping existing process: ${procName}`); + const oldProcess = processes[procName]; + + // Clean up the old process + oldProcess.on('close', () => { + delete processes[procName]; + if (outputCache[procName]) { + outputCache[procName] = []; + } + + // Start new process after old one is fully cleaned up + const childProcess = spawn(command, args, { + env: process.env, + }); + processes[procName] = childProcess; + + setupProcessHandlers(childProcess, procName, metadata); + + resolve({ + success: true, + message: `Process ${procName} restarted`, + }); + }); + + return oldProcess.kill(); + } + // No existing process, just start a new one + const childProcess = spawn(command, args, { + env: process.env, + }); + processes[procName] = childProcess; + + setupProcessHandlers(childProcess, procName, metadata); + + return resolve({ + success: true, + message: `Process ${procName} started`, + }); + }); +} + +async function autoStartServers(options = {}) { + const { entry, api, responses } = options; + + await killProcessOnPort('3000'); + await killProcessOnPort('3001'); + + await startProcess( + FRONTEND_PROCESS_NAME, + 'yarn', + ['--cwd', paths.root, 'watch', '--env', `entry=${entry}`, `api=${api}`], + { + forceRestart: true, + metadata: { + entries: [entry], + }, + }, + ); + + await startProcess( + 'mock-server', + 'yarn', + ['--cwd', paths.root, 'mock-api', '--responses', responses], + { + forceRestart: true, + metadata: { + responses, + }, + }, + ); +} + +module.exports = { + processes, + outputCache, + clients, + sendSSE, + addToCache, + startProcess, + autoStartServers, + killProcessOnPort, +}; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js new file mode 100644 index 000000000000..74634c574925 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js @@ -0,0 +1,19 @@ +/** + * Strip ANSI escape codes from a string + * this makes the output more readable in the UI for terminal output + * @param {string} str - The string to strip ANSI escape codes from + * @returns {string} - The string with ANSI escape codes removed + */ +function stripAnsi(str) { + // couldn't figure out a good way to strip ansi codes + // from the output without using a regex that contained control characters + // this eslint rule also is created to avoid mistakes as these control characters are 'rarely used' + // but in our case we need to strip them + return str.replace( + // eslint-disable-next-line no-control-regex + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + '', + ); +} + +module.exports = { stripAnsi }; diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js b/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js index d403e248f375..da5381aae8a2 100644 --- a/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js +++ b/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js @@ -123,7 +123,7 @@ class HeadingHierarchyAnalyzer { if (h1Count > 1) { issues.push({ type: 'multiple-h1', - message: `Page has ${h1Count} h1 headings (should have exactly 1)`, + message: `Page has multiple (${h1Count}) top level H1 headings and should have exactly 1`, }); } diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js b/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js new file mode 100644 index 000000000000..15787e517808 --- /dev/null +++ b/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js @@ -0,0 +1,10 @@ +import { format, parseISO } from 'date-fns'; + +export const formatDate = dateString => { + try { + const date = parseISO(dateString); + return format(date, 'HH:mm:ss:aaaaa'); + } catch (error) { + return dateString; // Return original string if parsing fails + } +}; diff --git a/src/applications/accredited-representative-portal/actions/user.js b/src/applications/accredited-representative-portal/actions/user.js deleted file mode 100644 index 35805b2dbde2..000000000000 --- a/src/applications/accredited-representative-portal/actions/user.js +++ /dev/null @@ -1,46 +0,0 @@ -import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; -import environment from '~/platform/utilities/environment'; - -export const FETCH_USER = 'FETCH_USER'; -export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'; -export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'; - -export function fetchUser() { - return async dispatch => { - dispatch({ - type: FETCH_USER, - }); - - try { - const path = '/accredited_representative_portal/v0/user'; - const user = await apiRequest(`${environment.API_URL}${path}`); - - /** - * This is an even stricter success condition than having a user. We - * additionally require what is needed for access token refreshing to - * function. - */ - const serviceName = user?.profile?.signIn?.serviceName; - if (!serviceName) - throw new Error('Missing user with sign in service name.'); - - // Needed for access token refreshing to function. - sessionStorage.setItem('serviceName', serviceName); - - dispatch({ - type: FETCH_USER_SUCCESS, - payload: user, - }); - } catch (e) { - const error = - e.errors?.[0]?.detail || - e.message || - 'Unknown error while fetching user'; - - dispatch({ - type: FETCH_USER_FAILURE, - error, - }); - } - }; -} diff --git a/src/applications/accredited-representative-portal/app-entry.jsx b/src/applications/accredited-representative-portal/app-entry.jsx index f0b459e6b464..897324feef1f 100644 --- a/src/applications/accredited-representative-portal/app-entry.jsx +++ b/src/applications/accredited-representative-portal/app-entry.jsx @@ -2,26 +2,27 @@ import '@department-of-veterans-affairs/platform-polyfills'; import React from 'react'; import { Provider } from 'react-redux'; -import { RouterProvider } from 'react-router-dom'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import startReactApp from '@department-of-veterans-affairs/platform-startup/react'; -import { connectFeatureToggle } from 'platform/utilities/feature-toggles'; import './sass/accredited-representative-portal.scss'; import './sass/POARequestCard.scss'; import './sass/POARequestDetails.scss'; +import './sass/Header.scss'; import manifest from './manifest.json'; -import router from './routes'; -import createReduxStore from './store'; -import rootReducer from './reducers'; +import routes from './routes'; +import createReduxStore from './utilities/store'; window.appName = manifest.entryName; -const store = createReduxStore(rootReducer); -connectFeatureToggle(store.dispatch); + +const router = createBrowserRouter(routes, { + basename: '/representative', +}); startReactApp( - + , ); diff --git a/src/applications/accredited-representative-portal/auth.js b/src/applications/accredited-representative-portal/auth.js deleted file mode 100644 index 7390ad8fe493..000000000000 --- a/src/applications/accredited-representative-portal/auth.js +++ /dev/null @@ -1,21 +0,0 @@ -import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; - -let user = null; - -export async function fetchUser() { - try { - user = await apiRequest('/accredited_representative_portal/v0/user'); - return user; - } catch (error) { - user = null; - throw error; - } -} - -export function getUser() { - return user; -} - -export function isAuthenticated() { - return user !== null; -} diff --git a/src/applications/accredited-representative-portal/components/DigitalSubmissionAlert/DigitalSubmissionAlert.jsx b/src/applications/accredited-representative-portal/components/DigitalSubmissionAlert/DigitalSubmissionAlert.jsx deleted file mode 100644 index e7dc7a516afa..000000000000 --- a/src/applications/accredited-representative-portal/components/DigitalSubmissionAlert/DigitalSubmissionAlert.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -const DigitalSubmissionAlert = () => ( - -

- Veterans can now digitally submit form 21-22 from VA.gov -

-

- Veterans can now{' '} - - find a VSO - {' '} - and{' '} - - sign and submit - {' '} - a digital version of form 21-22. Digital submissions will immediately - populate in the table below. -

-
-); - -export default DigitalSubmissionAlert; diff --git a/src/applications/accredited-representative-portal/components/ErrorBoundary.jsx b/src/applications/accredited-representative-portal/components/ErrorBoundary.jsx new file mode 100644 index 000000000000..496a21c3ee71 --- /dev/null +++ b/src/applications/accredited-representative-portal/components/ErrorBoundary.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +// import { useRouteError } from 'react-router-dom'; + +const ErrorBoundary = () => { + // const error = useRouteError(); + // console.log(error); + + return ( + +

We’re sorry. Something went wrong.

+
+

+ Please refresh this page or check back later. You can also check the + system status on the VA.gov homepage. +

+
+
+ ); +}; + +export default ErrorBoundary; diff --git a/src/applications/accredited-representative-portal/components/common/Footer/Footer.jsx b/src/applications/accredited-representative-portal/components/Footer.jsx similarity index 100% rename from src/applications/accredited-representative-portal/components/common/Footer/Footer.jsx rename to src/applications/accredited-representative-portal/components/Footer.jsx diff --git a/src/applications/accredited-representative-portal/components/Header.jsx b/src/applications/accredited-representative-portal/components/Header.jsx new file mode 100644 index 000000000000..d54457d763b5 --- /dev/null +++ b/src/applications/accredited-representative-portal/components/Header.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import GovBanner from './Header/GovBanner'; +import Nav from './Header/Nav'; + +const Header = () => { + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileOfficialGovtWebsite.jsx b/src/applications/accredited-representative-portal/components/Header/GovBanner.jsx similarity index 64% rename from src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileOfficialGovtWebsite.jsx rename to src/applications/accredited-representative-portal/components/Header/GovBanner.jsx index 0e08e4c815cd..008911623ea7 100644 --- a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileOfficialGovtWebsite.jsx +++ b/src/applications/accredited-representative-portal/components/Header/GovBanner.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import recordEvent from '~/platform/monitoring/record-event'; -export const MobileOfficialGovtWebsite = () => { +export const GovBanner = () => { const [expanded, setExpanded] = useState(false); const onToggle = () => { @@ -15,47 +15,54 @@ export const MobileOfficialGovtWebsite = () => { }; return ( -
- {/* Banner */} -
+
+
U.S. flag {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */}
{/* Expanded section */} {expanded && (
{ ); }; -export default MobileOfficialGovtWebsite; +export default GovBanner; diff --git a/src/applications/accredited-representative-portal/components/Header/Nav.jsx b/src/applications/accredited-representative-portal/components/Header/Nav.jsx new file mode 100644 index 000000000000..af5ccd308ffa --- /dev/null +++ b/src/applications/accredited-representative-portal/components/Header/Nav.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Link, useLoaderData } from 'react-router-dom'; + +import { getSignInUrl } from '../../utilities/constants'; +import UserNav from './UserNav'; + +function SignInButton() { + return ( + + Sign in + + ); +} + +export const Nav = () => { + const profile = useLoaderData()?.profile; + + return ( + + ); +}; + +export default Nav; diff --git a/src/applications/accredited-representative-portal/components/Header/NavDropdown.jsx b/src/applications/accredited-representative-portal/components/Header/NavDropdown.jsx new file mode 100644 index 000000000000..7aeb89df7270 --- /dev/null +++ b/src/applications/accredited-representative-portal/components/Header/NavDropdown.jsx @@ -0,0 +1,95 @@ +import React, { useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +// https://stackoverflow.com/a/54292872/1055505 +function useMouseDownOutside(callback) { + const elementRef = useRef(); + const callbackRef = useRef(); + + // set current callback in ref, before second useEffect uses it + useEffect(() => { + // useEffect wrapper to be safe for concurrent mode + callbackRef.current = callback; + }); + + useEffect(() => { + // read most recent callback and elementRef dom node from refs + function handleMouseDown(event) { + if (!elementRef.current || !callbackRef.current) return; + if (elementRef.current.contains(event.target)) return; + + callbackRef.current(event); + } + + document.addEventListener('mousedown', handleMouseDown); + return () => document.removeEventListener('mousedown', handleMouseDown); + }, []); // no need for callback + elementRef dep + + return elementRef; // return ref; client can omit `useRef` +} + +const NavDropdown = ({ + btnText, + icon, + className, + name, + iconClassName, + secondaryIcon, + srText, + children, + dataTestId, +}) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen); + const dropdownRef = useMouseDownOutside(() => setIsDropdownOpen(false)); + + return ( +
+ {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */} + + {isDropdownOpen && ( +
+
    {children}
+
+ )} +
+ ); +}; + +NavDropdown.propTypes = { + btnText: PropTypes.string, + children: PropTypes.object, + className: PropTypes.string, + dataTestId: PropTypes.string, + icon: PropTypes.string, + iconClassName: PropTypes.string, + name: PropTypes.string, + secondaryIcon: PropTypes.string, + srText: PropTypes.string, +}; + +export default NavDropdown; diff --git a/src/applications/accredited-representative-portal/components/Header/UserNav.jsx b/src/applications/accredited-representative-portal/components/Header/UserNav.jsx new file mode 100644 index 000000000000..94d44a2535ef --- /dev/null +++ b/src/applications/accredited-representative-portal/components/Header/UserNav.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { SIGN_OUT_URL } from '../../utilities/constants'; +import NavDropdown from './NavDropdown'; + +const UserHelpLinks = () => { + return ( + <> +
  • + + POA Requests + +
  • +
  • + + Get Help + +
  • + + ); +}; + +const UserNavLinks = () => { + return ( + + ); +}; + +function UserNav({ profile }) { + return ( + <> +
    + + + +
    +
    + + + + + + + +
    + + ); +} + +export default UserNav; diff --git a/src/applications/accredited-representative-portal/components/LandingPageWelcome/LandingPageWelcome.jsx b/src/applications/accredited-representative-portal/components/LandingPageWelcome/LandingPageWelcome.jsx deleted file mode 100644 index 8b03cbb809d5..000000000000 --- a/src/applications/accredited-representative-portal/components/LandingPageWelcome/LandingPageWelcome.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; - -const LandingPageWelcome = ({ firstName = '', children }) => ( -
    -
    -
    -
    -
    -

    - Welcome to the Accredited Representative Portal - {firstName && `, ${firstName}`} -

    -

    - Manage power of attorney requests -

    -

    - A system to help you get power of attorney and then support - Veterans by acting on their behalf. -

    - - Until sign in is added use this to simulate sign in - -
    -
    - {children} -
    -
    -
    -); - -LandingPageWelcome.propTypes = { - children: PropTypes.node, - firstName: PropTypes.string, -}; - -export default LandingPageWelcome; diff --git a/src/applications/accredited-representative-portal/components/NoPOAPermissionsAlert/NoPOAPermissionsAlert.jsx b/src/applications/accredited-representative-portal/components/NoPOAPermissionsAlert/NoPOAPermissionsAlert.jsx deleted file mode 100644 index 53aa1b541d47..000000000000 --- a/src/applications/accredited-representative-portal/components/NoPOAPermissionsAlert/NoPOAPermissionsAlert.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -const NoPOAPermissionsAlert = () => ( -
    -
    - -

    - You do not have permission to manage power of attorney requests -

    -
    -
      -
    • - - Do you have questions about gaining these permissions?{' '} - - Contact OGC -
    • -
    -
    -
    -
    -
    -); - -export default NoPOAPermissionsAlert; diff --git a/src/applications/accredited-representative-portal/components/NotInPilotAlert/NotInPilotAlert.jsx b/src/applications/accredited-representative-portal/components/NotInPilotAlert/NotInPilotAlert.jsx deleted file mode 100644 index ca4ddd9de221..000000000000 --- a/src/applications/accredited-representative-portal/components/NotInPilotAlert/NotInPilotAlert.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -// TODO: Add email addresses -const questions = [ - { - text: 'Would you like to join the pilot?', - email: 'addAnEmail@va.gov', - linkText: 'Contact add org here', - }, - { - text: 'Do you need help with SEP and other VA digital tools?', - email: 'addAnEmail@va.gov', - linkText: 'Contact add org here', - }, - { - text: 'Do you have questions about the accreditation process?', - email: 'ogcaccreditationmailbox@va.gov', - linkText: 'Contact OGC', - }, -]; - -const NotInPilotAlert = () => ( -
    -
    - -

    - Accredited Representative Portal is currently in pilot -

    -
    -
      - {questions.map((question, index) => ( -
    • - - {question.text}{' '} - - {question.linkText} -
    • - ))} -
    -
    -
    -
    -
    -); - -export default NotInPilotAlert; diff --git a/src/applications/accredited-representative-portal/components/POARequestCard/POARequestCard.jsx b/src/applications/accredited-representative-portal/components/POARequestCard.jsx similarity index 99% rename from src/applications/accredited-representative-portal/components/POARequestCard/POARequestCard.jsx rename to src/applications/accredited-representative-portal/components/POARequestCard.jsx index 406c7f7656fa..50c2fd7c2cf3 100644 --- a/src/applications/accredited-representative-portal/components/POARequestCard/POARequestCard.jsx +++ b/src/applications/accredited-representative-portal/components/POARequestCard.jsx @@ -32,7 +32,7 @@ const POARequestCard = ({ poaRequest, id }) => { > {poaRequest.status} - + View details for

    { - const words = segment.split('-'); - - const acronymsFixed = words.map(word => { - if (acronymMapping[word]) { - return acronymMapping[word]; - } - return lowerCase(word); - }); - return upperFirst(acronymsFixed.join(' ')); -}; - -const Breadcrumbs = () => { - const { pathname } = useLocation(); - const pathSegments = pathname.split('/').filter(Boolean); - let pathAccumulator = ''; - const breadcrumbs = pathSegments.map(segment => { - pathAccumulator += `/${segment}`; - return { - href: pathAccumulator, - label: formatSegment(segment), - }; - }); - breadcrumbs.unshift({ href: '/', label: 'Home' }); - const bcString = JSON.stringify(breadcrumbs); - - return ( - - ); -}; - -export default Breadcrumbs; diff --git a/src/applications/accredited-representative-portal/components/common/ErrorMessage.jsx b/src/applications/accredited-representative-portal/components/common/ErrorMessage.jsx deleted file mode 100644 index 0e2916572c15..000000000000 --- a/src/applications/accredited-representative-portal/components/common/ErrorMessage.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -const ErrorMessage = () => ( - -

    We’re sorry. Something went wrong.

    -
    -

    - Please refresh this page or check back later. You can also check the - system status on the VA.gov homepage. -

    -
    -
    -); - -export default ErrorMessage; diff --git a/src/applications/accredited-representative-portal/components/common/Header/Header.jsx b/src/applications/accredited-representative-portal/components/common/Header/Header.jsx deleted file mode 100644 index 1e0fc944589b..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/Header.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import MobileHeader from './MobileHeader/MobileHeader'; -import WiderThanMobileHeader from './WiderThanMobileHeader/WiderThanMobileHeader'; - -import './Header.scss'; - -const Header = () => { - return ( -
    - - -
    - ); -}; - -export default Header; diff --git a/src/applications/accredited-representative-portal/components/common/Header/Header.scss b/src/applications/accredited-representative-portal/components/common/Header/Header.scss deleted file mode 100644 index 5b3c7217e051..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/Header.scss +++ /dev/null @@ -1,106 +0,0 @@ -@import "~@department-of-veterans-affairs/css-library/dist/tokens/scss/variables"; -@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/mixins"; - -.arp-header { - min-height: 78px; - - @media (min-width: $medium-screen) { - min-height: 98px; - } - - .va-header-logo-wrapper { - display: flex; - align-items: center; - } - - .user-nav { - white-space: nowrap; - - .loading-icon-container { - color: var(--vads-color-white); - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; - } - - .icon { - width: 26px; - height: 24px; - } - - .user-dropdown-email { - margin-left: 4px; - display: block; - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .mobile { - .header-us-flag { - height: 20px; - } - - .expand-official-govt-explanation { - font-size: 12px; - } - - .govt-expanded-arrow { - transform: rotate(180deg); - transform-origin: 50% 50%; - } - - .va-crisis-line-container { - margin: 0px; - } - - .arp-logo { - width: 320px; - } - - .user-nav { - margin-left: 10px; - - .user-dropdown-email { - max-width: 64px; - } - } - - @media (min-width: $medium-screen) { - display: none !important; - } - } - - .wider-than-mobile { - display: none; - - @media (min-width: $medium-screen) { - display: block; - - .arp-logo { - width: 480px; - } - - .vet-toolbar { - display: flex; - justify-content: flex-end; - flex: 0 1 300px; - - .user-nav { - margin-inline: 10px; - - .loading-icon-container { - width: 88px; - } - } - } - - .va-header-logo-menu { - padding-left: 10px; - } - } - } -} diff --git a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileHeader.jsx b/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileHeader.jsx deleted file mode 100644 index a231e76d0408..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileHeader.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import MobileOfficialGovtWebsite from './MobileOfficialGovtWebsite'; -import MobileLogoRow from './MobileLogoRow'; - -const MobileHeader = () => { - return ( -
    -
    - -
    - -
    - ); -}; - -export default MobileHeader; diff --git a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileLogoRow.jsx b/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileLogoRow.jsx deleted file mode 100644 index 63be5ee2f8dc..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/MobileHeader/MobileLogoRow.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import UserNav from '../common/UserNav'; - -export const MobileLogoRow = () => { - return ( - - ); -}; - -export default MobileLogoRow; diff --git a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileHeader.jsx b/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileHeader.jsx deleted file mode 100644 index 7f9d789e376f..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileHeader.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import WiderThanMobileLogoRow from './WiderThanMobileLogoRow'; -import WiderThanMobileOfficialGovtWebsite from './WiderThanMobileOfficialGovtWebsite'; - -const WiderThanMobileHeader = () => { - return ( -
    -
    -
    - -
    -
    - -
    - ); -}; - -export default WiderThanMobileHeader; diff --git a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileLogoRow.jsx b/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileLogoRow.jsx deleted file mode 100644 index 429ab16aab3c..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileLogoRow.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -import UserNav from '../common/UserNav'; - -const WiderThanMobileLogoRow = () => { - return ( -
    -
    - - VA Accredited Representative Portal Logo, U.S. Department of Veterans Affairs - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - ); -}; - -export default WiderThanMobileLogoRow; diff --git a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileOfficialGovtWebsite.jsx b/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileOfficialGovtWebsite.jsx deleted file mode 100644 index b6cdc1c60616..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/WiderThanMobileHeader/WiderThanMobileOfficialGovtWebsite.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState } from 'react'; - -const WiderThanMobileOfficialGovtWebsite = () => { - const [isExpanded, setIsExpanded] = useState(false); - - const toggleExpansion = () => { - setIsExpanded(!isExpanded); - }; - return ( -
    -
    -
    -
    - U.S. flag - -

    An official website of the United States government

    - {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component, react/button-has-type */} - -
    -
    -
    -
    - Dot gov -
    -

    - The .gov means it’s official. -
    - Federal government websites often end in .gov or .mil. Before - sharing sensitive information, make sure you’re on a federal - government site. -

    -
    -
    -
    - SSL -
    -

    - The site is secure. -
    The https:// ensures that you’re - connecting to the official website and that any information you - provide is encrypted and sent securely. -

    -
    -
    -
    -
    -
    - ); -}; - -export default WiderThanMobileOfficialGovtWebsite; diff --git a/src/applications/accredited-representative-portal/components/common/Header/common/UserNav.jsx b/src/applications/accredited-representative-portal/components/common/Header/common/UserNav.jsx deleted file mode 100644 index 4d84fd551a37..000000000000 --- a/src/applications/accredited-representative-portal/components/common/Header/common/UserNav.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useState, useRef, useEffect, useContext } from 'react'; -import PropTypes from 'prop-types'; -import UserContext from '../../../../userContext'; -import { SIGN_IN_URL, SIGN_OUT_URL } from '../../../../constants'; - -const generateUniqueId = () => - `account-menu-${Math.random() - .toString(36) - .substring(2, 11)}`; - -const UserNav = ({ isMobile }) => { - const user = useContext(UserContext); - const profile = user?.profile; - const isLoading = !profile; - const uniqueId = useRef(generateUniqueId()); - - const [isDropdownOpen, setDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - - const toggleDropdown = () => setDropdownOpen(!isDropdownOpen); - - useEffect( - () => { - const handleClickOutside = event => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target) - ) { - setDropdownOpen(false); - } - }; - - if (isDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, - [isDropdownOpen], - ); - - let content; - if (isLoading) { - content = ( -
    - -
    - ); - } else if (!profile && isMobile) { - content = ( - - Sign in - - ); - } else if (!profile && !isMobile) { - content = ( - - Sign in - - ); - } else if (profile) { - content = ( -
    - {/* eslint-disable-next-line @department-of-veterans-affairs/prefer-button-component */} - -
    - -
    -
    - ); - } - - return
    {content}
    ; -}; - -UserNav.propTypes = { - isMobile: PropTypes.bool, -}; - -export default UserNav; diff --git a/src/applications/accredited-representative-portal/constants/index.js b/src/applications/accredited-representative-portal/constants/index.js deleted file mode 100644 index d999604373a3..000000000000 --- a/src/applications/accredited-representative-portal/constants/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as SIS from './sis'; -import * as USIP from './usip'; - -export const SIGN_IN_URL = (() => { - const url = new URL(USIP.PATH, USIP.BASE_URL); - url.searchParams.set(USIP.QUERY_PARAMS.application, USIP.APPLICATIONS.ARP); - url.searchParams.set(USIP.QUERY_PARAMS.OAuth, true); - return url; -})(); - -export const SIGN_OUT_URL = (() => { - const url = new URL(SIS.API_URL({ endpoint: 'logout' })); - url.searchParams.set( - SIS.QUERY_PARAM_KEYS.CLIENT_ID, - sessionStorage.getItem('ci'), - ); - return url; -})(); diff --git a/src/applications/accredited-representative-portal/constants/sis.js b/src/applications/accredited-representative-portal/constants/sis.js deleted file mode 100644 index edc0e4a41c20..000000000000 --- a/src/applications/accredited-representative-portal/constants/sis.js +++ /dev/null @@ -1,9 +0,0 @@ -export { - API_SIGN_IN_SERVICE_URL as API_URL, - CLIENT_IDS, - OAUTH_KEYS as QUERY_PARAM_KEYS, -} from '~/platform/utilities/oauth/constants'; - -export { - logoutUrlSiS as getLogoutUrl, -} from '~/platform/utilities/oauth/utilities'; diff --git a/src/applications/accredited-representative-portal/constants/usip.js b/src/applications/accredited-representative-portal/constants/usip.js deleted file mode 100644 index 418e80d4161d..000000000000 --- a/src/applications/accredited-representative-portal/constants/usip.js +++ /dev/null @@ -1,12 +0,0 @@ -import environment from '@department-of-veterans-affairs/platform-utilities/environment'; - -// To keep isolated application status, this is hardcoded rather than cross-app -// imported from `login/manifest.json`. -// https://depo-platform-documentation.scrollhelp.site/developer-docs/how-to-add-your-application-to-the-allow-list -export const PATH = '/sign-in'; -export const { BASE_URL } = environment; - -export { - AUTH_PARAMS as QUERY_PARAMS, - EXTERNAL_APPS as APPLICATIONS, -} from 'platform/user/authentication/constants'; diff --git a/src/applications/accredited-representative-portal/containers/App.jsx b/src/applications/accredited-representative-portal/containers/App.jsx index d7ef04d313f7..d18f6d3923ba 100644 --- a/src/applications/accredited-representative-portal/containers/App.jsx +++ b/src/applications/accredited-representative-portal/containers/App.jsx @@ -1,17 +1,14 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Outlet } from 'react-router'; +import React from 'react'; +import { Outlet, useNavigation } from 'react-router-dom'; import { VaLoadingIndicator } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import environment from '@department-of-veterans-affairs/platform-utilities/environment'; import { useFeatureToggle } from '~/platform/utilities/feature-toggles/useFeatureToggle'; -import Footer from '../components/common/Footer/Footer'; -import Header from '../components/common/Header/Header'; -import { fetchUser } from '../actions/user'; -import { selectIsUserLoading } from '../selectors/user'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; -const App = () => { +function App() { const { TOGGLE_NAMES: { accreditedRepresentativePortalFrontend: appToggleKey }, useToggleLoadingValue, @@ -23,10 +20,7 @@ const App = () => { const shouldExitApp = isProduction && !isAppEnabled; const isAppToggleLoading = useToggleLoadingValue(appToggleKey); - const isUserLoading = useSelector(selectIsUserLoading); - - const dispatch = useDispatch(); - useEffect(() => dispatch(fetchUser()), [dispatch]); + const navigation = useNavigation(); if (isAppToggleLoading) { return ( @@ -41,19 +35,17 @@ const App = () => { return null; } - const content = isUserLoading ? ( - - ) : ( - - ); - return (
    - {content} + {navigation.state === 'loading' ? ( + + ) : ( + + )}
    ); -}; +} export default App; diff --git a/src/applications/accredited-representative-portal/containers/LandingPage.jsx b/src/applications/accredited-representative-portal/containers/LandingPage.jsx index 98f0d50e4cb2..b469681211a7 100644 --- a/src/applications/accredited-representative-portal/containers/LandingPage.jsx +++ b/src/applications/accredited-representative-portal/containers/LandingPage.jsx @@ -1,53 +1,62 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { VaLoadingIndicator } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - -import { selectUserProfile, selectIsUserLoading } from '../selectors/user'; -import { SIGN_IN_URL } from '../constants'; -import LandingPageWelcome from '../components/LandingPageWelcome/LandingPageWelcome'; +import { useRouteLoaderData } from 'react-router'; +import { getSignInUrl } from '../utilities/constants'; const LandingPage = () => { - const profile = useSelector(selectUserProfile); - const isLoading = useSelector(selectIsUserLoading); - - if (isLoading) { - return ( -
    - -
    - ); - } - - if (profile) { - return ; - } + const user = useRouteLoaderData('root'); + const firstName = user && user.profile.firstName; return ( - -
    -
    -
    -

    - Create an account to start managing power of attorney. -

    - +
    +
    +
    +
    - Sign in or create account - +

    + Welcome to the Accredited Representative Portal + {firstName && `, ${firstName}`} +

    +

    + Manage power of attorney requests +

    +

    + A system to help you get power of attorney and then support + Veterans by acting on their behalf. +

    +
    + {!user && ( +
    +
    +
    +

    + Create an account to start managing power of attorney. +

    + + Sign in or create account + +
    +
    +
    + )}
    - +
    ); }; diff --git a/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx b/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx index 271a434fc8d2..7d4f3b42392b 100644 --- a/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx +++ b/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx @@ -1,12 +1,52 @@ import React, { useState } from 'react'; -import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; -import { Link, useLoaderData } from 'react-router-dom'; +import { Link, useLoaderData, Form, redirect } from 'react-router-dom'; import { formatDateShort } from 'platform/utilities/date/index'; import { VaRadio, VaRadioOption, } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import mockPOARequestsResponse from '../mocks/mockPOARequestsResponse.json'; + +import api from '../utilities/api'; + +const DECISION_TYPES = { + ACCEPTANCE: 'acceptance', + DECLINATION: 'declination', +}; + +const DECLINATION_OPTIONS = { + DECLINATION_HEALTH_RECORDS_WITHHELD: { + type: DECISION_TYPES.DECLINATION, + reason: + "I decline the request, because the claimant didn't provide access to health records", + }, + DECLINATION_ADDRESS_CHANGE_WITHHELD: { + type: DECISION_TYPES.DECLINATION, + reason: + "I decline the request, because the claimant didn't allow me to change their address", + }, + DECLINATION_BOTH_WITHHELD: { + type: DECISION_TYPES.DECLINATION, + reason: + 'I decline the request, because the claimant did not provide access to change address and to health records', + }, + DECLINATION_NOT_ACCEPTING_CLIENTS: { + type: DECISION_TYPES.DECLINATION, + reason: + 'I decline the request, because the VSO is not currently accepting new clients', + }, + DECLINATION_OTHER: { + type: DECISION_TYPES.DECLINATION, + reason: 'I decline for another reason', + }, +}; + +const DECISION_OPTIONS = { + ACCEPTANCE: { + type: DECISION_TYPES.ACCEPTANCE, + reason: null, + }, + ...DECLINATION_OPTIONS, +}; const checkAuthorizations = ( isTreatmentDisclosureAuthorized, @@ -25,7 +65,7 @@ const checkAuthorizations = ( }; const POARequestDetailsPage = () => { - const poaRequest = useLoaderData(); + const poaRequest = useLoaderData().attributes; const [error, setError] = useState(false); const handleChange = e => { e.preventDefault(); @@ -171,14 +211,14 @@ const POARequestDetailsPage = () => { Back to power of attorney list -
    { onVaValueChange={handleChange} required > - - - - - + + {Object.entries(DECLINATION_OPTIONS).map(([value, decision]) => ( + + ))} { > Submit decision - + ); }; -export default POARequestDetailsPage; +POARequestDetailsPage.loader = ({ params, request }) => { + return api.getPOARequest(params.id, { + signal: request.signal, + }); +}; -export async function poaRequestLoader({ params }) { - const { id } = params; +POARequestDetailsPage.createDecisionAction = async ({ request, params }) => { + const key = (await request.formData()).get('decision'); + const decision = DECISION_OPTIONS[key]; - try { - const response = await apiRequest(`/power_of_attorney_requests/${id}`, { - apiVersion: 'accredited_representative_portal/v0', - }); - return response.data; - } catch (error) { - // Return mock data if API fails (TODO: remove this before pilot and replace with commented throw below) - // throwing the error will cause the app to show the error message configured in routes.jsx - return mockPOARequestsResponse.data.find(r => r.id === Number(id)) - ?.attributes; - // throw error; - } -} + await api.createPOARequestDecision(params.id, decision); + + return redirect('..'); +}; + +export default POARequestDetailsPage; diff --git a/src/applications/accredited-representative-portal/containers/POARequestSearchPage.jsx b/src/applications/accredited-representative-portal/containers/POARequestSearchPage.jsx index 0eaafeb464cf..fb1c67653a7e 100644 --- a/src/applications/accredited-representative-portal/containers/POARequestSearchPage.jsx +++ b/src/applications/accredited-representative-portal/containers/POARequestSearchPage.jsx @@ -6,11 +6,8 @@ import { Link, } from 'react-router-dom'; -import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; - -import mockPOARequestsResponse from '../mocks/mockPOARequestsResponse.json'; -import POARequestCard from '../components/POARequestCard/POARequestCard'; -import DigitalSubmissionAlert from '../components/DigitalSubmissionAlert/DigitalSubmissionAlert'; +import api from '../utilities/api'; +import POARequestCard from '../components/POARequestCard'; const STATUSES = { PENDING: 'pending', @@ -39,18 +36,45 @@ const SearchResults = ({ poaRequests }) => { ); }; -const StatusTabLink = ({ status, searchStatus, children }) => { - const active = status === searchStatus; +const StatusTabLink = ({ tabStatus, searchStatus, children }) => { + const active = tabStatus === searchStatus; const classNames = ['poa-request__tab-link']; if (active) classNames.push('active'); return ( - + {children} ); }; +const DigitalSubmissionAlert = () => ( + +

    + Veterans can now digitally submit form 21-22 from VA.gov +

    +

    + Veterans can now{' '} + + find a VSO + {' '} + and{' '} + + sign and submit + {' '} + a digital version of form 21-22. Digital submissions will immediately + populate in the table below. +

    +
    +); + const POARequestSearchPage = () => { const poaRequests = useLoaderData(); const searchStatus = useSearchParams()[0].get('status'); @@ -62,11 +86,14 @@ const POARequestSearchPage = () => {
    - - Pending + + Pending requests Completed @@ -102,32 +129,21 @@ const POARequestSearchPage = () => { ); }; -export default POARequestSearchPage; +POARequestSearchPage.loader = ({ request }) => { + const { searchParams } = new URL(request.url); + const status = searchParams.get('status'); -export async function poaRequestsLoader({ request }) { - try { - const response = await apiRequest('/power_of_attorney_requests', { - apiVersion: 'accredited_representative_portal/v0', - }); - return response.data; - } catch (error) { - const { searchParams } = new URL(request.url); - const status = searchParams.get('status'); - - if (!Object.values(STATUSES).includes(status)) { - searchParams.set('status', STATUSES.PENDING); - throw redirect(`?${searchParams.toString()}`); - } - - // Return mock data if API fails - // TODO: Remove mock data before pilot and uncomment throw statement - const requests = mockPOARequestsResponse?.data?.map(req => req); - return requests?.filter(x => { - if (status === 'completed') { - return x.attributes.status !== 'Pending'; - } - return x.attributes.status === 'Pending'; - }); - // throw error; + if (!Object.values(STATUSES).includes(status)) { + searchParams.set('status', STATUSES.PENDING); + throw redirect(`?${searchParams}`); } -} + + return api.getPOARequests( + { status }, + { + signal: request.signal, + }, + ); +}; + +export default POARequestSearchPage; diff --git a/src/applications/accredited-representative-portal/containers/SignedInLayout.jsx b/src/applications/accredited-representative-portal/containers/SignedInLayout.jsx index 0276d4aa7033..78d2d443779c 100644 --- a/src/applications/accredited-representative-portal/containers/SignedInLayout.jsx +++ b/src/applications/accredited-representative-portal/containers/SignedInLayout.jsx @@ -1,25 +1,114 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Outlet } from 'react-router-dom'; +import environment from '@department-of-veterans-affairs/platform-utilities/environment'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles/useFeatureToggle'; import { VaLoadingIndicator } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import NoPOAPermissionsAlert from '../components/NoPOAPermissionsAlert/NoPOAPermissionsAlert'; -import NotInPilotAlert from '../components/NotInPilotAlert/NotInPilotAlert'; +export const NotInPilotAlert = () => { + // TODO: Add email addresses + const questions = [ + { + text: 'Would you like to join the pilot?', + email: 'addAnEmail@va.gov', + linkText: 'Contact add org here', + }, + { + text: 'Do you need help with SEP and other VA digital tools?', + email: 'addAnEmail@va.gov', + linkText: 'Contact add org here', + }, + { + text: 'Do you have questions about the accreditation process?', + email: 'ogcaccreditationmailbox@va.gov', + linkText: 'Contact OGC', + }, + ]; + + return ( +
    +
    + +

    + Accredited Representative Portal is currently in pilot +

    +
    +
      + {questions.map((question, index) => ( +
    • + + {question.text}{' '} + + {question.linkText} +
    • + ))} +
    +
    +
    +
    +
    + ); +}; + +export const NoPOAPermissionsAlert = () => { + return ( +
    +
    + +

    + You do not have permission to manage power of attorney requests +

    +
    +
      +
    • + + Do you have questions about gaining these permissions?{' '} + + Contact OGC +
    • +
    +
    +
    +
    +
    + ); +}; + +const SignedInLayout = () => { + const { + useToggleValue, + useToggleLoadingValue, + TOGGLE_NAMES, + } = useFeatureToggle(); + + const isPilotToggleLoading = useToggleLoadingValue( + TOGGLE_NAMES.accreditedRepresentativePortalPilot, + ); + const isInPilot = useToggleValue( + TOGGLE_NAMES.accreditedRepresentativePortalPilot, + ); + const isProduction = window.Cypress || environment.isProduction(); + + // TODO: Update with permissions check + const hasPOAPermissions = true; -const SignedInLayout = ({ - isPilotToggleLoading, - isInPilot, - isProduction, - hasPOAPermissions, -}) => { if (isPilotToggleLoading) { return (
    - +
    ); } @@ -41,11 +130,4 @@ const SignedInLayout = ({ ); }; -SignedInLayout.propTypes = { - hasPOAPermissions: PropTypes.bool, - isInPilot: PropTypes.bool, - isPilotToggleLoading: PropTypes.bool, - isProduction: PropTypes.bool, -}; - export default SignedInLayout; diff --git a/src/applications/accredited-representative-portal/containers/SignedInLayoutWrapper.jsx b/src/applications/accredited-representative-portal/containers/SignedInLayoutWrapper.jsx deleted file mode 100644 index b6fa2d6b5364..000000000000 --- a/src/applications/accredited-representative-portal/containers/SignedInLayoutWrapper.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -import environment from '@department-of-veterans-affairs/platform-utilities/environment'; -import { useFeatureToggle } from '~/platform/utilities/feature-toggles/useFeatureToggle'; -import SignedInLayout from './SignedInLayout'; - -const SignedInLayoutWrapper = () => { - const { - useToggleValue, - useToggleLoadingValue, - TOGGLE_NAMES, - } = useFeatureToggle(); - - const isPilotToggleLoading = useToggleLoadingValue( - TOGGLE_NAMES.accreditedRepresentativePortalPilot, - ); - const isInPilot = useToggleValue( - TOGGLE_NAMES.accreditedRepresentativePortalPilot, - ); - const isProduction = window.Cypress || environment.isProduction(); - - // TODO: Update with permissions check - const hasPOAPermissions = true; - return ( - - ); -}; - -export default SignedInLayoutWrapper; diff --git a/src/applications/accredited-representative-portal/mocks/mockPOARequestsResponse.json b/src/applications/accredited-representative-portal/mocks/mockPOARequestsResponse.json deleted file mode 100644 index d1b3daa35a7a..000000000000 --- a/src/applications/accredited-representative-portal/mocks/mockPOARequestsResponse.json +++ /dev/null @@ -1,224 +0,0 @@ -{ - "data": [ - { - "id": 12345, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Declined", - "declinedReason": "Refused to disclose health information", - "powerOfAttorneyCode": "012", - "submittedAt": "2024-04-10T04:51:12Z", - "expiresAt": "2024-11-31T15:20:00Z", - "acceptedOrDeclinedAt": "2024-04-10T04:51:12Z", - "isAddressChangingAuthorized": true, - "isTreatmentDisclosureAuthorized": false, - "representative": { - "firstName": "John", - "lastName": "Smith", - "email": "john.smith@vsorg.org" - }, - "claimant": { - "firstName": "Morgan", - "lastName": "Fox", - "participantId": 23456, - "relationshipToVeteran": "Child" - }, - "claimantAddress": { - "city": "Baltimore", - "state": "MD", - "zip": "21218", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - }, - { - "id": 67890, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Pending", - "powerOfAttorneyCode": "034", - "submittedAt": "2024-03-29T15:20:00Z", - "expiresAt": "2024-12-07T12:15:47Z", - "acceptedOrDeclinedAt": null, - "isAddressChangingAuthorized": false, - "isTreatmentDisclosureAuthorized": true, - "representative": { - "firstName": "Michael", - "lastName": "Johnson", - "email": "michael.johnson@vsorg.org" - }, - "claimant": { - "firstName": "Emily", - "lastName": "Wells", - "participantId": 56789, - "relationshipToVeteran": "Parent" - }, - "claimantAddress": { - "city": "New York", - "state": "NY", - "zip": "10001", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - }, - { - "id": 54321, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Accepted", - "powerOfAttorneyCode": "056", - "submittedAt": "2024-04-20T09:22:33Z", - "expiresAt": "2024-12-31T15:20:00Z", - "acceptedOrDeclinedAt": "2024-10-31T15:20:00Z", - "isAddressChangingAuthorized": true, - "isTreatmentDisclosureAuthorized": false, - "veteran": { - "firstName": "Bob", - "middleName": "G", - "lastName": "Norris", - "participantId": 800067890 - }, - "representative": { - "firstName": "Christopher", - "lastName": "Lee", - "email": "christopher.lee@vsorg.org" - }, - "claimant": { - "firstName": "Gamma", - "lastName": "Theta", - "participantId": 34567, - "relationshipToVeteran": "Child" - }, - "claimantAddress": { - "city": "San Francisco", - "state": "CA", - "zip": "94102", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - }, - { - "id": 24680, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Accepted", - "powerOfAttorneyCode": "077", - "submittedAt": "2024-02-14T10:30:00Z", - "expiresAt": "2024-11-31T15:20:00Z", - "acceptedOrDeclinedAt": "2024-02-15T14:45:22Z", - "isAddressChangingAuthorized": false, - "isTreatmentDisclosureAuthorized": false, - "veteran": { - "firstName": "Larry", - "middleName": "David", - "lastName": "Allen", - "participantId": 900077600 - }, - "representative": { - "firstName": "Alexander", - "lastName": "Williams", - "email": "alexander.williams@vsorg.org" - }, - "claimant": { - "firstName": "Jacob", - "lastName": "Parker", - "participantId": 45678, - "relationshipToVeteran": "Brother" - }, - "claimantAddress": { - "city": "Chicago", - "state": "IL", - "zip": "60607", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - }, - { - "id": 13579, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Pending", - "powerOfAttorneyCode": "089", - "submittedAt": "2024-05-01T12:15:47Z", - "expiresAt": "2024-05-01T12:15:47Z", - "acceptedOrDeclinedAt": null, - "isAddressChangingAuthorized": true, - "isTreatmentDisclosureAuthorized": true, - "veteran": { - "firstName": "Suzanne", - "middleName": "Jane", - "lastName": "Baker", - "participantId": 101112130 - }, - "representative": { - "firstName": "Benjamin", - "lastName": "Davis", - "email": "benjamin.davis@vsorg.org" - }, - "claimant": { - "firstName": "Samantha", - "lastName": "Jones", - "participantId": 98765, - "relationshipToVeteran": "Sister" - }, - "claimantAddress": { - "city": "Miami", - "state": "FL", - "zip": "33101", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - }, - { - "id": 97531, - "type": "powerOfAttorneyRequest", - "attributes": { - "status": "Pending", - "powerOfAttorneyCode": "102", - "submittedAt": "2024-03-20T08:00:00Z", - "expiresAt": "2024-05-01T12:15:47Z", - "acceptedOrDeclinedAt": null, - "isAddressChangingAuthorized": false, - "isTreatmentDisclosureAuthorized": false, - "veteran": { - "firstName": "Michael", - "middleName": "Karl", - "lastName": "Sage", - "participantId": 111213140 - }, - "representative": { - "firstName": "Nicholas", - "lastName": "Brown", - "email": "nicholas.brown@vsorg.org" - }, - "claimant": { - "firstName": "Lucas", - "lastName": "Brown", - "participantId": 65432, - "relationshipToVeteran": "Child" - }, - "claimantAddress": { - "city": "Los Angeles", - "state": "CA", - "zip": "90001", - "country": "US", - "militaryPostOffice": null, - "militaryPostalCode": null - } - } - } - ], - "meta": { - "totalRecords": "6" - } -} diff --git a/src/applications/accredited-representative-portal/reducers/index.js b/src/applications/accredited-representative-portal/reducers/index.js deleted file mode 100644 index dd1db6c8fbb5..000000000000 --- a/src/applications/accredited-representative-portal/reducers/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { combineReducers } from 'redux'; -import { FeatureToggleReducer } from 'platform/site-wide/feature-toggles/reducers'; -import userReducer from './user'; - -const rootReducer = combineReducers({ - user: userReducer, - featureToggles: FeatureToggleReducer, -}); - -export default rootReducer; diff --git a/src/applications/accredited-representative-portal/reducers/user.js b/src/applications/accredited-representative-portal/reducers/user.js deleted file mode 100644 index 01863a105914..000000000000 --- a/src/applications/accredited-representative-portal/reducers/user.js +++ /dev/null @@ -1,76 +0,0 @@ -import { combineReducers } from 'redux'; - -import { - FETCH_USER, - FETCH_USER_FAILURE, - FETCH_USER_SUCCESS, -} from '../actions/user'; - -function getNullProfileState() { - return { - firstName: null, - lastName: null, - loading: false, - }; -} - -function transformProfilePayload(payload) { - return { - firstName: payload.profile.firstName, - lastName: payload.profile.lastName, - }; -} - -function profileReducer(state = getNullProfileState(), action) { - switch (action.type) { - case FETCH_USER: - return { - ...state, - loading: true, - }; - case FETCH_USER_SUCCESS: - return { - ...state, - ...transformProfilePayload(action.payload), - loading: false, - }; - case FETCH_USER_FAILURE: - return { - ...state, - ...getNullProfileState(), - loading: false, - }; - default: - return state; - } -} - -function getNullLoginState() { - return { - currentlyLoggedIn: false, - }; -} - -function loginReducer(state = getNullLoginState(), action) { - switch (action.type) { - case FETCH_USER_SUCCESS: - return { - ...state, - currentlyLoggedIn: true, - }; - case FETCH_USER_FAILURE: - return { - ...state, - ...getNullLoginState(), - }; - default: - return state; - } -} - -const userReducer = combineReducers({ - profile: profileReducer, - login: loginReducer, -}); - -export default userReducer; diff --git a/src/applications/accredited-representative-portal/routes.jsx b/src/applications/accredited-representative-portal/routes.jsx index 0ee2e092f064..40776d591497 100644 --- a/src/applications/accredited-representative-portal/routes.jsx +++ b/src/applications/accredited-representative-portal/routes.jsx @@ -1,90 +1,98 @@ import React from 'react'; -import { - createBrowserRouter, - useNavigation, - useLoaderData, - Navigate, - Outlet, -} from 'react-router-dom'; +import { redirect } from 'react-router-dom'; -import { VaLoadingIndicator } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import App from './containers/App'; import LandingPage from './containers/LandingPage'; -import POARequestSearchPage, { - poaRequestsLoader, -} from './containers/POARequestSearchPage'; -import SignedInLayoutWrapper from './containers/SignedInLayoutWrapper'; -import POARequestDetailsPage, { - poaRequestLoader, -} from './containers/POARequestDetailsPage'; -import ErrorMessage from './components/common/ErrorMessage'; -import UserContext from './userContext'; -import { userLoader } from './userLoader'; -import { SIGN_IN_URL } from './constants'; +import POARequestSearchPage from './containers/POARequestSearchPage'; +import POARequestDetailsPage from './containers/POARequestDetailsPage'; +import SignedInLayout from './containers/SignedInLayout'; +import ErrorBoundary from './components/ErrorBoundary'; -const LoadingWrapper = () => { - const navigation = useNavigation(); +import { userPromise } from './utilities/auth'; +import { getSignInUrl } from './utilities/constants'; - if (navigation.state === 'loading') { - return ; - } +const transformRoutes = (transform, route) => { + transform(route); - return ; + const children = route.children || []; + children.forEach(childRoute => transformRoutes(transform, childRoute)); + + return route; }; -const AuthenticatedRoutes = () => { - const user = useLoaderData(); +/* eslint-disable no-param-reassign */ +const addSignInRedirection = route => { + const { loader } = route; + if (!loader) return; - if (!user || !user.profile) { - return ; - } + route.loader = async ({ params, request }) => { + if (await userPromise) { + try { + // `route.loader` results in infinite recursion, hence the alias. + return await loader({ params, request }); + } catch (e) { + /** + * Likely a temporary solution. For our singleton `userPromise` to look + * good, but then we still experience an authentication related error + * means that it has been long enough for our refresh token to have + * expired. If so, we'll bring the user to login and then return here, + * which is the same experience when hard-nav'ing here. + * + * Also of note, the platform API wrapper we're using is probably not so + * suited to us, because it erases a fair amount of error information. + */ + if (e.errors !== 'Access token JWT is malformed') { + throw e; + } + } + } - return ( - - - - ); + throw redirect( + getSignInUrl({ + returnUrl: request.url, + }), + ); + }; }; +/* eslint-enable no-param-reassign */ -const router = createBrowserRouter( - [ - { - element: , - children: [ - { - index: true, - element: , - }, - { - path: '/', - element: , - children: [ - { - loader: userLoader, - element: , - children: [ - { - path: 'poa-requests', - element: , - loader: poaRequestsLoader, - errorElement: , - }, - { - path: 'poa-requests/:id', - element: , - loader: poaRequestLoader, - errorElement: , - }, - ], - }, - ], - }, - ], - }, - ], +const routes = [ { - basename: '/representative', + id: 'root', + path: '/', + loader() { + return userPromise; + }, + element: , + errorElement: , + children: [ + { + index: true, + element: , + }, + transformRoutes(addSignInRedirection, { + element: , + children: [ + { + path: 'poa-requests', + element: , + loader: POARequestSearchPage.loader, + }, + { + path: 'poa-requests/:id', + element: , + loader: POARequestDetailsPage.loader, + children: [ + { + path: 'decision', + action: POARequestDetailsPage.createDecisionAction, + }, + ], + }, + ], + }), + ], }, -); +]; -export default router; +export default routes; diff --git a/src/applications/accredited-representative-portal/sass/Header.scss b/src/applications/accredited-representative-portal/sass/Header.scss new file mode 100644 index 000000000000..0966e3e5e5ff --- /dev/null +++ b/src/applications/accredited-representative-portal/sass/Header.scss @@ -0,0 +1,174 @@ +@import "~@department-of-veterans-affairs/css-library/dist/tokens/scss/variables"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/mixins"; + +.header { + min-height: auto; + + &__govt-site { + width: 100%; + margin: 8px 16px; + max-width: 1000px; + @media (min-width: $medium-screen) { + margin: 0 auto; + } + + &-explanation { + flex-direction: column; + color: var(--vads-color-black); + width: 91%; + font-size: 12.8px; + + @media (min-width: $medium-screen) { + flex-direction: row; + margin: 4px; + } + + &-sub-text { + text-decoration: underline; + } + } + } + + &__flag { + width: 16px; + height: 16px; + margin-right: 15px; + @media (min-width: $medium-screen) { + margin: 4px 12px 4px; + } + } +} +.user-nav { + margin-left: auto; + + &__chevron { + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); + } +} +.nav { + padding: 1.5px 8px; + flex-wrap: wrap; + + &__sign-in { + margin-left: auto; + } + + &__user-list { + list-style: none; + margin: 0; + padding: 0; + + li { + border-bottom: 1px solid $vads-color-base-light; + padding: 12px 0; + + &:last-child { + border: none; + } + } + + a { + width: max-content; + text-decoration: none; + + } + } + + &__user-btn { + width: max-content; + + &--user { + background-color: transparent !important; + box-shadow: none !important; + display: flex; + } + + &--menu { + background: var(--vads-color-white) !important; + } + + &-icon { + margin-left: 8px; + } + } + + &__container { + width: 100%; + flex-wrap: wrap; + margin: 0 auto; + max-width: 1000px; + align-items: center; + flex-wrap: nowrap; + } + + &__decorator { + display: none; + width: 100%; + height: 1px; + background-color: var(--vads-color-white); + margin-bottom: 48px; + + &--login { + margin: 0 auto !important; + } + + @media (min-width: $medium-screen) { + display: flex; + max-width: 1000px; + margin: 0 auto 48px; + } + } + + > .usa-button { + ~ .nav__decorator { + margin-bottom: 0; + } + } + &__sign-in { + width: auto; + margin-left: auto; + } + + &__link { + align-items: center; + } + &__logo { + height: 24px; + margin-right: 10px; + + &--desktop { + height: auto; + width: 636px; + margin: 16px 5px 16px 0; + } + + &-text { + display: flex; + flex-wrap: wrap; + width: 40%; + color: var(--vads-color-white); + text-decoration: none; + line-height: 15.08px; + font-size: 12px; + } + } + + &__dropdown { + width: 174px; + } + + .mobile { + @media (min-width: $medium-screen) { + display: none !important; + } + } + .desktop { + @media (max-width: $medium-screen) { + display: none !important; + } + } +} diff --git a/src/applications/accredited-representative-portal/selectors/user.js b/src/applications/accredited-representative-portal/selectors/user.js deleted file mode 100644 index 3fe40c8cc76c..000000000000 --- a/src/applications/accredited-representative-portal/selectors/user.js +++ /dev/null @@ -1,4 +0,0 @@ -export const selectIsUserLoading = state => state.user.profile.loading; -export const selectIsUserLoggedIn = state => state.user.login.currentlyLoggedIn; -export const selectUserProfile = state => - selectIsUserLoggedIn(state) ? state.user.profile : null; diff --git a/src/applications/accredited-representative-portal/store.js b/src/applications/accredited-representative-portal/store.js deleted file mode 100644 index b14d812afb76..000000000000 --- a/src/applications/accredited-representative-portal/store.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import environment from '@department-of-veterans-affairs/platform-utilities/environment'; - -const createReduxStore = rootReducer => { - const useDevTools = - !environment.isProduction() && window.__REDUX_DEVTOOLS_EXTENSION__; - - return createStore( - rootReducer, - compose( - applyMiddleware(thunk), - useDevTools ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f, - ), - ); -}; - -export default createReduxStore; diff --git a/src/applications/accredited-representative-portal/tests/e2e/accredited-representative-portal.cypress.spec.js b/src/applications/accredited-representative-portal/tests/e2e/accredited-representative-portal.cypress.spec.js index cf8a63efdc73..4799fddc2209 100644 --- a/src/applications/accredited-representative-portal/tests/e2e/accredited-representative-portal.cypress.spec.js +++ b/src/applications/accredited-representative-portal/tests/e2e/accredited-representative-portal.cypress.spec.js @@ -56,10 +56,10 @@ describe('Accredited Representative Portal', () => { cy.location('pathname').should('eq', '/sign-in/'); }); - it('displays an alert when in production and when user is not in pilot', () => { + // Bring back when repairing testing. + it.skip('displays an alert when in production and when user is not in pilot', () => { cy.axeCheck(); cy.loginArpUser(); - cy.get('[data-testid=landing-page-bypass-sign-in-link]').click(); cy.get('[data-testid=not-in-pilot-alert]').should('exist'); cy.get('[data-testid=not-in-pilot-alert-heading]').should( 'have.text', @@ -92,7 +92,6 @@ describe('Accredited Representative Portal', () => { 'have.text', 'Welcome to the Accredited Representative Portal, William', ); - cy.get('[data-testid=landing-page-bypass-sign-in-link]').click(); cy.location('pathname').should('eq', '/representative/poa-requests'); cy.axeCheck(); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/DigitalSubmissionAlert.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/DigitalSubmissionAlert.unit.spec.jsx deleted file mode 100644 index e1340e2aab6a..000000000000 --- a/src/applications/accredited-representative-portal/tests/unit/components/DigitalSubmissionAlert.unit.spec.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { expect } from 'chai'; - -import DigitalSubmissionAlert from '../../../components/DigitalSubmissionAlert/DigitalSubmissionAlert'; - -describe('DigitalSubmissionAlert', () => { - const getDigitalSubmissionAlert = () => render(); - - it('renders alert', () => { - const { getByTestId } = getDigitalSubmissionAlert(); - expect(getByTestId('digital-submission-alert')).to.exist; - }); - - it('renders heading', () => { - const { getByTestId } = getDigitalSubmissionAlert(); - expect(getByTestId('digital-submission-alert-heading').textContent).to.eq( - 'Veterans can now digitally submit form 21-22 from VA.gov', - ); - }); - - it('renders description', () => { - const { getByTestId } = getDigitalSubmissionAlert(); - expect(getByTestId('digital-submission-alert-description')).to.exist; - }); -}); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/common/ErrorMessage.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/ErrorBoundary.unit.spec.jsx similarity index 50% rename from src/applications/accredited-representative-portal/tests/unit/components/common/ErrorMessage.unit.spec.jsx rename to src/applications/accredited-representative-portal/tests/unit/components/ErrorBoundary.unit.spec.jsx index f9528ac9c8a9..257be039d8ee 100644 --- a/src/applications/accredited-representative-portal/tests/unit/components/common/ErrorMessage.unit.spec.jsx +++ b/src/applications/accredited-representative-portal/tests/unit/components/ErrorBoundary.unit.spec.jsx @@ -2,13 +2,13 @@ import React from 'react'; import { render } from '@testing-library/react'; import { expect } from 'chai'; -import ErrorMessage from '../../../../components/common/ErrorMessage'; +import ErrorBoundary from '../../../components/ErrorBoundary'; -describe('ErrorMessage', () => { - const getErrorMessage = () => render(); +describe('ErrorBoundary', () => { + const getErrorBoundary = () => render(); it('renders error message', () => { - const { getByTestId } = getErrorMessage(); + const { getByTestId } = getErrorBoundary(); expect(getByTestId('error-message')).to.exist; }); }); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/common/Footer/Footer.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/Footer.unit.spec.jsx similarity index 89% rename from src/applications/accredited-representative-portal/tests/unit/components/common/Footer/Footer.unit.spec.jsx rename to src/applications/accredited-representative-portal/tests/unit/components/Footer.unit.spec.jsx index b0fa1a946865..7327ea0fcab2 100644 --- a/src/applications/accredited-representative-portal/tests/unit/components/common/Footer/Footer.unit.spec.jsx +++ b/src/applications/accredited-representative-portal/tests/unit/components/Footer.unit.spec.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { expect } from 'chai'; -import Footer from '../../../../../components/common/Footer/Footer'; +import Footer from '../../../components/Footer'; describe('Footer', () => { const getFooter = () => render(
    ); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/Header.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/Header.unit.spec.jsx new file mode 100644 index 000000000000..69e26f1a13c0 --- /dev/null +++ b/src/applications/accredited-representative-portal/tests/unit/components/Header.unit.spec.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { expect } from 'chai'; +import Header from '../../../components/Header'; +import { renderTestComponent } from '../helpers'; + +describe('Header', () => { + it('renders header', () => { + const { getByTestId } = renderTestComponent(
    ); + expect(getByTestId('arp-header')).to.exist; + }); +}); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/Header/GovBanner.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/Header/GovBanner.unit.spec.jsx new file mode 100644 index 000000000000..dd3175c449ff --- /dev/null +++ b/src/applications/accredited-representative-portal/tests/unit/components/Header/GovBanner.unit.spec.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { expect } from 'chai'; + +import GovBanner from '../../../../components/Header/GovBanner'; + +describe('GovBanner', () => { + it('renders toggle text on mobile', () => { + const { getByTestId } = render(); + expect(getByTestId('official-govt-site-text').textContent).to.eq( + 'An official website of the United States government.', + ); + }); + + it('renders proper aria tag on click', () => { + const { getByTestId } = render(); + fireEvent.click(getByTestId('official-govt-site-toggle')); + expect( + getByTestId('official-govt-site-content').getAttribute('aria-hidden'), + ).to.eq('false'); + }); +}); diff --git a/src/applications/accredited-representative-portal/tests/unit/components/Header/Nav.unit.spec.jsx b/src/applications/accredited-representative-portal/tests/unit/components/Header/Nav.unit.spec.jsx new file mode 100644 index 000000000000..9dd152860124 --- /dev/null +++ b/src/applications/accredited-representative-portal/tests/unit/components/Header/Nav.unit.spec.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { expect } from 'chai'; + +import Nav from '../../../../components/Header/Nav'; +import { renderTestComponent } from '../../helpers'; + +describe('Nav', () => { + it('should render the mobile logo with correct alt text and source', () => { + const { getByTestId } = renderTestComponent(

    {ShowIssuesList({ issues })}

    - If an issue is missing, please{' '} - go back and add it + Go back to add more issues .

    diff --git a/src/applications/appeals/996/subtask/pages/start.jsx b/src/applications/appeals/996/subtask/pages/start.jsx index f88a541799a4..6a7d28efbc0e 100644 --- a/src/applications/appeals/996/subtask/pages/start.jsx +++ b/src/applications/appeals/996/subtask/pages/start.jsx @@ -83,8 +83,8 @@ const BenefitType = ({ data = {}, error, setPageData }) => { uswds >

    - If you don’t think this is the right form for you, find out about - other decision review options. + If you don’t think this is the right form for you, there other + decision review options.

    Learn about choosing a decision review option diff --git a/src/applications/appeals/996/tests/pages/issueSummary.unit.spec.jsx b/src/applications/appeals/996/tests/pages/issueSummary.unit.spec.jsx index 1d3199c6a288..8b11ecac3dd3 100644 --- a/src/applications/appeals/996/tests/pages/issueSummary.unit.spec.jsx +++ b/src/applications/appeals/996/tests/pages/issueSummary.unit.spec.jsx @@ -46,7 +46,7 @@ describe('HLR selected issues summary page', () => { const link = form.find('Link'); expect(link.length).to.equal(1); - expect(link.text()).to.contain('go back and add'); + expect(link.text()).to.contain('Go back to add more issues'); expect(link.props().to.pathname).to.equal(CONTESTABLE_ISSUES_PATH); expect(link.props().to.search).to.equal('?redirect'); form.unmount(); diff --git a/src/applications/appeals/shared/props.js b/src/applications/appeals/shared/props.js index ab8cc8d729c9..b96af8ce8e34 100644 --- a/src/applications/appeals/shared/props.js +++ b/src/applications/appeals/shared/props.js @@ -84,6 +84,7 @@ export const data995 = shape({ shape({ locationAndName: string, issues: arrayOf(string), + treatmentDate: string, evidenceDates: shape({ from: string, to: string, diff --git a/src/applications/appeals/testing/hlr/content/issueSummary.jsx b/src/applications/appeals/testing/hlr/content/issueSummary.jsx index 15dbb8df40f0..437180065e69 100644 --- a/src/applications/appeals/testing/hlr/content/issueSummary.jsx +++ b/src/applications/appeals/testing/hlr/content/issueSummary.jsx @@ -19,13 +19,12 @@ export const SummaryTitle = ({ formData }) => {

    If an issue is missing, please{' '} - go back and add it + Go back to add more issues .

    diff --git a/src/applications/appeals/testing/nod-new/content/issueSummary.jsx b/src/applications/appeals/testing/nod-new/content/issueSummary.jsx index 2f5ff183bba5..9f2d8ecf2564 100644 --- a/src/applications/appeals/testing/nod-new/content/issueSummary.jsx +++ b/src/applications/appeals/testing/nod-new/content/issueSummary.jsx @@ -15,12 +15,7 @@ export const SummaryTitle = ({ formData }) => { {ShowIssuesList({ issues })}

    - - Add an issue - + Add an issue

    ); diff --git a/src/applications/ask-va/components/RequireSignInModal.jsx b/src/applications/ask-va/components/RequireSignInModal.jsx index 16d0a6e4c94b..0930958ef237 100644 --- a/src/applications/ask-va/components/RequireSignInModal.jsx +++ b/src/applications/ask-va/components/RequireSignInModal.jsx @@ -1,16 +1,15 @@ import { VaModal } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import { toggleLoginModal } from '@department-of-veterans-affairs/platform-site-wide/actions'; import { focusElement } from 'platform/utilities/ui'; import PropTypes from 'prop-types'; import React, { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import manifest from '../manifest.json'; const RequireSignInModal = ({ onClose, show, restrictedItem, message }) => { - const dispatch = useDispatch(); - - const openLoginModal = () => { + const navigateToAskVAAndTriggerLoginModal = () => { onClose(); - dispatch(toggleLoginModal(true)); + window.location.href = `${ + manifest.rootUrl + }/introduction?showSignInModal=true`; }; useEffect( @@ -35,6 +34,10 @@ const RequireSignInModal = ({ onClose, show, restrictedItem, message }) => { }, }; + if (!show) { + return null; + } + return ( { modalTitle="You need to sign in" onCloseEvent={onClose} visible={show} - onPrimaryButtonClick={openLoginModal} + onPrimaryButtonClick={navigateToAskVAAndTriggerLoginModal} onSecondaryButtonClick={onClose} primaryButtonText="Sign in" secondaryButtonText={`Go back to ${ @@ -66,6 +69,7 @@ const RequireSignInModal = ({ onClose, show, restrictedItem, message }) => { RequireSignInModal.propTypes = { message: PropTypes.string, restrictedItem: PropTypes.string, + router: PropTypes.object, show: PropTypes.bool, onClose: PropTypes.func, }; diff --git a/src/applications/ask-va/components/YourPersonalInformationAuthenticated.jsx b/src/applications/ask-va/components/YourPersonalInformationAuthenticated.jsx index 7c81d10231ea..995b1c2af5d9 100644 --- a/src/applications/ask-va/components/YourPersonalInformationAuthenticated.jsx +++ b/src/applications/ask-va/components/YourPersonalInformationAuthenticated.jsx @@ -5,6 +5,7 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import FormNavButtons from '~/platform/forms-system/src/js/components/FormNavButtons'; +import { hasPrefillInformation } from '../constants'; const PersonalAuthenticatedInformation = ({ goForward, @@ -12,10 +13,14 @@ const PersonalAuthenticatedInformation = ({ isLoggedIn, router, }) => { - if (!isLoggedIn) { + if (!isLoggedIn || !hasPrefillInformation(formData)) { goForward(formData); } + const handleGoBack = () => { + router.push('/'); + }; + const { first, last, @@ -34,10 +39,6 @@ const PersonalAuthenticatedInformation = ({ ssnLastFour = ssn.substr(ssn.length - 4); } - const handleGoBack = () => { - router.push('/'); - }; - useEffect( () => { focusElement('h2'); diff --git a/src/applications/ask-va/config/form.js b/src/applications/ask-va/config/form.js index 0d937ae74374..a65a9fb83a7f 100644 --- a/src/applications/ask-va/config/form.js +++ b/src/applications/ask-va/config/form.js @@ -1,11 +1,8 @@ import { - CategoryEducation, CHAPTER_1, CHAPTER_2, CHAPTER_3, requiredForSubtopicPage, - TopicEducationBenefitsAndWorkStudy, - TopicVeteranReadinessAndEmploymentChapter31, whoIsYourQuestionAboutLabels, } from '../constants'; import manifest from '../manifest.json'; @@ -60,6 +57,7 @@ import { aboutSomeoneElseRelationshipVeteranCondition, aboutSomeoneElseRelationshipVeteranOrFamilyMemberEducationCondition, generalQuestionCondition, + whoIsYourQuestionAboutCondition, } from './helpers'; import prefillTransformer from './prefill-transformer'; @@ -157,14 +155,8 @@ const formConfig = { CustomPageReview: CustomPageReviewField, uiSchema: whoIsYourQuestionAboutPage.uiSchema, schema: whoIsYourQuestionAboutPage.schema, - // Hidden - EDU Question are always 'General Question' unless topic is VR&E - depends: form => { - if (form.selectCategory !== CategoryEducation) { - return form.selectTopic !== TopicEducationBenefitsAndWorkStudy; - } - return ( - form.selectTopic === TopicVeteranReadinessAndEmploymentChapter31 - ); + depends: formData => { + return whoIsYourQuestionAboutCondition(formData); }, }, relationshipToVeteran: { diff --git a/src/applications/ask-va/config/helpers.jsx b/src/applications/ask-va/config/helpers.jsx index c5a264a99bfe..45e0ec34ab53 100644 --- a/src/applications/ask-va/config/helpers.jsx +++ b/src/applications/ask-va/config/helpers.jsx @@ -4,6 +4,7 @@ import { enUS } from 'date-fns/locale'; import React from 'react'; import { + CategoryBenefitsIssuesOutsidetheUS, CategoryEducation, CategoryGuardianshipCustodianshipFiduciaryIssues, CategoryHealthCare, @@ -16,6 +17,7 @@ import { relationshipOptionsSomeoneElse, statesRequiringPostalCode, TopicAppraisals, + TopicEducationBenefitsAndWorkStudy, TopicSpeciallyAdapatedHousing, TopicVeteranReadinessAndEmploymentChapter31, whoIsYourQuestionAboutLabels, @@ -708,7 +710,7 @@ export const convertDateForInquirySubheader = dateString => { } // Ensure the date is valid - if (isNaN(utcDate.getTime())) { + if (Number.isNaN(utcDate.getTime())) { return 'Invalid Date'; } @@ -756,6 +758,29 @@ export const getFiles = files => { }); }; +export const isEducationNonVRE = formData => + formData.selectCategory === CategoryEducation && + formData.selectTopic !== TopicVeteranReadinessAndEmploymentChapter31; + +export const isOutsideUSEducation = formData => + formData.selectCategory === CategoryBenefitsIssuesOutsidetheUS && + formData.selectTopic === TopicEducationBenefitsAndWorkStudy; + +// Who is your question about? rules: +// CATEGORY = EDUCATION BENEFITS AND WORK STUDY +// AND +// TOPIC =/ VETERAN READINESS & EMPLOYMENT +// +// ALSO HIDDEN IF: +// CATEGORY = BENEFITS ISSUES OUTSIDE THE US +// AND +// TOPIC = EDUCATION BENEFITS AND WORK STUDY +// +// BECAUSE 'EDU' QUESTIONS ARE SENT AS "GENERAL QUESTIONS" TO CRM. BUT SHOULD CONTINUE DOWN THE 'SOMEONE ELSE' FLOW. +export const whoIsYourQuestionAboutCondition = formData => { + return !(isEducationNonVRE(formData) || isOutsideUSEducation(formData)); +}; + export const aboutMyselfRelationshipVeteranCondition = formData => { return ( formData.whoIsYourQuestionAbout === whoIsYourQuestionAboutLabels.MYSELF && @@ -832,16 +857,14 @@ export const aboutSomeoneElseRelationshipConnectedThroughWorkCondition = formDat export const aboutSomeoneElseRelationshipConnectedThroughWorkEducationCondition = formData => { return ( formData.relationshipToVeteran === relationshipOptionsSomeoneElse.WORK && - formData.selectCategory === CategoryEducation && - formData.selectTopic !== TopicVeteranReadinessAndEmploymentChapter31 + (isEducationNonVRE(formData) || isOutsideUSEducation(formData)) ); }; export const aboutSomeoneElseRelationshipVeteranOrFamilyMemberEducationCondition = formData => { return ( formData.relationshipToVeteran !== relationshipOptionsSomeoneElse.WORK && - formData.selectCategory === CategoryEducation && - formData.selectTopic !== TopicVeteranReadinessAndEmploymentChapter31 + (isEducationNonVRE(formData) || isOutsideUSEducation(formData)) ); }; diff --git a/src/applications/ask-va/config/schema-helpers/formFlowHelper.js b/src/applications/ask-va/config/schema-helpers/formFlowHelper.js index 4cfb2cc4607e..0fa03979a42b 100644 --- a/src/applications/ask-va/config/schema-helpers/formFlowHelper.js +++ b/src/applications/ask-va/config/schema-helpers/formFlowHelper.js @@ -171,8 +171,12 @@ const ch3Pages = { schema: aboutYourselfPage.schema, reviewTitle: 'Your personal information', depends: form => { - const { first, last, socialSecurityNumber } = form.aboutYourself; - return !(first && last && socialSecurityNumber); + if (!form?.aboutYourself) return true; + return ( + !form.aboutYourself.first || + !form.aboutYourself.last || + !form.aboutYourself.socialSecurityNumber + ); }, }, aboutYourselfGeneral: { @@ -181,8 +185,12 @@ const ch3Pages = { schema: aboutYourselfGeneralPage.schema, reviewTitle: 'Your personal information', depends: form => { - const { first, last, socialSecurityNumber } = form.aboutYourself; - return !(first && last && socialSecurityNumber); + if (!form?.aboutYourself) return true; + return ( + !form.aboutYourself.first || + !form.aboutYourself.last || + !form.aboutYourself.socialSecurityNumber + ); }, }, aboutYourselfRelationshipFamilyMember: { @@ -192,8 +200,12 @@ const ch3Pages = { schema: aboutYourselfRelationshipFamilyMemberPage.schema, reviewTitle: 'Your personal information', depends: form => { - const { first, last, socialSecurityNumber } = form.aboutYourself; - return !(first && last && socialSecurityNumber); + if (!form?.aboutYourself) return true; + return ( + !form.aboutYourself.first || + !form.aboutYourself.last || + !form.aboutYourself.socialSecurityNumber + ); }, }, searchSchools: { diff --git a/src/applications/ask-va/constants.js b/src/applications/ask-va/constants.js index fef78405edcb..4f02c8473804 100644 --- a/src/applications/ask-va/constants.js +++ b/src/applications/ask-va/constants.js @@ -70,6 +70,8 @@ export const CategoryGuardianshipCustodianshipFiduciaryIssues = 'Guardianship, custodianship, or fiduciary issues'; export const CategoryHousingAssistanceAndHomeLoans = 'Housing assistance and home loans'; +export const CategoryBenefitsIssuesOutsidetheUS = + 'Benefits issues outside the U.S.'; // Topics export const TopicVeteranReadinessAndEmploymentChapter31 = @@ -77,18 +79,9 @@ export const TopicVeteranReadinessAndEmploymentChapter31 = export const TopicSpeciallyAdapatedHousing = 'Specially Adapted Housing (SAH) and Special Home Adaptation (SHA) grants'; export const TopicAppraisals = 'Appraisals'; -export const requireSignInCategories = [ - CategoryEducation, - 'Education benefits and work study', - 'Disability compensation', - 'Debt for benefit overpayments and health care copay bills', - 'Benefits issues outside the U.S.', -]; export const TopicEducationBenefitsAndWorkStudy = 'Education benefits and work study'; -export const requireSignInTopics = ['Compensation', CategoryEducation]; - // list of topics required to render the subtopic page export const requiredForSubtopicPage = [ 'Board Appeals', @@ -121,7 +114,12 @@ export const branchOfServiceRuleforCategories = [ export const hasPrefillInformation = form => { const { first, last, dateOfBirth, socialOrServiceNum } = form.aboutYourself; - return !!(first && last && dateOfBirth && socialOrServiceNum); + return !!( + first && + last && + dateOfBirth && + (socialOrServiceNum.ssn || socialOrServiceNum.serviceNumber) + ); }; // Response Page headers diff --git a/src/applications/ask-va/containers/CategorySelectPage.jsx b/src/applications/ask-va/containers/CategorySelectPage.jsx index 4cfe0eed5313..85ab2895fdf6 100644 --- a/src/applications/ask-va/containers/CategorySelectPage.jsx +++ b/src/applications/ask-va/containers/CategorySelectPage.jsx @@ -20,18 +20,7 @@ const CategorySelectPage = props => { const [loading, isLoading] = useState(false); const [error, hasError] = useState(false); const [validationError, setValidationError] = useState(null); - const [showModal, setShowModal] = useState({ show: false, selected: '' }); - - const onModalNo = () => { - isLoading(true); - onChange({ - ...formData, - selectCategory: undefined, - allowAttachments: undefined, - }); - setShowModal({ show: false, selected: '' }); - setTimeout(() => isLoading(false), 200); - }; + const [showModal, setShowModal] = useState(false); const showError = data => { if (data.selectCategory) { @@ -43,10 +32,12 @@ const CategorySelectPage = props => { const handleChange = event => { const selectedValue = event.detail.value; - const selected = apiData.find(cat => cat.attributes.name === selectedValue); + const selected = apiData.find( + category => category.attributes.name === selectedValue, + ); localStorage.removeItem('askVAFiles'); if (selected.attributes.requiresAuthentication && !isLoggedIn) { - setShowModal({ show: true, selected: `${selectedValue}` }); + setShowModal(true); } else { dispatch(setCategoryID(selected.id)); onChange({ @@ -123,8 +114,8 @@ const CategorySelectPage = props => { setShowModal(false)} + show={showModal} restrictedItem="category" /> diff --git a/src/applications/ask-va/containers/IntroductionPage.jsx b/src/applications/ask-va/containers/IntroductionPage.jsx index e2381462dbcd..94d3c6540b27 100644 --- a/src/applications/ask-va/containers/IntroductionPage.jsx +++ b/src/applications/ask-va/containers/IntroductionPage.jsx @@ -7,6 +7,7 @@ import { setData } from '@department-of-veterans-affairs/platform-forms-system/a import { getNextPagePath } from '@department-of-veterans-affairs/platform-forms-system/routing'; import { isLoggedIn, + isProfileLoading, selectProfile, } from '@department-of-veterans-affairs/platform-user/selectors'; import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; @@ -15,7 +16,7 @@ import SaveInProgressIntro from 'platform/forms/save-in-progress/SaveInProgressI import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link, withRouter } from 'react-router'; import { toggleLoginModal as toggleLoginModalAction } from '~/platform/site-wide/user-nav/actions'; import { envUrl } from '../constants'; import { @@ -26,11 +27,25 @@ import { import DashboardCards from './DashboardCards'; const IntroductionPage = props => { - const { route, toggleLoginModal, loggedIn } = props; + const { route, toggleLoginModal, loggedIn, showLoadingIndicator } = props; const { formConfig, pageList, pathname, formData } = route; const [inquiryData, setInquiryData] = useState(false); const [searchReferenceNumber, setSearchReferenceNumber] = useState(''); const [hasError, setHasError] = useState(false); + const showSignInModal = () => { + toggleLoginModal(true); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if ( + params.get('showSignInModal') === 'true' && + !loggedIn && + !showLoadingIndicator + ) { + showSignInModal(); + } + }); const getStartPage = () => { const data = formData || {}; @@ -38,10 +53,6 @@ const IntroductionPage = props => { return pageList[1].path; }; - const showSignInModal = () => { - toggleLoginModal(true, 'askVA'); - }; - useEffect( () => { focusElement('.schemaform-title > h1'); @@ -287,7 +298,6 @@ const IntroductionPage = props => { return (
    - {/* */}

    {formConfig.title} @@ -298,8 +308,13 @@ const IntroductionPage = props => {

    )}
    - {loggedIn && authenticatedUI} - {!loggedIn && unAuthenticatedUI} + {showLoadingIndicator && } + {!showLoadingIndicator && ( + <> + {loggedIn && authenticatedUI} + {!loggedIn && unAuthenticatedUI} + + )}
    ); }; @@ -330,6 +345,7 @@ IntroductionPage.propTypes = { pathname: PropTypes.string, pageList: PropTypes.array, }), + showLoadingIndicator: PropTypes.bool, }; function mapStateToProps(state) { @@ -337,6 +353,7 @@ function mapStateToProps(state) { formData: state.form?.data || {}, loggedIn: isLoggedIn(state), profile: selectProfile(state), + showLoadingIndicator: isProfileLoading(state), }; } @@ -348,4 +365,4 @@ const mapDispatchToProps = dispatch => ({ export default connect( mapStateToProps, mapDispatchToProps, -)(IntroductionPage); +)(withRouter(IntroductionPage)); diff --git a/src/applications/ask-va/containers/TopicSelectPage.jsx b/src/applications/ask-va/containers/TopicSelectPage.jsx index 3a0ab8909af5..d753e23ba0ed 100644 --- a/src/applications/ask-va/containers/TopicSelectPage.jsx +++ b/src/applications/ask-va/containers/TopicSelectPage.jsx @@ -36,12 +36,7 @@ const TopicSelectPage = props => { const [loading, isLoading] = useState(false); const [error, hasError] = useState(false); const [validationError, setValidationError] = useState(null); - const [showModal, setShowModal] = useState({ show: false, selected: '' }); - - const onModalNo = () => { - onChange(''); - setShowModal({ show: false, selected: '' }); - }; + const [showModal, setShowModal] = useState(false); const showError = data => { if (data.selectTopic) { @@ -61,7 +56,7 @@ const TopicSelectPage = props => { ); if (selected.attributes.requiresAuthentication && !loggedIn) { - setShowModal({ show: true, selected: selectedValue }); + setShowModal(true); } else { dispatch(setTopicID(selected.id)); onChange({ ...formData, selectTopic: selectedValue }); @@ -134,8 +129,8 @@ const TopicSelectPage = props => { setShowModal(false)} + show={showModal} restrictedItem="topic" /> diff --git a/src/applications/ask-va/containers/WhoIsYourQuestionAboutCustomPage.jsx b/src/applications/ask-va/containers/WhoIsYourQuestionAboutCustomPage.jsx index 8581da566a6a..b99f02d2b486 100644 --- a/src/applications/ask-va/containers/WhoIsYourQuestionAboutCustomPage.jsx +++ b/src/applications/ask-va/containers/WhoIsYourQuestionAboutCustomPage.jsx @@ -22,11 +22,6 @@ const WhoIsYourQuestionAboutCustomPage = props => { }); }; - const onModalNo = () => { - onChange({ ...formData, whoIsYourQuestionAbout: undefined }); - setShowModal(false); - }; - const showError = data => { if (data.whoIsYourQuestionAbout) { goForward(data); @@ -80,7 +75,7 @@ const WhoIsYourQuestionAboutCustomPage = props => { setShowModal(false)} show={showModal} restrictedItem="question" /> diff --git a/src/applications/ask-va/routes.jsx b/src/applications/ask-va/routes.jsx index 8ec968751733..fb8908603b5d 100644 --- a/src/applications/ask-va/routes.jsx +++ b/src/applications/ask-va/routes.jsx @@ -19,12 +19,6 @@ const routes = [ indexRoute: { onEnter: (nextState, replace) => replace('/introduction') }, childRoutes: createRoutesWithSaveInProgress(formConfig), }, - { - path: '/introduction', - component: App, - indexRoute: { onEnter: (nextState, replace) => replace('/introduction') }, - childRoutes: createRoutesWithSaveInProgress(formConfig), - }, ]; export default routes; diff --git a/src/applications/ask-va/tests/e2e/fixtures/api_va_gov/feature-toggles.json b/src/applications/ask-va/tests/e2e/fixtures/api_va_gov/feature-toggles.json index ab71d912f6bc..cf323a94b882 100644 --- a/src/applications/ask-va/tests/e2e/fixtures/api_va_gov/feature-toggles.json +++ b/src/applications/ask-va/tests/e2e/fixtures/api_va_gov/feature-toggles.json @@ -2,54 +2,6 @@ "data": { "type": "feature_toggles", "features": [ - { - "name": "thisIsOnlyATest", - "value": false - }, - { - "name": "this_is_only_a_test", - "value": false - }, - { - "name": "accreditedRepresentativePortalFrontend", - "value": false - }, - { - "name": "accredited_representative_portal_frontend", - "value": false - }, - { - "name": "accreditedRepresentativePortalApi", - "value": false - }, - { - "name": "accredited_representative_portal_api", - "value": false - }, - { - "name": "aedpVadx", - "value": false - }, - { - "name": "aedp_vadx", - "value": false - }, - { - "name": "allClaimsAddDisabilitiesEnhancement", - "value": false - }, - { - "name": "all_claims_add_disabilities_enhancement", - "value": false - }, - { - "name": "appointmentsConsolidation", - "value": false - }, - { - "name": "appointments_consolidation", - "value": false - }, { "name": "askVAFormFeature", "value": false @@ -73,4343 +25,7 @@ { "name": "ask_va_introduction_page_feature", "value": false - }, - { - "name": "authExpVBADowntimeMessage", - "value": false - }, - { - "name": "auth_exp_vba_downtime_message", - "value": false - }, - { - "name": "avsEnabled", - "value": true - }, - { - "name": "avs_enabled", - "value": true - }, - { - "name": "bcasLettersUseLighthouse", - "value": true - }, - { - "name": "bcas_letters_use_lighthouse", - "value": true - }, - { - "name": "benefitsDocumentsUseLighthouse", - "value": false - }, - { - "name": "benefits_documents_use_lighthouse", - "value": false - }, - { - "name": "benefitsEducationUseLighthouse", - "value": true - }, - { - "name": "benefits_education_use_lighthouse", - "value": true - }, - { - "name": "benefitsRequireGatewayOrigin", - "value": true - }, - { - "name": "benefits_require_gateway_origin", - "value": true - }, - { - "name": "caregiverUseFacilitiesApi", - "value": true - }, - { - "name": "caregiver_use_facilities_API", - "value": true - }, - { - "name": "caregiverBrowserMonitoringEnabled", - "value": true - }, - { - "name": "caregiver_browser_monitoring_enabled", - "value": true - }, - { - "name": "caregiverCARMASubmittedAt", - "value": true - }, - { - "name": "caregiver_carma_submitted_at", - "value": true - }, - { - "name": "caregiver1010", - "value": true - }, - { - "name": "caregiver1010", - "value": true - }, - { - "name": "caregiverUseVANotifyOnSubmissionFailure", - "value": true - }, - { - "name": "caregiver_use_va_notify_on_submission_failure", - "value": true - }, - { - "name": "caregiverRetryFormValidation", - "value": true - }, - { - "name": "caregiver_retry_form_validation", - "value": true - }, - { - "name": "disabilityCompensationStagingLighthouseBrd", - "value": true - }, - { - "name": "disability_compensation_staging_lighthouse_brd", - "value": true - }, - { - "name": "documentUploadValidationEnabled", - "value": false - }, - { - "name": "document_upload_validation_enabled", - "value": false - }, - { - "name": "hcaBrowserMonitoringEnabled", - "value": true - }, - { - "name": "hca_browser_monitoring_enabled", - "value": true - }, - { - "name": "hcaDisableBGSService", - "value": false - }, - { - "name": "hca_disable_bgs_service", - "value": false - }, - { - "name": "hcaEnrollmentStatusOverrideEnabled", - "value": true - }, - { - "name": "hca_enrollment_status_override_enabled", - "value": true - }, - { - "name": "hcaInsuranceV2Enabled", - "value": true - }, - { - "name": "hca_insurance_v2_enabled", - "value": true - }, - { - "name": "hcaPerformanceAlertEnabled", - "value": false - }, - { - "name": "hca_performance_alert_enabled", - "value": false - }, - { - "name": "hcaRegOnlyEnabled", - "value": true - }, - { - "name": "hca_reg_only_enabled", - "value": true - }, - { - "name": "hcaSigiEnabled", - "value": false - }, - { - "name": "hca_sigi_enabled", - "value": false - }, - { - "name": "hcaTeraBranchingEnabled", - "value": true - }, - { - "name": "hca_tera_branching_enabled", - "value": true - }, - { - "name": "hcaUseFacilitiesApi", - "value": true - }, - { - "name": "hca_use_facilities_API", - "value": true - }, - { - "name": "hcaLogFormAttachmentCreate", - "value": false - }, - { - "name": "hca_log_form_attachment_create", - "value": false - }, - { - "name": "hcaLogEmailDiffInProgressForm", - "value": true - }, - { - "name": "hca_log_email_diff_in_progress_form", - "value": true - }, - { - "name": "hcaRetrieveFacilitiesWithoutRepopulating", - "value": false - }, - { - "name": "hca_retrieve_facilities_without_repopulating", - "value": false - }, - { - "name": "hcaZeroSilentFailures", - "value": false - }, - { - "name": "hca_zero_silent_failures", - "value": false - }, - { - "name": "cg1010OAuth2Enabled", - "value": true - }, - { - "name": "cg1010_oauth_2_enabled", - "value": true - }, - { - "name": "ezrProdEnabled", - "value": true - }, - { - "name": "ezr_prod_enabled", - "value": true - }, - { - "name": "ezrUploadEnabled", - "value": false - }, - { - "name": "ezr_upload_enabled", - "value": false - }, - { - "name": "ezrAuthOnlyEnabled", - "value": false - }, - { - "name": "ezr_auth_only_enabled", - "value": false - }, - { - "name": "ezrEmergencyContactsEnabled", - "value": false - }, - { - "name": "ezr_emergency_contacts_enabled", - "value": false - }, - { - "name": "ezrNextOfKinEnabled", - "value": false - }, - { - "name": "ezr_next_of_kin_enabled", - "value": false - }, - { - "name": "ezrUseVANotifyOnSubmissionFailure", - "value": true - }, - { - "name": "ezr_use_va_notify_on_submission_failure", - "value": true - }, - { - "name": "cernerOverride653", - "value": true - }, - { - "name": "cerner_override_653", - "value": true - }, - { - "name": "cernerOverride668", - "value": true - }, - { - "name": "cerner_override_668", - "value": true - }, - { - "name": "cernerOverride687", - "value": true - }, - { - "name": "cerner_override_687", - "value": true - }, - { - "name": "cernerOverride692", - "value": true - }, - { - "name": "cerner_override_692", - "value": true - }, - { - "name": "cernerOverride757", - "value": true - }, - { - "name": "cerner_override_757", - "value": true - }, - { - "name": "champvaMultipleStampRetry", - "value": true - }, - { - "name": "champva_multiple_stamp_retry", - "value": true - }, - { - "name": "champvaFailureEmailJobEnabled", - "value": true - }, - { - "name": "champva_failure_email_job_enabled", - "value": true - }, - { - "name": "champvaConfirmationEmailBugfix", - "value": true - }, - { - "name": "champva_confirmation_email_bugfix", - "value": true - }, - { - "name": "champvaEnhancedMonitorLogging", - "value": true - }, - { - "name": "champva_enhanced_monitor_logging", - "value": true - }, - { - "name": "champvaPdfDecrypt", - "value": true - }, - { - "name": "champva_pdf_decrypt", - "value": true - }, - { - "name": "checkInExperienceEnabled", - "value": true - }, - { - "name": "check_in_experience_enabled", - "value": true - }, - { - "name": "checkInExperiencePreCheckInEnabled", - "value": true - }, - { - "name": "check_in_experience_pre_check_in_enabled", - "value": true - }, - { - "name": "checkInExperienceUpcomingAppointmentsEnabled", - "value": true - }, - { - "name": "check_in_experience_upcoming_appointments_enabled", - "value": true - }, - { - "name": "checkInExperienceTranslationDisclaimerSpanishEnabled", - "value": true - }, - { - "name": "check_in_experience_translation_disclaimer_spanish_enabled", - "value": true - }, - { - "name": "checkInExperienceTranslationDisclaimerTagalogEnabled", - "value": true - }, - { - "name": "check_in_experience_translation_disclaimer_tagalog_enabled", - "value": true - }, - { - "name": "checkInExperienceMockEnabled", - "value": false - }, - { - "name": "check_in_experience_mock_enabled", - "value": false - }, - { - "name": "checkInExperienceTravelReimbursement", - "value": true - }, - { - "name": "check_in_experience_travel_reimbursement", - "value": true - }, - { - "name": "checkInExperienceCernerTravelClaimsEnabled", - "value": false - }, - { - "name": "check_in_experience_cerner_travel_claims_enabled", - "value": false - }, - { - "name": "checkInExperienceCheckClaimStatusOnTimeout", - "value": true - }, - { - "name": "check_in_experience_check_claim_status_on_timeout", - "value": true - }, - { - "name": "checkInExperienceBrowserMonitoring", - "value": true - }, - { - "name": "check_in_experience_browser_monitoring", - "value": true - }, - { - "name": "checkInExperienceMedicationReviewContent", - "value": true - }, - { - "name": "check_in_experience_medication_review_content", - "value": true - }, - { - "name": "claimLettersAccess", - "value": true - }, - { - "name": "claim_letters_access", - "value": true - }, - { - "name": "claimsApiSpecialIssuesUpdaterUsesLocalBGS", - "value": false - }, - { - "name": "claims_api_special_issues_updater_uses_local_bgs", - "value": false - }, - { - "name": "claimsApiFlashUpdaterUsesLocalBGS", - "value": false - }, - { - "name": "claims_api_flash_updater_uses_local_bgs", - "value": false - }, - { - "name": "claimsApiLocalBGSRefactor", - "value": false - }, - { - "name": "claims_api_local_bgs_refactor", - "value": false - }, - { - "name": "claimsApiPoaVBMSUpdaterUsesLocalBGS", - "value": true - }, - { - "name": "claims_api_poa_vbms_updater_uses_local_bgs", - "value": true - }, - { - "name": "claimsApiBdRefactor", - "value": true - }, - { - "name": "claims_api_bd_refactor", - "value": true - }, - { - "name": "claimsApiEwsUpdaterEnablesLocalBGS", - "value": false - }, - { - "name": "claims_api_ews_updater_enables_local_bgs", - "value": false - }, - { - "name": "claimsApiEwsUploadsBdRefactor", - "value": true - }, - { - "name": "claims_api_ews_uploads_bd_refactor", - "value": true - }, - { - "name": "claimsApiPoaUploadsBdRefactor", - "value": true - }, - { - "name": "claims_api_poa_uploads_bd_refactor", - "value": true - }, - { - "name": "claimsApi526ValidationsV1LocalBGS", - "value": false - }, - { - "name": "claims_api_526_validations_v1_local_bgs", - "value": false - }, - { - "name": "claimsApiUsePersonWebService", - "value": true - }, - { - "name": "claims_api_use_person_web_service", - "value": true - }, - { - "name": "claimsApiUseVetRecordService", - "value": true - }, - { - "name": "claims_api_use_vet_record_service", - "value": true - }, - { - "name": "claimsApi526V2UploadsBdRefactor", - "value": true - }, - { - "name": "claims_api_526_v2_uploads_bd_refactor", - "value": true - }, - { - "name": "confirmationPageNew", - "value": true - }, - { - "name": "confirmation_page_new", - "value": true - }, - { - "name": "lighthouseClaimsApiHardcodeWsdl", - "value": true - }, - { - "name": "lighthouse_claims_api_hardcode_wsdl", - "value": true - }, - { - "name": "cst5103UpdateEnabled", - "value": true - }, - { - "name": "cst_5103_update_enabled", - "value": true - }, - { - "name": "cstClaimPhases", - "value": true - }, - { - "name": "cst_claim_phases", - "value": true - }, - { - "name": "cstIncludeDdl5103Letters", - "value": true - }, - { - "name": "cst_include_ddl_5103_letters", - "value": true - }, - { - "name": "cstIncludeDdlBoaLetters", - "value": false - }, - { - "name": "cst_include_ddl_boa_letters", - "value": false - }, - { - "name": "cstIncludeDdlSqdLetters", - "value": true - }, - { - "name": "cst_include_ddl_sqd_letters", - "value": true - }, - { - "name": "cstUseLighthouse5103", - "value": true - }, - { - "name": "cst_use_lighthouse_5103", - "value": true - }, - { - "name": "cstUseLighthouseIndex", - "value": true - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cstUseLighthouseShow", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - }, - { - "name": "cstSendEvidenceFailureEmails", - "value": true - }, - { - "name": "cst_send_evidence_failure_emails", - "value": true - }, - { - "name": "cstSynchronousEvidenceUploads", - "value": true - }, - { - "name": "cst_synchronous_evidence_uploads", - "value": true - }, - { - "name": "cstUseDdRum", - "value": true - }, - { - "name": "cst_use_dd_rum", - "value": true - }, - { - "name": "coeAccess", - "value": true - }, - { - "name": "coe_access", - "value": true - }, - { - "name": "combinedDebtPortalAccess", - "value": true - }, - { - "name": "combined_debt_portal_access", - "value": true - }, - { - "name": "combinedFinancialStatusReport", - "value": true - }, - { - "name": "combined_financial_status_report", - "value": true - }, - { - "name": "communicationPreferences", - "value": true - }, - { - "name": "communication_preferences", - "value": true - }, - { - "name": "contactInfoChangeEmail", - "value": true - }, - { - "name": "contact_info_change_email", - "value": true - }, - { - "name": "covidVaccineRegistration", - "value": true - }, - { - "name": "covid_vaccine_registration", - "value": true - }, - { - "name": "covidVaccineRegistrationExpanded", - "value": true - }, - { - "name": "covid_vaccine_registration_expanded", - "value": true - }, - { - "name": "covidVaccineRegistrationFrontend", - "value": true - }, - { - "name": "covid_vaccine_registration_frontend", - "value": true - }, - { - "name": "covidVaccineRegistrationFrontendCta", - "value": false - }, - { - "name": "covid_vaccine_registration_frontend_cta", - "value": false - }, - { - "name": "covidVaccineRegistrationFrontendEnableExpandedEligibility", - "value": true - }, - { - "name": "covid_vaccine_registration_frontend_enable_expanded_eligibility", - "value": true - }, - { - "name": "covidVaccineRegistrationFrontendHideAuth", - "value": true - }, - { - "name": "covid_vaccine_registration_frontend_hide_auth", - "value": true - }, - { - "name": "covidVaccineSchedulingFrontend", - "value": false - }, - { - "name": "covid_vaccine_scheduling_frontend", - "value": false - }, - { - "name": "covidVolunteerIntakeBackendEnabled", - "value": true - }, - { - "name": "covid_volunteer_intake_backend_enabled", - "value": true - }, - { - "name": "covidVolunteerIntakeEnabled", - "value": true - }, - { - "name": "covid_volunteer_intake_enabled", - "value": true - }, - { - "name": "covidVolunteerUpdateEnabled", - "value": true - }, - { - "name": "covid_volunteer_update_enabled", - "value": true - }, - { - "name": "covidVolunteerDelivery", - "value": true - }, - { - "name": "covid_volunteer_delivery", - "value": true - }, - { - "name": "claimsClaimUploaderUseBd", - "value": true - }, - { - "name": "claims_claim_uploader_use_bd", - "value": true - }, - { - "name": "claimsLoadTesting", - "value": false - }, - { - "name": "claims_load_testing", - "value": false - }, - { - "name": "claimsStatusV1BGSEnabled", - "value": true - }, - { - "name": "claims_status_v1_bgs_enabled", - "value": true - }, - { - "name": "claimsStatusV2LhBenefitsDocsServiceEnabled", - "value": true - }, - { - "name": "claims_status_v2_lh_benefits_docs_service_enabled", - "value": true - }, - { - "name": "claimsHourlySlackErrorReportEnabled", - "value": false - }, - { - "name": "claims_hourly_slack_error_report_enabled", - "value": false - }, - { - "name": "claimsStatusV1LhAutoEstablishClaimEnabled", - "value": true - }, - { - "name": "claims_status_v1_lh_auto_establish_claim_enabled", - "value": true - }, - { - "name": "debtLettersShowLettersVBMS", - "value": false - }, - { - "name": "debt_letters_show_letters_vbms", - "value": false - }, - { - "name": "debtsCacheDmcEmptyResponse", - "value": true - }, - { - "name": "debts_cache_dmc_empty_response", - "value": true - }, - { - "name": "debtsCacheVBSCopaysEmptyResponse", - "value": false - }, - { - "name": "debts_cache_vbs_copays_empty_response", - "value": false - }, - { - "name": "debtsCopayLogging", - "value": false - }, - { - "name": "debts_copay_logging", - "value": false - }, - { - "name": "decisionReviewHlrEmail", - "value": true - }, - { - "name": "decision_review_hlr_email", - "value": true - }, - { - "name": "decisionReviewNodEmail", - "value": true - }, - { - "name": "decision_review_nod_email", - "value": true - }, - { - "name": "decisionReviewScEmail", - "value": true - }, - { - "name": "decision_review_sc_email", - "value": true - }, - { - "name": "decisionReviewHlrStatusUpdaterEnabled", - "value": false - }, - { - "name": "decision_review_hlr_status_updater_enabled", - "value": false - }, - { - "name": "decisionReviewNodStatusUpdaterEnabled", - "value": true - }, - { - "name": "decision_review_nod_status_updater_enabled", - "value": true - }, - { - "name": "decisionReviewScStatusUpdaterEnabled", - "value": false - }, - { - "name": "decision_review_sc_status_updater_enabled", - "value": false - }, - { - "name": "decisionReviewIcnUpdaterEnabled", - "value": false - }, - { - "name": "decision_review_icn_updater_enabled", - "value": false - }, - { - "name": "decisionReviewWeeklyErrorReportEnabled", - "value": true - }, - { - "name": "decision_review_weekly_error_report_enabled", - "value": true - }, - { - "name": "decisionReviewDailyErrorReportEnabled", - "value": true - }, - { - "name": "decision_review_daily_error_report_enabled", - "value": true - }, - { - "name": "decisionReviewDailyStuckRecordsReportEnabled", - "value": true - }, - { - "name": "decision_review_daily_stuck_records_report_enabled", - "value": true - }, - { - "name": "decisionReviewMonthlyStatsReportEnabled", - "value": true - }, - { - "name": "decision_review_monthly_stats_report_enabled", - "value": true - }, - { - "name": "decisionReviewDelayEvidence", - "value": true - }, - { - "name": "decision_review_delay_evidence", - "value": true - }, - { - "name": "decisionReviewHlrFormV4Enabled", - "value": true - }, - { - "name": "decision_review_hlr_form_v4_enabled", - "value": true - }, - { - "name": "decisionReviewScFormV4Enabled", - "value": false - }, - { - "name": "decision_review_sc_form_v4_enabled", - "value": false - }, - { - "name": "decisionReviewSavedClaimHlrStatusUpdaterJobEnabled", - "value": true - }, - { - "name": "decision_review_saved_claim_hlr_status_updater_job_enabled", - "value": true - }, - { - "name": "decisionReviewSavedClaimNodStatusUpdaterJobEnabled", - "value": true - }, - { - "name": "decision_review_saved_claim_nod_status_updater_job_enabled", - "value": true - }, - { - "name": "decisionReviewSavedClaimScStatusUpdaterJobEnabled", - "value": true - }, - { - "name": "decision_review_saved_claim_sc_status_updater_job_enabled", - "value": true - }, - { - "name": "decisionReviewDeleteSavedClaimsJobEnabled", - "value": true - }, - { - "name": "decision_review_delete_saved_claims_job_enabled", - "value": true - }, - { - "name": "decisionReviewFailureNotificationEmailJobEnabled", - "value": true - }, - { - "name": "decision_review_failure_notification_email_job_enabled", - "value": true - }, - { - "name": "decisionReviewTrack4142Submissions", - "value": true - }, - { - "name": "decision_review_track_4142_submissions", - "value": true - }, - { - "name": "decisionReviewNotify4142Failures", - "value": true - }, - { - "name": "decision_review_notify_4142_failures", - "value": true - }, - { - "name": "decisionReviewHlrNewApi", - "value": false - }, - { - "name": "decision_review_hlr_new_api", - "value": false - }, - { - "name": "decisionReviewNodNewApi", - "value": false - }, - { - "name": "decision_review_nod_new_api", - "value": false - }, - { - "name": "decisionReviewScNewApi", - "value": false - }, - { - "name": "decision_review_sc_new_api", - "value": false - }, - { - "name": "decisionReviewNewEngine4142Job", - "value": true - }, - { - "name": "decision_review_new_engine_4142_job", - "value": true - }, - { - "name": "decisionReviewNewEngineSubmitUploadJob", - "value": true - }, - { - "name": "decision_review_new_engine_submit_upload_job", - "value": true - }, - { - "name": "dependencyVerification", - "value": true - }, - { - "name": "dependency_verification", - "value": true - }, - { - "name": "dependentsEnqueueWithUserStruct", - "value": true - }, - { - "name": "dependents_enqueue_with_user_struct", - "value": true - }, - { - "name": "dependentsPensionCheck", - "value": false - }, - { - "name": "dependents_pension_check", - "value": false - }, - { - "name": "dependentsRemovalCheck", - "value": true - }, - { - "name": "dependents_removal_check", - "value": true - }, - { - "name": "dependentsManagement", - "value": false - }, - { - "name": "dependents_management", - "value": false - }, - { - "name": "dependentsTriggerActionNeededEmail", - "value": false - }, - { - "name": "dependents_trigger_action_needed_email", - "value": false - }, - { - "name": "disability526Form4142PollingRecords", - "value": true - }, - { - "name": "disability_526_form4142_polling_records", - "value": true - }, - { - "name": "disability526Form4142PollingRecordFailureEmail", - "value": false - }, - { - "name": "disability_526_form4142_polling_record_failure_email", - "value": false - }, - { - "name": "contentionClassificationClaimLinker", - "value": false - }, - { - "name": "contention_classification_claim_linker", - "value": false - }, - { - "name": "disability526EpMergeApi", - "value": true - }, - { - "name": "disability_526_ep_merge_api", - "value": true - }, - { - "name": "disability526EeProcessAlsFlash", - "value": false - }, - { - "name": "disability_526_ee_process_als_flash", - "value": false - }, - { - "name": "disability526ToxicExposure", - "value": true - }, - { - "name": "disability_526_toxic_exposure", - "value": true - }, - { - "name": "disability526ToxicExposureIpf", - "value": true - }, - { - "name": "disability_526_toxic_exposure_ipf", - "value": true - }, - { - "name": "disability526NewConfirmationPage", - "value": false - }, - { - "name": "disability_526_new_confirmation_page", - "value": false - }, - { - "name": "disability526ToxicExposureDocumentUploadPolling", - "value": true - }, - { - "name": "disability_526_toxic_exposure_document_upload_polling", - "value": true - }, - { - "name": "disability526CallReceivedEmailFromPolling", - "value": true - }, - { - "name": "disability_526_call_received_email_from_polling", - "value": true - }, - { - "name": "disability526ImprovedAutosuggestionsAddDisabilitiesPage", - "value": true - }, - { - "name": "disability_526_improved_autosuggestions_add_disabilities_page", - "value": true - }, - { - "name": "disability526ExpandedContentionClassification", - "value": false - }, - { - "name": "disability_526_expanded_contention_classification", - "value": false - }, - { - "name": "disabilityCompensationFlashes", - "value": true - }, - { - "name": "disability_compensation_flashes", - "value": true - }, - { - "name": "disabilityCompensationTempSeparationLocationCodeString", - "value": true - }, - { - "name": "disability_compensation_temp_separation_location_code_string", - "value": true - }, - { - "name": "disabilityCompensationForm4142Supplemental", - "value": true - }, - { - "name": "disability_compensation_form4142_supplemental", - "value": true - }, - { - "name": "disabilityCompensationPifFailNotification", - "value": false - }, - { - "name": "disability_compensation_pif_fail_notification", - "value": false - }, - { - "name": "disabilityCompensationProductionTester", - "value": false - }, - { - "name": "disability_compensation_production_tester", - "value": false - }, - { - "name": "disabilityCompensationFailSubmission", - "value": false - }, - { - "name": "disability_compensation_fail_submission", - "value": false - }, - { - "name": "disabilityCompensationSyncModern0781Flow", - "value": false - }, - { - "name": "disability_compensation_sync_modern_0781_flow", - "value": false - }, - { - "name": "educationReportsCleanup", - "value": true - }, - { - "name": "education_reports_cleanup", - "value": true - }, - { - "name": "enrollmentVerification", - "value": false - }, - { - "name": "enrollment_verification", - "value": false - }, - { - "name": "dischargeWizardFeatures", - "value": true - }, - { - "name": "discharge_wizard_features", - "value": true - }, - { - "name": "disputeDebt", - "value": false - }, - { - "name": "dispute_debt", - "value": false - }, - { - "name": "facilitiesPPMSSuppressAll", - "value": false - }, - { - "name": "facilities_ppms_suppress_all", - "value": false - }, - { - "name": "facilitiesPPMSSuppressPharmacies", - "value": false - }, - { - "name": "facilities_ppms_suppress_pharmacies", - "value": false - }, - { - "name": "facilityLocatorLatLongOnly", - "value": false - }, - { - "name": "facility_locator_lat_long_only", - "value": false - }, - { - "name": "facilityLocatorPredictiveLocationSearch", - "value": false - }, - { - "name": "facility_locator_predictive_location_search", - "value": false - }, - { - "name": "facilityLocatorRailsEngine", - "value": true - }, - { - "name": "facility_locator_rails_engine", - "value": true - }, - { - "name": "facilityLocatorRestoreCommunityCarePagination", - "value": true - }, - { - "name": "facility_locator_restore_community_care_pagination", - "value": true - }, - { - "name": "facilityLocatorShowCommunityCares", - "value": true - }, - { - "name": "facility_locator_show_community_cares", - "value": true - }, - { - "name": "facilityLocatorShowHealthConnectNumber", - "value": true - }, - { - "name": "facility_locator_show_health_connect_number", - "value": true - }, - { - "name": "facilityLocatorShowOperationalHoursSpecialInstructions", - "value": false - }, - { - "name": "facility_locator_show_operational_hours_special_instructions", - "value": false - }, - { - "name": "fileUploadShortWorkflowEnabled", - "value": true - }, - { - "name": "file_upload_short_workflow_enabled", - "value": true - }, - { - "name": "fsr5655ServerSideTransform", - "value": true - }, - { - "name": "fsr_5655_server_side_transform", - "value": true - }, - { - "name": "financialStatusReportDebtsApiModule", - "value": true - }, - { - "name": "financial_status_report_debts_api_module", - "value": true - }, - { - "name": "financialStatusReportExpensesUpdate", - "value": true - }, - { - "name": "financial_status_report_expenses_update", - "value": true - }, - { - "name": "financialStatusReportReviewPageNavigation", - "value": true - }, - { - "name": "financial_status_report_review_page_navigation", - "value": true - }, - { - "name": "findARepresentativeEnabled", - "value": true - }, - { - "name": "find_a_representative_enabled", - "value": true - }, - { - "name": "findARepresentativeEnableApi", - "value": true - }, - { - "name": "find_a_representative_enable_api", - "value": true - }, - { - "name": "findARepresentativeEnableFrontend", - "value": true - }, - { - "name": "find_a_representative_enable_frontend", - "value": true - }, - { - "name": "findARepresentativeFlagResultsEnabled", - "value": false - }, - { - "name": "find_a_representative_flag_results_enabled", - "value": false - }, - { - "name": "findARepresentativeUseAccreditedModels", - "value": false - }, - { - "name": "find_a_representative_use_accredited_models", - "value": false - }, - { - "name": "representativeStatusEnabled", - "value": true - }, - { - "name": "representative_status_enabled", - "value": true - }, - { - "name": "form526IncludeDocumentUploadListInOverflowText", - "value": true - }, - { - "name": "form526_include_document_upload_list_in_overflow_text", - "value": true - }, - { - "name": "appointARepresentativeEnableFrontend", - "value": false - }, - { - "name": "appoint_a_representative_enable_frontend", - "value": false - }, - { - "name": "appointARepresentativeEnablePdf", - "value": true - }, - { - "name": "appoint_a_representative_enable_pdf", - "value": true - }, - { - "name": "form526Legacy", - "value": true - }, - { - "name": "form526_legacy", - "value": true - }, - { - "name": "form526SendDocumentUploadFailureNotification", - "value": false - }, - { - "name": "form526_send_document_upload_failure_notification", - "value": false - }, - { - "name": "form526SendBackupSubmissionPollingFailureEmailNotice", - "value": false - }, - { - "name": "form526_send_backup_submission_polling_failure_email_notice", - "value": false - }, - { - "name": "form526SendBackupSubmissionExhaustionEmailNotice", - "value": false - }, - { - "name": "form526_send_backup_submission_exhaustion_email_notice", - "value": false - }, - { - "name": "form526Send4142FailureNotification", - "value": true - }, - { - "name": "form526_send_4142_failure_notification", - "value": true - }, - { - "name": "form526Send0781FailureNotification", - "value": false - }, - { - "name": "form526_send_0781_failure_notification", - "value": false - }, - { - "name": "form0994ConfirmationEmail", - "value": true - }, - { - "name": "form0994_confirmation_email", - "value": true - }, - { - "name": "form1990ConfirmationEmail", - "value": true - }, - { - "name": "form1990_confirmation_email", - "value": true - }, - { - "name": "form1995ConfirmationEmail", - "value": true - }, - { - "name": "form1995_confirmation_email", - "value": true - }, - { - "name": "form1990eConfirmationEmail", - "value": true - }, - { - "name": "form1990e_confirmation_email", - "value": true - }, - { - "name": "form210966ConfirmationEmail", - "value": true - }, - { - "name": "form21_0966_confirmation_email", - "value": true - }, - { - "name": "form210972ConfirmationEmail", - "value": true - }, - { - "name": "form21_0972_confirmation_email", - "value": true - }, - { - "name": "form2110203ConfirmationEmail", - "value": true - }, - { - "name": "form21_10203_confirmation_email", - "value": true - }, - { - "name": "form2110210ConfirmationEmail", - "value": true - }, - { - "name": "form21_10210_confirmation_email", - "value": true - }, - { - "name": "form2010206ConfirmationEmail", - "value": true - }, - { - "name": "form20_10206_confirmation_email", - "value": true - }, - { - "name": "form2010207ConfirmationEmail", - "value": true - }, - { - "name": "form20_10207_confirmation_email", - "value": true - }, - { - "name": "form210845ConfirmationEmail", - "value": true - }, - { - "name": "form21_0845_confirmation_email", - "value": true - }, - { - "name": "form21p0847ConfirmationEmail", - "value": true - }, - { - "name": "form21p_0847_confirmation_email", - "value": true - }, - { - "name": "form214142ConfirmationEmail", - "value": true - }, - { - "name": "form21_4142_confirmation_email", - "value": true - }, - { - "name": "form2210282ConfirmationEmail", - "value": false - }, - { - "name": "form22_10282_confirmation_email", - "value": false - }, - { - "name": "form264555ConfirmationEmail", - "value": false - }, - { - "name": "form26_4555_confirmation_email", - "value": false - }, - { - "name": "form526RequiredIdentifiersInUserObject", - "value": true - }, - { - "name": "form_526_required_identifiers_in_user_object", - "value": true - }, - { - "name": "form400247ConfirmationEmail", - "value": true - }, - { - "name": "form40_0247_confirmation_email", - "value": true - }, - { - "name": "form4010007ConfirmationEmail", - "value": true - }, - { - "name": "form40_10007_confirmation_email", - "value": true - }, - { - "name": "form1990mebConfirmationEmail", - "value": true - }, - { - "name": "form1990meb_confirmation_email", - "value": true - }, - { - "name": "form1990emebConfirmationEmail", - "value": true - }, - { - "name": "form1990emeb_confirmation_email", - "value": true - }, - { - "name": "form5490ConfirmationEmail", - "value": true - }, - { - "name": "form5490_confirmation_email", - "value": true - }, - { - "name": "form5495ConfirmationEmail", - "value": true - }, - { - "name": "form5495_confirmation_email", - "value": true - }, - { - "name": "simpleFormsEmailConfirmations", - "value": true - }, - { - "name": "simple_forms_email_confirmations", - "value": true - }, - { - "name": "simpleFormsEmailNotifications", - "value": true - }, - { - "name": "simple_forms_email_notifications", - "value": true - }, - { - "name": "simpleFormsNotificationCallbacks", - "value": false - }, - { - "name": "simple_forms_notification_callbacks", - "value": false - }, - { - "name": "form2010206", - "value": true - }, - { - "name": "form2010206", - "value": true - }, - { - "name": "form2010207", - "value": true - }, - { - "name": "form2010207", - "value": true - }, - { - "name": "form210845", - "value": true - }, - { - "name": "form210845", - "value": true - }, - { - "name": "form210966", - "value": false - }, - { - "name": "form210966", - "value": false - }, - { - "name": "form210972", - "value": true - }, - { - "name": "form210972", - "value": true - }, - { - "name": "form214142", - "value": true - }, - { - "name": "form214142", - "value": true - }, - { - "name": "form2110210", - "value": true - }, - { - "name": "form2110210", - "value": true - }, - { - "name": "form21p0847", - "value": false - }, - { - "name": "form21p0847", - "value": false - }, - { - "name": "form264555", - "value": true - }, - { - "name": "form264555", - "value": true - }, - { - "name": "form400247", - "value": true - }, - { - "name": "form400247", - "value": true - }, - { - "name": "form1010d", - "value": true - }, - { - "name": "form1010d", - "value": true - }, - { - "name": "form107959c", - "value": true - }, - { - "name": "form107959c", - "value": true - }, - { - "name": "form107959a", - "value": true - }, - { - "name": "form107959a", - "value": true - }, - { - "name": "form107959f2", - "value": true - }, - { - "name": "form107959f2", - "value": true - }, - { - "name": "formUploadFlow", - "value": true - }, - { - "name": "form_upload_flow", - "value": true - }, - { - "name": "getHelpAskForm", - "value": true - }, - { - "name": "get_help_ask_form", - "value": true - }, - { - "name": "getHelpMessages", - "value": true - }, - { - "name": "get_help_messages", - "value": true - }, - { - "name": "haCpapSuppliesCta", - "value": false - }, - { - "name": "ha_cpap_supplies_cta", - "value": false - }, - { - "name": "inProgressFormCustomExpiration", - "value": true - }, - { - "name": "in_progress_form_custom_expiration", - "value": true - }, - { - "name": "inProgressFormReminder", - "value": false - }, - { - "name": "in_progress_form_reminder", - "value": false - }, - { - "name": "inProgressFormReminderAgeParam", - "value": true - }, - { - "name": "in_progress_form_reminder_age_param", - "value": true - }, - { - "name": "clearStaleInProgressRemindersSent", - "value": false - }, - { - "name": "clear_stale_in_progress_reminders_sent", - "value": false - }, - { - "name": "inProgress1880FormCron", - "value": false - }, - { - "name": "in_progress_1880_form_cron", - "value": false - }, - { - "name": "inProgress1880FormReminder", - "value": true - }, - { - "name": "in_progress_1880_form_reminder", - "value": true - }, - { - "name": "inProgressFormReminder1010ez", - "value": true - }, - { - "name": "in_progress_form_reminder_1010ez", - "value": true - }, - { - "name": "inProgressFormReminder526ez", - "value": false - }, - { - "name": "in_progress_form_reminder_526ez", - "value": false - }, - { - "name": "lettersCheckDiscrepancies", - "value": false - }, - { - "name": "letters_check_discrepancies", - "value": false - }, - { - "name": "lighthouseClaimsApiV2AddPersonProxy", - "value": false - }, - { - "name": "lighthouse_claims_api_v2_add_person_proxy", - "value": false - }, - { - "name": "lighthouseClaimsApiPoaDependentClaimants", - "value": true - }, - { - "name": "lighthouse_claims_api_poa_dependent_claimants", - "value": true - }, - { - "name": "lighthouseClaimsApiV2PoaVANotify", - "value": false - }, - { - "name": "lighthouse_claims_api_v2_poa_va_notify", - "value": false - }, - { - "name": "lighthouseClaimsV2PoaRequestsSkipBGS", - "value": false - }, - { - "name": "lighthouse_claims_v2_poa_requests_skip_bgs", - "value": false - }, - { - "name": "lighthouseClaimsApiPoaUseBd", - "value": true - }, - { - "name": "lighthouse_claims_api_poa_use_bd", - "value": true - }, - { - "name": "lighthouseClaimsApiUseBirlsId", - "value": true - }, - { - "name": "lighthouse_claims_api_use_birls_id", - "value": true - }, - { - "name": "loopPages", - "value": true - }, - { - "name": "loop_pages", - "value": true - }, - { - "name": "showMbsPreneedChangeVA4010007", - "value": false - }, - { - "name": "show_mbs_preneed_change_va_4010007", - "value": false - }, - { - "name": "medicalCopaysSixMoWindow", - "value": false - }, - { - "name": "medical_copays_six_mo_window", - "value": false - }, - { - "name": "medicalCopaysApiKeyChange", - "value": true - }, - { - "name": "medical_copays_api_key_change", - "value": true - }, - { - "name": "medicalCopayNotifications", - "value": true - }, - { - "name": "medical_copay_notifications", - "value": true - }, - { - "name": "mhvAccountCreationApiConsumption", - "value": true - }, - { - "name": "mhv_account_creation_api_consumption", - "value": true - }, - { - "name": "mhvAccountCreationAfterLogin", - "value": true - }, - { - "name": "mhv_account_creation_after_login", - "value": true - }, - { - "name": "mhvAcceleratedDeliveryEnabled", - "value": false - }, - { - "name": "mhv_accelerated_delivery_enabled", - "value": false - }, - { - "name": "mhvAcceleratedDeliveryAllergiesEnabled", - "value": false - }, - { - "name": "mhv_accelerated_delivery_allergies_enabled", - "value": false - }, - { - "name": "mhvAcceleratedDeliveryVitalSignsEnabled", - "value": false - }, - { - "name": "mhv_accelerated_delivery_vital_signs_enabled", - "value": false - }, - { - "name": "mhvVAHealthChatEnabled", - "value": false - }, - { - "name": "mhv_va_health_chat_enabled", - "value": false - }, - { - "name": "mhvLandingPageShowPriorityGroup", - "value": false - }, - { - "name": "mhv_landing_page_show_priority_group", - "value": false - }, - { - "name": "mhvLandingPagePersonalization", - "value": true - }, - { - "name": "mhv_landing_page_personalization", - "value": true - }, - { - "name": "mhvTransitionalMedicalRecordsLandingPage", - "value": true - }, - { - "name": "mhv_transitional_medical_records_landing_page", - "value": true - }, - { - "name": "mhvIntegrationMedicalRecordsToPhase1", - "value": true - }, - { - "name": "mhv_integration_medical_records_to_phase_1", - "value": true - }, - { - "name": "mhvInterstitialEnabled", - "value": false - }, - { - "name": "mhv_interstitial_enabled", - "value": false - }, - { - "name": "mhvSecureMessagingCernerPilot", - "value": false - }, - { - "name": "mhv_secure_messaging_cerner_pilot", - "value": false - }, - { - "name": "mhvSecureMessagingFilterAccordion", - "value": false - }, - { - "name": "mhv_secure_messaging_filter_accordion", - "value": false - }, - { - "name": "mhvSecureMessagingRemoveLefthandNav", - "value": true - }, - { - "name": "mhv_secure_messaging_remove_lefthand_nav", - "value": true - }, - { - "name": "mhvSecureMessagingEditContactList", - "value": true - }, - { - "name": "mhv_secure_messaging_edit_contact_list", - "value": true - }, - { - "name": "mhvSecureMessagingTriageGroupPlainLanguage", - "value": false - }, - { - "name": "mhv_secure_messaging_triage_group_plain_language", - "value": false - }, - { - "name": "mhvSecureMessagingRecipientOptGroups", - "value": false - }, - { - "name": "mhv_secure_messaging_recipient_opt_groups", - "value": false - }, - { - "name": "mhvSecureMessagingRecipientCombobox", - "value": false - }, - { - "name": "mhv_secure_messaging_recipient_combobox", - "value": false - }, - { - "name": "mhvMedicalRecordsAllowTxtDownloads", - "value": true - }, - { - "name": "mhv_medical_records_allow_txt_downloads", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayConditions", - "value": true - }, - { - "name": "mhv_medical_records_display_conditions", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayDomains", - "value": true - }, - { - "name": "mhv_medical_records_display_domains", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayLabsAndTests", - "value": true - }, - { - "name": "mhv_medical_records_display_labs_and_tests", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayNotes", - "value": true - }, - { - "name": "mhv_medical_records_display_notes", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplaySidenav", - "value": true - }, - { - "name": "mhv_medical_records_display_sidenav", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayVaccines", - "value": true - }, - { - "name": "mhv_medical_records_display_vaccines", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplaySettingsPage", - "value": true - }, - { - "name": "mhv_medical_records_display_settings_page", - "value": true - }, - { - "name": "mhvMedicalRecordsDisplayVitals", - "value": true - }, - { - "name": "mhv_medical_records_display_vitals", - "value": true - }, - { - "name": "mhvMedicalRecordsPhrRefreshOnLogin", - "value": false - }, - { - "name": "mhv_medical_records_phr_refresh_on_login", - "value": false - }, - { - "name": "mhvMedicalRecordsRedactFHIRClientLogs", - "value": true - }, - { - "name": "mhv_medical_records_redact_fhir_client_logs", - "value": true - }, - { - "name": "mhvMedicalRecordsToVAGovRelease", - "value": true - }, - { - "name": "mhv_medical_records_to_va_gov_release", - "value": true - }, - { - "name": "mhvMedicalRecordsNewEligibilityCheck", - "value": false - }, - { - "name": "mhv_medical_records_new_eligibility_check", - "value": false - }, - { - "name": "mhvMedicationsToVAGovRelease", - "value": true - }, - { - "name": "mhv_medications_to_va_gov_release", - "value": true - }, - { - "name": "mhvMedicationsDisplayRefillContent", - "value": true - }, - { - "name": "mhv_medications_display_refill_content", - "value": true - }, - { - "name": "mhvMedicationsDisplayDocumentationContent", - "value": true - }, - { - "name": "mhv_medications_display_documentation_content", - "value": true - }, - { - "name": "mhvMedicationsDisplayAllergies", - "value": true - }, - { - "name": "mhv_medications_display_allergies", - "value": true - }, - { - "name": "mhvMedicationsDisplayFilter", - "value": true - }, - { - "name": "mhv_medications_display_filter", - "value": true - }, - { - "name": "mhvMedicationsDisplayGrouping", - "value": true - }, - { - "name": "mhv_medications_display_grouping", - "value": true - }, - { - "name": "mobileAllergyIntoleranceModel", - "value": false - }, - { - "name": "mobile_allergy_intolerance_model", - "value": false - }, - { - "name": "mobileApi", - "value": true - }, - { - "name": "mobile_api", - "value": true - }, - { - "name": "mobileFilterDoc27DecisionLettersOut", - "value": true - }, - { - "name": "mobile_filter_doc_27_decision_letters_out", - "value": true - }, - { - "name": "mobileClaimsLogDecisionLetterSent", - "value": true - }, - { - "name": "mobile_claims_log_decision_letter_sent", - "value": true - }, - { - "name": "multipleAddress1010ez", - "value": true - }, - { - "name": "multiple_address_10_10ez", - "value": true - }, - { - "name": "organicConversionExperiment", - "value": true - }, - { - "name": "organic_conversion_experiment", - "value": true - }, - { - "name": "pcpgTriggerActionNeededEmail", - "value": false - }, - { - "name": "pcpg_trigger_action_needed_email", - "value": false - }, - { - "name": "pensionIncomeAndAssetsClarification", - "value": true - }, - { - "name": "pension_income_and_assets_clarification", - "value": true - }, - { - "name": "pensionMedicalEvidenceClarification", - "value": true - }, - { - "name": "pension_medical_evidence_clarification", - "value": true - }, - { - "name": "pensionErrorEmailNotification", - "value": true - }, - { - "name": "pension_error_email_notification", - "value": true - }, - { - "name": "pensionReceivedEmailNotification", - "value": false - }, - { - "name": "pension_received_email_notification", - "value": false - }, - { - "name": "preEntryCovid19Screener", - "value": false - }, - { - "name": "pre_entry_covid19_screener", - "value": false - }, - { - "name": "profilePPIURejectRequests", - "value": false - }, - { - "name": "profile_ppiu_reject_requests", - "value": false - }, - { - "name": "profileEnhancedMilitaryInfo", - "value": true - }, - { - "name": "profile_enhanced_military_info", - "value": true - }, - { - "name": "profileLighthouseRatingInfo", - "value": true - }, - { - "name": "profile_lighthouse_rating_info", - "value": true - }, - { - "name": "profileUserClaims", - "value": true - }, - { - "name": "profile_user_claims", - "value": true - }, - { - "name": "profileShowMHVNotificationSettingsEmailAppointmentReminders", - "value": false - }, - { - "name": "profile_show_mhv_notification_settings_email_appointment_reminders", - "value": false - }, - { - "name": "profileShowMHVNotificationSettingsEmailRxShipment", - "value": false - }, - { - "name": "profile_show_mhv_notification_settings_email_rx_shipment", - "value": false - }, - { - "name": "profileShowMHVNotificationSettingsNewSecureMessaging", - "value": false - }, - { - "name": "profile_show_mhv_notification_settings_new_secure_messaging", - "value": false - }, - { - "name": "profileShowMHVNotificationSettingsMedicalImages", - "value": false - }, - { - "name": "profile_show_mhv_notification_settings_medical_images", - "value": false - }, - { - "name": "profileShowMilitaryAcademyAttendance", - "value": false - }, - { - "name": "profile_show_military_academy_attendance", - "value": false - }, - { - "name": "profileHideDirectDeposit", - "value": false - }, - { - "name": "profile_hide_direct_deposit", - "value": false - }, - { - "name": "profileShowCredentialRetirementMessaging", - "value": false - }, - { - "name": "profile_show_credential_retirement_messaging", - "value": false - }, - { - "name": "profileShowPaymentsNotificationSetting", - "value": true - }, - { - "name": "profile_show_payments_notification_setting", - "value": true - }, - { - "name": "profileShowNewBenefitOverpaymentDebtNotificationSetting", - "value": false - }, - { - "name": "profile_show_new_benefit_overpayment_debt_notification_setting", - "value": false - }, - { - "name": "profileShowNewHealthCareCopayBillNotificationSetting", - "value": false - }, - { - "name": "profile_show_new_health_care_copay_bill_notification_setting", - "value": false - }, - { - "name": "profileShowPrivacyPolicy", - "value": false - }, - { - "name": "profile_show_privacy_policy", - "value": false - }, - { - "name": "profileShowPronounsAndSexualOrientation", - "value": false - }, - { - "name": "profile_show_pronouns_and_sexual_orientation", - "value": false - }, - { - "name": "profileShowQuickSubmitNotificationSetting", - "value": false - }, - { - "name": "profile_show_quick_submit_notification_setting", - "value": false - }, - { - "name": "profileShowNoValidationKeyAddressAlert", - "value": false - }, - { - "name": "profile_show_no_validation_key_address_alert", - "value": false - }, - { - "name": "profileUseExperimental", - "value": false - }, - { - "name": "profile_use_experimental", - "value": false - }, - { - "name": "profileUseVafsc", - "value": false - }, - { - "name": "profile_use_vafsc", - "value": false - }, - { - "name": "pwEhrCtaUseSlo", - "value": true - }, - { - "name": "pw_ehr_cta_use_slo", - "value": true - }, - { - "name": "myVAExperimental", - "value": false - }, - { - "name": "my_va_experimental", - "value": false - }, - { - "name": "myVAExperimentalFrontend", - "value": true - }, - { - "name": "my_va_experimental_frontend", - "value": true - }, - { - "name": "myVAExperimentalFullstack", - "value": false - }, - { - "name": "my_va_experimental_fullstack", - "value": false - }, - { - "name": "myVAHideNotificationsSection", - "value": true - }, - { - "name": "my_va_hide_notifications_section", - "value": true - }, - { - "name": "myVANotificationComponent", - "value": true - }, - { - "name": "my_va_notification_component", - "value": true - }, - { - "name": "myVANotificationDotIndicator", - "value": true - }, - { - "name": "my_va_notification_dot_indicator", - "value": true - }, - { - "name": "myVAEnableMHVLink", - "value": false - }, - { - "name": "my_va_enable_mhv_link", - "value": false - }, - { - "name": "myVAUpdateErrorsWarnings", - "value": true - }, - { - "name": "my_va_update_errors_warnings", - "value": true - }, - { - "name": "myVALighthouseUploadsReport", - "value": false - }, - { - "name": "my_va_lighthouse_uploads_report", - "value": false - }, - { - "name": "myVAFormSubmissionPdfLink", - "value": true - }, - { - "name": "my_va_form_submission_pdf_link", - "value": true - }, - { - "name": "ratedDisabilitiesDetectDiscrepancies", - "value": false - }, - { - "name": "rated_disabilities_detect_discrepancies", - "value": false - }, - { - "name": "ratedDisabilitiesSortAbTest", - "value": false - }, - { - "name": "rated_disabilities_sort_ab_test", - "value": false - }, - { - "name": "ratedDisabilitiesUseLighthouse", - "value": true - }, - { - "name": "rated_disabilities_use_lighthouse", - "value": true - }, - { - "name": "schemaContractAppointmentsIndex", - "value": true - }, - { - "name": "schema_contract_appointments_index", - "value": true - }, - { - "name": "searchRepresentative", - "value": false - }, - { - "name": "search_representative", - "value": false - }, - { - "name": "searchGovMaintenance", - "value": false - }, - { - "name": "search_gov_maintenance", - "value": false - }, - { - "name": "show526Wizard", - "value": true - }, - { - "name": "show526_wizard", - "value": true - }, - { - "name": "showEduBenefits0994Wizard", - "value": true - }, - { - "name": "show_edu_benefits_0994_wizard", - "value": true - }, - { - "name": "showEduBenefits1990Wizard", - "value": true - }, - { - "name": "show_edu_benefits_1990_wizard", - "value": true - }, - { - "name": "showEduBenefits1990eWizard", - "value": false - }, - { - "name": "show_edu_benefits_1990e_wizard", - "value": false - }, - { - "name": "showEduBenefits1990nWizard", - "value": false - }, - { - "name": "show_edu_benefits_1990n_wizard", - "value": false - }, - { - "name": "showEduBenefits1995Wizard", - "value": true - }, - { - "name": "show_edu_benefits_1995_wizard", - "value": true - }, - { - "name": "showEduBenefits5490Wizard", - "value": true - }, - { - "name": "show_edu_benefits_5490_wizard", - "value": true - }, - { - "name": "showEduBenefits5495Wizard", - "value": true - }, - { - "name": "show_edu_benefits_5495_wizard", - "value": true - }, - { - "name": "showFinancialStatusReport", - "value": true - }, - { - "name": "show_financial_status_report", - "value": true - }, - { - "name": "showFinancialStatusReportWizard", - "value": true - }, - { - "name": "show_financial_status_report_wizard", - "value": true - }, - { - "name": "showFormI18n", - "value": false - }, - { - "name": "show_form_i18n", - "value": false - }, - { - "name": "showDGIDirectDeposit1990ez", - "value": false - }, - { - "name": "show_dgi_direct_deposit_1990EZ", - "value": false - }, - { - "name": "showMeb1990ezMaintenanceAlert", - "value": false - }, - { - "name": "show_meb_1990EZ_maintenance_alert", - "value": false - }, - { - "name": "showMeb1990ezR6MaintenanceMessage", - "value": false - }, - { - "name": "show_meb_1990EZ_R6_maintenance_message", - "value": false - }, - { - "name": "showMeb1990eMaintenanceAlert", - "value": false - }, - { - "name": "show_meb_1990E_maintenance_alert", - "value": false - }, - { - "name": "showMeb1990eR6MaintenanceMessage", - "value": false - }, - { - "name": "show_meb_1990E_R6_maintenance_message", - "value": false - }, - { - "name": "showMebLettersMaintenanceAlert", - "value": false - }, - { - "name": "show_meb_letters_maintenance_alert", - "value": false - }, - { - "name": "showMebEnrollmentVerificationMaintenanceAlert", - "value": false - }, - { - "name": "show_meb_enrollment_verification_maintenance_alert", - "value": false - }, - { - "name": "showMebInternationalAddressPrefill", - "value": true - }, - { - "name": "show_meb_international_address_prefill", - "value": true - }, - { - "name": "showMebServiceHistoryCategorizeDisagreement", - "value": true - }, - { - "name": "show_meb_service_history_categorize_disagreement", - "value": true - }, - { - "name": "showMeb5490MaintenanceAlert", - "value": false - }, - { - "name": "show_meb_5490_maintenance_alert", - "value": false - }, - { - "name": "submissionPdfS3Upload", - "value": true - }, - { - "name": "submission_pdf_s3_upload", - "value": true - }, - { - "name": "meb160630Automation", - "value": true - }, - { - "name": "meb_1606_30_automation", - "value": true - }, - { - "name": "mebExclusionPeriodEnabled", - "value": true - }, - { - "name": "meb_exclusion_period_enabled", - "value": true - }, - { - "name": "mebAutoPopulateRelinquishmentDate", - "value": false - }, - { - "name": "meb_auto_populate_relinquishment_date", - "value": false - }, - { - "name": "dgiRudisillHideBenefitsSelectionStep", - "value": true - }, - { - "name": "dgi_rudisill_hide_benefits_selection_step", - "value": true - }, - { - "name": "showFormsApp", - "value": true - }, - { - "name": "show_forms_app", - "value": true - }, - { - "name": "signInServiceEnabled", - "value": true - }, - { - "name": "sign_in_service_enabled", - "value": true - }, - { - "name": "signInModalV2", - "value": true - }, - { - "name": "sign_in_modal_v2", - "value": true - }, - { - "name": "medicalCopaysZeroDebt", - "value": false - }, - { - "name": "medical_copays_zero_debt", - "value": false - }, - { - "name": "showHealthcareExperienceQuestionnaire", - "value": true - }, - { - "name": "show_healthcare_experience_questionnaire", - "value": true - }, - { - "name": "showNewRefillTrackPrescriptionsPage", - "value": true - }, - { - "name": "show_new_refill_track_prescriptions_page", - "value": true - }, - { - "name": "showNewScheduleViewAppointmentsPage", - "value": true - }, - { - "name": "show_new_schedule_view_appointments_page", - "value": true - }, - { - "name": "showUpdatedFryDeaApp", - "value": false - }, - { - "name": "show_updated_fry_dea_app", - "value": false - }, - { - "name": "spoolTestingError2", - "value": true - }, - { - "name": "spool_testing_error_2", - "value": true - }, - { - "name": "spoolTestingError3", - "value": true - }, - { - "name": "spool_testing_error_3", - "value": true - }, - { - "name": "stemAutomatedDecision", - "value": true - }, - { - "name": "stem_automated_decision", - "value": true - }, - { - "name": "subform89404192", - "value": false - }, - { - "name": "subform_8940_4192", - "value": false - }, - { - "name": "useVeteranModelsForAppoint", - "value": true - }, - { - "name": "use_veteran_models_for_appoint", - "value": true - }, - { - "name": "vaNotifyCustomErrors", - "value": true - }, - { - "name": "va_notify_custom_errors", - "value": true - }, - { - "name": "vaOnlineScheduling", - "value": true - }, - { - "name": "va_online_scheduling", - "value": true - }, - { - "name": "vaOnlineSchedulingBookingExclusion", - "value": false - }, - { - "name": "va_online_scheduling_booking_exclusion", - "value": false - }, - { - "name": "vaOnlineSchedulingCancellationExclusion", - "value": false - }, - { - "name": "va_online_scheduling_cancellation_exclusion", - "value": false - }, - { - "name": "vaOnlineSchedulingCancel", - "value": true - }, - { - "name": "va_online_scheduling_cancel", - "value": true - }, - { - "name": "vaOnlineSchedulingCommunityCare", - "value": true - }, - { - "name": "va_online_scheduling_community_care", - "value": true - }, - { - "name": "vaOnlineSchedulingDirect", - "value": true - }, - { - "name": "va_online_scheduling_direct", - "value": true - }, - { - "name": "vaOnlineSchedulingRequests", - "value": true - }, - { - "name": "va_online_scheduling_requests", - "value": true - }, - { - "name": "vaOnlineSchedulingStaticLandingPage", - "value": true - }, - { - "name": "va_online_scheduling_static_landing_page", - "value": true - }, - { - "name": "vaOnlineSchedulingStsOAuthToken", - "value": true - }, - { - "name": "va_online_scheduling_sts_oauth_token", - "value": true - }, - { - "name": "vaOnlineSchedulingVAOSServiceCCAppointments", - "value": true - }, - { - "name": "va_online_scheduling_vaos_service_cc_appointments", - "value": true - }, - { - "name": "vaOnlineSchedulingVAOSServiceRequests", - "value": true - }, - { - "name": "va_online_scheduling_vaos_service_requests", - "value": true - }, - { - "name": "vaOnlineSchedulingVAOSServiceVAAppointments", - "value": true - }, - { - "name": "va_online_scheduling_vaos_service_va_appointments", - "value": true - }, - { - "name": "vaOnlineSchedulingVAOSV2Next", - "value": true - }, - { - "name": "va_online_scheduling_vaos_v2_next", - "value": true - }, - { - "name": "vaOnlineSchedulingVAOSAlternateRoute", - "value": false - }, - { - "name": "va_online_scheduling_vaos_alternate_route", - "value": false - }, - { - "name": "vaOnlineSchedulingClinicFilter", - "value": true - }, - { - "name": "va_online_scheduling_clinic_filter", - "value": true - }, - { - "name": "vaOnlineSchedulingBreadcrumbUrlUpdate", - "value": true - }, - { - "name": "va_online_scheduling_breadcrumb_url_update", - "value": true - }, - { - "name": "vaOnlineSchedulingUseDsot", - "value": true - }, - { - "name": "va_online_scheduling_use_dsot", - "value": true - }, - { - "name": "vaOnlineSchedulingPocTypeOfCare", - "value": false - }, - { - "name": "va_online_scheduling_poc_type_of_care", - "value": false - }, - { - "name": "vaDependentsV2", - "value": false - }, - { - "name": "va_dependents_v2", - "value": false - }, - { - "name": "vaDependentsNewFieldsForPdf", - "value": false - }, - { - "name": "va_dependents_new_fields_for_pdf", - "value": false - }, - { - "name": "vaDependentsSubmit674", - "value": false - }, - { - "name": "va_dependents_submit674", - "value": false - }, - { - "name": "vaOnlineSchedulingEnableOhCancellations", - "value": false - }, - { - "name": "va_online_scheduling_enable_OH_cancellations", - "value": false - }, - { - "name": "vaOnlineSchedulingEnableOhEligibility", - "value": true - }, - { - "name": "va_online_scheduling_enable_OH_eligibility", - "value": true - }, - { - "name": "vaOnlineSchedulingEnableOhRequests", - "value": false - }, - { - "name": "va_online_scheduling_enable_OH_requests", - "value": false - }, - { - "name": "vaOnlineSchedulingEnableOhSlotsSearch", - "value": false - }, - { - "name": "va_online_scheduling_enable_OH_slots_search", - "value": false - }, - { - "name": "vaOnlineSchedulingDatadogRum", - "value": true - }, - { - "name": "va_online_scheduling_datadog_RUM", - "value": true - }, - { - "name": "vaOnlineSchedulingCCDirectScheduling", - "value": false - }, - { - "name": "va_online_scheduling_cc_direct_scheduling", - "value": false - }, - { - "name": "vaOnlineSchedulingUseVpg", - "value": true - }, - { - "name": "va_online_scheduling_use_vpg", - "value": true - }, - { - "name": "vaOnlineSchedulingRecentLocationsFilter", - "value": false - }, - { - "name": "va_online_scheduling_recent_locations_filter", - "value": false - }, - { - "name": "vaOnlineSchedulingOhDirectSchedule", - "value": false - }, - { - "name": "va_online_scheduling_OH_direct_schedule", - "value": false - }, - { - "name": "vaOnlineSchedulingOhRequest", - "value": false - }, - { - "name": "va_online_scheduling_OH_request", - "value": false - }, - { - "name": "vaOnlineSchedulingRemovePodiatry", - "value": false - }, - { - "name": "va_online_scheduling_remove_podiatry", - "value": false - }, - { - "name": "vaV2PersonService", - "value": false - }, - { - "name": "va_v2_person_service", - "value": false - }, - { - "name": "vaV3ContactInformationService", - "value": true - }, - { - "name": "va_v3_contact_information_service", - "value": true - }, - { - "name": "validateSavedClaimsWithJsonSchemer", - "value": false - }, - { - "name": "validate_saved_claims_with_json_schemer", - "value": false - }, - { - "name": "veteranOnboardingBetaFlow", - "value": false - }, - { - "name": "veteran_onboarding_beta_flow", - "value": false - }, - { - "name": "veteranOnboardingContactInfoFlow", - "value": false - }, - { - "name": "veteran_onboarding_contact_info_flow", - "value": false - }, - { - "name": "veteranOnboardingShowToNewlyOnboarded", - "value": false - }, - { - "name": "veteran_onboarding_show_to_newly_onboarded", - "value": false - }, - { - "name": "veteranOnboardingShowWelcomeMessageToNewUsers", - "value": false - }, - { - "name": "veteran_onboarding_show_welcome_message_to_new_users", - "value": false - }, - { - "name": "veteranStatusCardUseLighthouse", - "value": false - }, - { - "name": "veteran_status_card_use_lighthouse", - "value": false - }, - { - "name": "veteranStatusCardUseLighthouseFrontend", - "value": false - }, - { - "name": "veteran_status_card_use_lighthouse_frontend", - "value": false - }, - { - "name": "vreTriggerActionNeededEmail", - "value": false - }, - { - "name": "vre_trigger_action_needed_email", - "value": false - }, - { - "name": "showEduBenefits1990ezWizard", - "value": true - }, - { - "name": "show_edu_benefits_1990EZ_Wizard", - "value": true - }, - { - "name": "showDashboardNotifications", - "value": true - }, - { - "name": "show_dashboard_notifications", - "value": true - }, - { - "name": "checkVAInboxEnabled", - "value": false - }, - { - "name": "check_va_inbox_enabled", - "value": false - }, - { - "name": "dhpConnectedDevicesFitbit", - "value": false - }, - { - "name": "dhp_connected_devices_fitbit", - "value": false - }, - { - "name": "showExpandableVamcAlert", - "value": true - }, - { - "name": "show_expandable_vamc_alert", - "value": true - }, - { - "name": "paymentHistory", - "value": true - }, - { - "name": "payment_history", - "value": true - }, - { - "name": "cdpPaymentHistoryVBA", - "value": true - }, - { - "name": "cdp_payment_history_vba", - "value": true - }, - { - "name": "showDigitalForm1095b", - "value": true - }, - { - "name": "show_digital_form_1095b", - "value": true - }, - { - "name": "showMebDgi40Features", - "value": true - }, - { - "name": "show_meb_dgi40_features", - "value": true - }, - { - "name": "showMebDgi42Features", - "value": true - }, - { - "name": "show_meb_dgi42_features", - "value": true - }, - { - "name": "showMebEnhancements", - "value": true - }, - { - "name": "show_meb_enhancements", - "value": true - }, - { - "name": "showMebEnhancements06", - "value": true - }, - { - "name": "show_meb_enhancements_06", - "value": true - }, - { - "name": "showMebEnhancements08", - "value": true - }, - { - "name": "show_meb_enhancements_08", - "value": true - }, - { - "name": "showMebEnhancements09", - "value": true - }, - { - "name": "show_meb_enhancements_09", - "value": true - }, - { - "name": "mebGatePersonCriteria", - "value": true - }, - { - "name": "meb_gate_person_criteria", - "value": true - }, - { - "name": "supplyReorderingSleepApneaEnabled", - "value": true - }, - { - "name": "supply_reordering_sleep_apnea_enabled", - "value": true - }, - { - "name": "toeDupContactInfoCall", - "value": true - }, - { - "name": "toe_dup_contact_info_call", - "value": true - }, - { - "name": "toeShortCircuitBGSFailure", - "value": true - }, - { - "name": "toe_short_circuit_bgs_failure", - "value": true - }, - { - "name": "toeHighSchoolInfoChange", - "value": true - }, - { - "name": "toe_high_school_info_change", - "value": true - }, - { - "name": "toeLightHouseDGIDirectDeposit", - "value": false - }, - { - "name": "toe_light_house_dgi_direct_deposit", - "value": false - }, - { - "name": "moveFormBackButton", - "value": false - }, - { - "name": "move_form_back_button", - "value": false - }, - { - "name": "mobileCernerTransition", - "value": false - }, - { - "name": "mobile_cerner_transition", - "value": false - }, - { - "name": "mobileLighthouseLetters", - "value": false - }, - { - "name": "mobile_lighthouse_letters", - "value": false - }, - { - "name": "mobileLighthouseDirectDeposit", - "value": false - }, - { - "name": "mobile_lighthouse_direct_deposit", - "value": false - }, - { - "name": "mobileLighthouseClaims", - "value": true - }, - { - "name": "mobile_lighthouse_claims", - "value": true - }, - { - "name": "mobileLighthouseRequestDecision", - "value": false - }, - { - "name": "mobile_lighthouse_request_decision", - "value": false - }, - { - "name": "mobileLighthouseDocumentUpload", - "value": false - }, - { - "name": "mobile_lighthouse_document_upload", - "value": false - }, - { - "name": "mobileLighthouseDisabilityRatings", - "value": false - }, - { - "name": "mobile_lighthouse_disability_ratings", - "value": false - }, - { - "name": "mobileMilitaryIndicatorLogger", - "value": false - }, - { - "name": "mobile_military_indicator_logger", - "value": false - }, - { - "name": "mobileAppealModel", - "value": true - }, - { - "name": "mobile_appeal_model", - "value": true - }, - { - "name": "mobileV2ContactInfo", - "value": false - }, - { - "name": "mobile_v2_contact_info", - "value": false - }, - { - "name": "mobilePushRegisterLogging", - "value": false - }, - { - "name": "mobile_push_register_logging", - "value": false - }, - { - "name": "form526BackupSubmissionTempKillswitch", - "value": false - }, - { - "name": "form526_backup_submission_temp_killswitch", - "value": false - }, - { - "name": "virtualAgentShowFloatingChatbot", - "value": false - }, - { - "name": "virtual_agent_show_floating_chatbot", - "value": false - }, - { - "name": "disabilityCompensationEmailVeteranOnPolledLighthouseDocFailure", - "value": true - }, - { - "name": "disability_compensation_email_veteran_on_polled_lighthouse_doc_failure", - "value": true - }, - { - "name": "disabilityCompensationLighthouseRatedDisabilitiesProviderForeground", - "value": false - }, - { - "name": "disability_compensation_lighthouse_rated_disabilities_provider_foreground", - "value": false - }, - { - "name": "disabilityCompensationLighthouseRatedDisabilitiesProviderBackground", - "value": false - }, - { - "name": "disability_compensation_lighthouse_rated_disabilities_provider_background", - "value": false - }, - { - "name": "disabilityCompensationLighthouseDocumentServiceProvider", - "value": false - }, - { - "name": "disability_compensation_lighthouse_document_service_provider", - "value": false - }, - { - "name": "disabilityCompensationLighthouseClaimsServiceProvider", - "value": true - }, - { - "name": "disability_compensation_lighthouse_claims_service_provider", - "value": true - }, - { - "name": "disabilityCompensationLighthouseIntentToFileProvider", - "value": true - }, - { - "name": "disability_compensation_lighthouse_intent_to_file_provider", - "value": true - }, - { - "name": "disabilityCompensationLighthousePPIUDirectDepositProvider", - "value": true - }, - { - "name": "disability_compensation_lighthouse_ppiu_direct_deposit_provider", - "value": true - }, - { - "name": "disabilityCompensationPreventSubmissionJob", - "value": false - }, - { - "name": "disability_compensation_prevent_submission_job", - "value": false - }, - { - "name": "disabilityCompensationRemovePciu", - "value": false - }, - { - "name": "disability_compensation_remove_pciu", - "value": false - }, - { - "name": "disabilityCompensationLighthouseBrd", - "value": true - }, - { - "name": "disability_compensation_lighthouse_brd", - "value": true - }, - { - "name": "disabilityCompensationLighthouseGeneratePdf", - "value": false - }, - { - "name": "disability_compensation_lighthouse_generate_pdf", - "value": false - }, - { - "name": "disabilityCompensationUseApiProviderForBddInstructions", - "value": true - }, - { - "name": "disability_compensation_use_api_provider_for_bdd_instructions", - "value": true - }, - { - "name": "disabilityCompensationUploadBddInstructionsToLighthouse", - "value": false - }, - { - "name": "disability_compensation_upload_bdd_instructions_to_lighthouse", - "value": false - }, - { - "name": "disabilityCompensationUseApiProviderFor0781Uploads", - "value": true - }, - { - "name": "disability_compensation_use_api_provider_for_0781_uploads", - "value": true - }, - { - "name": "disabilityCompensationUpload0781ToLighthouse", - "value": true - }, - { - "name": "disability_compensation_upload_0781_to_lighthouse", - "value": true - }, - { - "name": "disabilityCompensationUseApiProviderForSubmitVeteranUpload", - "value": true - }, - { - "name": "disability_compensation_use_api_provider_for_submit_veteran_upload", - "value": true - }, - { - "name": "disabilityCompensationUploadVeteranEvidenceToLighthouse", - "value": true - }, - { - "name": "disability_compensation_upload_veteran_evidence_to_lighthouse", - "value": true - }, - { - "name": "disablityBenefitsBrowserMonitoringEnabled", - "value": false - }, - { - "name": "disablity_benefits_browser_monitoring_enabled", - "value": false - }, - { - "name": "virtualAgentFetchJwtToken", - "value": true - }, - { - "name": "virtual_agent_fetch_jwt_token", - "value": true - }, - { - "name": "virtualAgentLighthouseClaims", - "value": true - }, - { - "name": "virtual_agent_lighthouse_claims", - "value": true - }, - { - "name": "virtualAgentVoice", - "value": true - }, - { - "name": "virtual_agent_voice", - "value": true - }, - { - "name": "notificationCenter", - "value": false - }, - { - "name": "notification_center", - "value": false - }, - { - "name": "nodPart3Update", - "value": true - }, - { - "name": "nod_part3_update", - "value": true - }, - { - "name": "nodBrowserMonitoringEnabled", - "value": true - }, - { - "name": "nod_browser_monitoring_enabled", - "value": true - }, - { - "name": "nodCallbacksEndpoint", - "value": true - }, - { - "name": "nod_callbacks_endpoint", - "value": true - }, - { - "name": "nodConfirmationUpdate", - "value": true - }, - { - "name": "nod_confirmation_update", - "value": true - }, - { - "name": "hlrConfirmationUpdate", - "value": true - }, - { - "name": "hlr_confirmation_update", - "value": true - }, - { - "name": "scConfirmationUpdate", - "value": true - }, - { - "name": "sc_confirmation_update", - "value": true - }, - { - "name": "hlrUpdateedContnet", - "value": true - }, - { - "name": "hlr_updateed_contnet", - "value": true - }, - { - "name": "scNewForm", - "value": true - }, - { - "name": "sc_new_form", - "value": true - }, - { - "name": "hlrBrowserMonitoringEnabled", - "value": true - }, - { - "name": "hlr_browser_monitoring_enabled", - "value": true - }, - { - "name": "scBrowserMonitoringEnabled", - "value": true - }, - { - "name": "sc_browser_monitoring_enabled", - "value": true - }, - { - "name": "virtualAgentEnablePva2Chatbot", - "value": false - }, - { - "name": "virtual_agent_enable_pva2_chatbot", - "value": false - }, - { - "name": "virtualAgentEnableRootBot", - "value": true - }, - { - "name": "virtual_agent_enable_root_bot", - "value": true - }, - { - "name": "virtualAgentComponentTesting", - "value": true - }, - { - "name": "virtual_agent_component_testing", - "value": true - }, - { - "name": "termsOfUse", - "value": true - }, - { - "name": "terms_of_use", - "value": true - }, - { - "name": "burialFormEnabled", - "value": true - }, - { - "name": "burial_form_enabled", - "value": true - }, - { - "name": "burialConfirmationPage", - "value": true - }, - { - "name": "burial_confirmation_page", - "value": true - }, - { - "name": "burialErrorEmailNotification", - "value": false - }, - { - "name": "burial_error_email_notification", - "value": false - }, - { - "name": "burialReceivedEmailNotification", - "value": false - }, - { - "name": "burial_received_email_notification", - "value": false - }, - { - "name": "burialBrowserMonitoringEnabled", - "value": false - }, - { - "name": "burial_browser_monitoring_enabled", - "value": false - }, - { - "name": "pensionFormEnabled", - "value": true - }, - { - "name": "pension_form_enabled", - "value": true - }, - { - "name": "pensionBrowserMonitoringEnabled", - "value": true - }, - { - "name": "pension_browser_monitoring_enabled", - "value": true - }, - { - "name": "pensionMultiplePageResponse", - "value": false - }, - { - "name": "pension_multiple_page_response", - "value": false - }, - { - "name": "pensionIntroductionUpdate", - "value": true - }, - { - "name": "pension_introduction_update", - "value": true - }, - { - "name": "pensionSupportingDocumentsUpdate", - "value": true - }, - { - "name": "pension_supporting_documents_update", - "value": true - }, - { - "name": "pensionDocumentUploadUpdate", - "value": true - }, - { - "name": "pension_document_upload_update", - "value": true - }, - { - "name": "pensionConfirmationUpdate", - "value": true - }, - { - "name": "pension_confirmation_update", - "value": true - }, - { - "name": "incomeAndAssetsFormEnabled", - "value": true - }, - { - "name": "income_and_assets_form_enabled", - "value": true - }, - { - "name": "intentToFileLighthouseEnabled", - "value": true - }, - { - "name": "intent_to_file_lighthouse_enabled", - "value": true - }, - { - "name": "centralMailBenefitsIntakeSubmission", - "value": true - }, - { - "name": "central_mail_benefits_intake_submission", - "value": true - }, - { - "name": "eccBenefitsIntakeSubmission", - "value": false - }, - { - "name": "ecc_benefits_intake_submission", - "value": false - }, - { - "name": "virtualAgentEnableParamErrorDetection", - "value": true - }, - { - "name": "virtual_agent_enable_param_error_detection", - "value": true - }, - { - "name": "virtualAgentEnableMsftPvaTesting", - "value": false - }, - { - "name": "virtual_agent_enable_msft_pva_testing", - "value": false - }, - { - "name": "virtualAgentEnableNluPvaTesting", - "value": false - }, - { - "name": "virtual_agent_enable_nlu_pva_testing", - "value": false - }, - { - "name": "vyeRequestAllowed", - "value": true - }, - { - "name": "vye_request_allowed", - "value": true - }, - { - "name": "sobUpdatedDesign", - "value": true - }, - { - "name": "sob_updated_design", - "value": true - }, - { - "name": "travelPayPowerSwitch", - "value": true - }, - { - "name": "travel_pay_power_switch", - "value": true - }, - { - "name": "travelPayViewClaimDetails", - "value": true - }, - { - "name": "travel_pay_view_claim_details", - "value": true - }, - { - "name": "travelPaySubmitMileageExpense", - "value": false - }, - { - "name": "travel_pay_submit_mileage_expense", - "value": false - }, - { - "name": "yellowRibbonAutomatedDateOnSchoolSearch", - "value": false - }, - { - "name": "yellow_ribbon_automated_date_on_school_search", - "value": false - }, - { - "name": "accreditedRepresentativePortalPilot", - "value": false - }, - { - "name": "accredited_representative_portal_pilot", - "value": false - }, - { - "name": "toggleVyeAddressDirectDepositForms", - "value": true - }, - { - "name": "toggle_vye_address_direct_deposit_forms", - "value": true - }, - { - "name": "veteranReadinessEmploymentToRes", - "value": true - }, - { - "name": "veteran_readiness_employment_to_res", - "value": true - }, - { - "name": "vyeLoginWidget", - "value": true - }, - { - "name": "vye_login_widget", - "value": true - }, - { - "name": "toggleVyeAddressDirectDepositFormsInProfile", - "value": true - }, - { - "name": "toggle_vye_address_direct_deposit_forms_in_profile", - "value": true - }, - { - "name": "toggleVyeApplication", - "value": true - }, - { - "name": "toggle_vye_application", - "value": true - }, - { - "name": "militaryBenefitEstimates", - "value": true - }, - { - "name": "military_benefit_estimates", - "value": true - }, - { - "name": "merge1995And5490", - "value": true - }, - { - "name": "merge_1995_and_5490", - "value": true - }, - { - "name": "mgibVerificationsMaintenance", - "value": false - }, - { - "name": "mgib_verifications_maintenance", - "value": false - }, - { - "name": "searchUseV2Gsa", - "value": true - }, - { - "name": "search_use_v2_gsa", - "value": true - }, - { - "name": "removePciu", - "value": false - }, - { - "name": "remove_pciu", - "value": false - }, - { - "name": "showYellowRibbonTable", - "value": true - }, - { - "name": "show_yellow_ribbon_table", - "value": true - }, - { - "name": "bannerUpdateAlternativeBanners", - "value": true - }, - { - "name": "banner_update_alternative_banners", - "value": true - }, - { - "name": "bannerUseAlternativeBanners", - "value": false - }, - { - "name": "banner_use_alternative_banners", - "value": false - }, - { - "name": "fsrWizard", - "value": false - }, - { - "name": "fsr_wizard", - "value": false - }, - { - "name": "giComparisonToolShowRatings", - "value": true - }, - { - "name": "gi_comparison_tool_show_ratings", - "value": true - }, - { - "name": "giComparisonToolProgramsToggleFlag", - "value": true - }, - { - "name": "gi_comparison_tool_programs_toggle_flag", - "value": true - }, - { - "name": "giComparisonToolLceToggleFlag", - "value": false - }, - { - "name": "gi_comparison_tool_lce_toggle_flag", - "value": false - }, - { - "name": "vaNotifyInProgressMetadata", - "value": true - }, - { - "name": "va_notify_in_progress_metadata", - "value": true - }, - { - "name": "vaNotifyNotificationCreation", - "value": true - }, - { - "name": "va_notify_notification_creation", - "value": true - }, - { - "name": "isDgibEndpoint", - "value": true - }, - { - "name": "is_DGIB_endpoint", - "value": true - }, - { - "name": "lighthouseVeteransHealthDebugLogging", - "value": false - }, - { - "name": "lighthouse_veterans_health_debug_logging", - "value": false - }, - { - "name": "benefitsNonDisabilityCh31V2", - "value": false - }, - { - "name": "benefits_non_disability_ch31_v2", - "value": false - }, - { - "name": "isUpdatedGI", - "value": false - }, - { - "name": "is_updated_gi", - "value": false - }, - { - "name": "showRudisill1995", - "value": true - }, - { - "name": "show_rudisill_1995", - "value": true } - ] + ] } } \ No newline at end of file diff --git a/src/applications/avs/api/mocks/index.js b/src/applications/avs/api/mocks/index.js index 0595238ea1c4..3ddbf12239b7 100644 --- a/src/applications/avs/api/mocks/index.js +++ b/src/applications/avs/api/mocks/index.js @@ -1,21 +1,30 @@ /* eslint-disable camelcase */ -const delay = require('mocker-api/lib/delay'); - -const commonResponses = require('../../../../platform/testing/local-dev-mock-api/common'); +const commonResponses = require('@department-of-veterans-affairs/platform-testing/common'); const featureToggles = require('./feature-toggles'); const avs = require('./avs'); +const delaySingleResponse = (cb, responseDelay = 3) => { + setTimeout(() => { + cb(); + }, responseDelay * 1000); +}; + const responses = { ...commonResponses, - 'GET /v0/feature_toggles': featureToggles.generateFeatureToggles({}), + 'GET /v0/feature_toggles': (req, res) => { + const secondsOfDelay = 1; + const { data } = featureToggles.generateFeatureToggles({}); + return delaySingleResponse(() => res.json({ data }), secondsOfDelay); + }, 'GET /avs/v0/avs/:id': (req, res) => { + const secondsOfDelay = 1; const { data } = avs.data(req.params.id); if (!data.data) { return res.status(404).json(data); } - return res.json(data); + return delaySingleResponse(() => res.json(data), secondsOfDelay); }, }; -module.exports = delay(responses, 1000); +module.exports = responses; diff --git a/src/applications/avs/api/v0.js b/src/applications/avs/api/v0.js deleted file mode 100644 index 463109354609..000000000000 --- a/src/applications/avs/api/v0.js +++ /dev/null @@ -1,18 +0,0 @@ -import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/exports'; -import environment from '@department-of-veterans-affairs/platform-utilities/environment'; - -const apiBasePath = `${environment.API_URL}/avs/v0`; - -/** - * Get an AVS by ID - * @param {String} id - * - * @returns {Object} AVS - */ -export const getAvs = async id => { - return apiRequest(`${apiBasePath}/avs/${id}`, { - headers: { - 'Content-Type': 'application/json', - }, - }); -}; diff --git a/src/applications/avs/app-entry.jsx b/src/applications/avs/app-entry.jsx index 2a7efdc79172..4eb49ca4e7df 100644 --- a/src/applications/avs/app-entry.jsx +++ b/src/applications/avs/app-entry.jsx @@ -1,9 +1,9 @@ import '@department-of-veterans-affairs/platform-polyfills'; import './sass/avs.scss'; -import { startAppFromRouter as startApp } from '@department-of-veterans-affairs/platform-startup/exports'; +import { startAppFromRouterV6 as startApp } from '@department-of-veterans-affairs/platform-startup/exports'; -import routes from './routes'; +import router from './router'; import reducer from './reducers'; import manifest from './manifest.json'; @@ -11,5 +11,5 @@ startApp({ entryName: manifest.entryName, url: manifest.rootUrl, reducer, - routes, + router, }); diff --git a/src/applications/avs/components/ErrorBoundary.jsx b/src/applications/avs/components/ErrorBoundary.jsx index 54de45483fe0..831235d60c7c 100644 --- a/src/applications/avs/components/ErrorBoundary.jsx +++ b/src/applications/avs/components/ErrorBoundary.jsx @@ -34,7 +34,7 @@ class ErrorBoundary extends React.Component {
    ); - return hasError ? : <>{children}; + return hasError || !children ? : <>{children}; } } diff --git a/src/applications/avs/containers/Avs.jsx b/src/applications/avs/containers/Avs.jsx index 754dce3cf10d..80a89fa0816d 100644 --- a/src/applications/avs/containers/Avs.jsx +++ b/src/applications/avs/containers/Avs.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { Suspense } from 'react'; +import { Await, useLoaderData } from 'react-router-dom-v5-compat'; import { connect, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; @@ -8,7 +8,6 @@ import { selectUser } from '@department-of-veterans-affairs/platform-user/select import backendServices from '@department-of-veterans-affairs/platform-user/profile/backendServices'; import { RequiredLoginView } from '@department-of-veterans-affairs/platform-user/RequiredLoginView'; -import { getAvs } from '../api/v0'; import { getFormattedAppointmentDate } from '../utils'; import { useDatadogRum } from '../hooks/useDatadogRum'; @@ -26,6 +25,7 @@ const generateAppointmentHeader = avs => { }; const Avs = props => { + const { id, isLoggedIn } = props; useDatadogRum(); const user = useSelector(selectUser); @@ -38,51 +38,23 @@ const Avs = props => { }, state => state.featureToggles, ); - const { isLoggedIn } = props; - const { id } = useParams(); - const [avs, setAvs] = useState({}); - const [avsLoading, setAvsLoading] = useState(true); - - const [error, setError] = useState(null); - - if (error) { - throw error; - } - - useEffect( - () => { - const fetchAvs = async () => { - try { - const response = await getAvs(id); - - // cf. https://github.com/department-of-veterans-affairs/avs/blob/master/ll-avs-web/src/main/java/gov/va/med/lom/avs/client/model/AvsDataModel.java - setAvs(response.data.attributes); - setAvsLoading(false); - } catch (e) { - setError(e); - } - }; - - if (isLoggedIn && avsLoading && id) { - fetchAvs(); - } - }, - [avs, avsLoading, id, isLoggedIn], - ); + const loader = useLoaderData(); if (avsEnabled === false) { window.location.replace('/'); return null; } - if (isLoggedIn && id && (avsLoading || featureTogglesLoading)) { - return ( - - ); + const loadingIndicator = ( + + ); + + if (isLoggedIn && (!loader.avs || featureTogglesLoading)) { + return loadingIndicator; } if (!id) { @@ -96,50 +68,62 @@ const Avs = props => { user={user} serviceRequired={[backendServices.USER_PROFILE]} > - -

    After-visit summary

    - {avs.meta?.pageHeader && ( -

    - -

    - )} - - - - - - - - - - - - - - - + + + {avs => ( + <> + +

    After-visit summary

    + {avs.meta?.pageHeader && ( +

    + +

    + )} + + + + + + + + + + + + + + + + + )} +
    +
    ); }; -const mapStateToProps = state => ({ - isLoggedIn: state.user.login.currentlyLoggedIn, -}); +const mapStateToProps = state => { + return { + isLoggedIn: state.user.login.currentlyLoggedIn, + }; +}; Avs.propTypes = { + id: PropTypes.string, isLoggedIn: PropTypes.bool, params: PropTypes.object, }; +export { Avs }; export default connect(mapStateToProps)(Avs); diff --git a/src/applications/avs/loaders/avsLoader.js b/src/applications/avs/loaders/avsLoader.js new file mode 100644 index 000000000000..d9eadc6c710c --- /dev/null +++ b/src/applications/avs/loaders/avsLoader.js @@ -0,0 +1,21 @@ +import { defer } from 'react-router-dom-v5-compat'; +import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/api'; +import { environment } from '@department-of-veterans-affairs/platform-utilities/exports'; + +const apiBasePath = `${environment.API_URL}/avs/v0`; + +export async function avsLoader({ params }) { + const { id } = params; + try { + const data = apiRequest(`${apiBasePath}/avs/${id}`).then(response => { + return response.data.attributes; + }); + return defer({ + avs: data, + }); + } catch (e) { + throw new Error('Error loading prescription data'); + } +} + +export default avsLoader; diff --git a/src/applications/avs/router.jsx b/src/applications/avs/router.jsx new file mode 100644 index 000000000000..163bf6d061e6 --- /dev/null +++ b/src/applications/avs/router.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { createBrowserRouter, useParams } from 'react-router-dom-v5-compat'; +import { authenticatedLoader } from '@department-of-veterans-affairs/platform-startup/exports'; +import PageNotFound from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; + +import ErrorBoundary from './components/ErrorBoundary'; +import avsLoader from './loaders/avsLoader'; + +import Avs from './containers/Avs'; + +const ErrorBoundaryWrapper = props => { + const { id } = useParams(); + return ( + + + + ); +}; + +const routes = [ + { + path: '/my-health/medical-records/summaries-and-notes/visit-summary/', + element: , + }, + { + path: '/my-health/medical-records/summaries-and-notes/visit-summary/:id', + loader: authenticatedLoader({ + loader: avsLoader, + defaultReturn: { avs: {} }, + }), + element: , + errorElement: , + }, + { + path: '*', + element: , + }, +]; +const router = createBrowserRouter(routes); + +export { routes, router as default }; diff --git a/src/applications/avs/routes.jsx b/src/applications/avs/routes.jsx deleted file mode 100644 index a6252aeee677..000000000000 --- a/src/applications/avs/routes.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; -import PageNotFound from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; -import ErrorBoundary from './components/ErrorBoundary'; - -import Avs from './containers/Avs'; - -const ErrorBoundaryWrapper = props => ( - - - -); - -const routes = ( - - - - - - - - - - - -); - -export default routes; diff --git a/src/applications/avs/tests/containers/Avs.unit.spec.jsx b/src/applications/avs/tests/containers/Avs.unit.spec.jsx index 329d5cee517f..903e5d0c88da 100644 --- a/src/applications/avs/tests/containers/Avs.unit.spec.jsx +++ b/src/applications/avs/tests/containers/Avs.unit.spec.jsx @@ -1,5 +1,10 @@ import React from 'react'; -import { MemoryRouter, Route } from 'react-router-dom'; +import { + createMemoryRouter, + MemoryRouter, + Route, + RouterProvider, +} from 'react-router-dom-v5-compat'; import { expect } from 'chai'; import { mockApiRequest } from '@department-of-veterans-affairs/platform-testing/helpers'; import { renderInReduxProvider } from '@department-of-veterans-affairs/platform-testing/react-testing-library-helpers'; @@ -7,24 +12,40 @@ import { waitFor } from '@testing-library/react'; import backendServices from '@department-of-veterans-affairs/platform-user/profile/backendServices'; import sinon from 'sinon'; -import Avs from '../../containers/Avs'; +// import { routes } from '../../router'; +import { Avs } from '../../containers/Avs'; import ErrorBoundary from '../../components/ErrorBoundary'; import mockAvs from '../fixtures/9A7AF40B2BC2471EA116891839113252.json'; +import avsLoader from '../../loaders/avsLoader'; -describe('Avs container', () => { +const id = '9A7AF40B2BC2471EA116891839113252'; + +// Temporarily tested with cypress. +// These tests can be re-instated once the node upgrade is complete. +describe.skip('Avs container', () => { let oldLocation; + const sandbox = sinon.createSandbox(); beforeEach(() => { oldLocation = global.window.location; delete global.window.location; global.window.location = { - replace: sinon.spy(), + pathname: '/', + href: { + value: 'foo', + }, + origin: 'http://localhost', + replace: sandbox.stub().callsFake(path => { + window.location.pathname += path; + window.location.search = path.slice(path.indexOf('?')); + }), }; }); afterEach(() => { global.window.location = oldLocation; + sandbox.restore(); }); const initialState = { featureToggles: { @@ -38,47 +59,55 @@ describe('Avs container', () => { }, }, }; - const id = '9A7AF40B2BC2471EA116891839113252'; - const props = {}; - it('user is not logged in', () => { + const props = { id }; + + it('user is not logged in', async () => { // expected behavior is be redirected to the home page with next in the url - renderInReduxProvider( - - - - - , - { - initialState, - }, + const router = createMemoryRouter( + [ + { + path: '/:id', + element: , + loader: avsLoader, + }, + ], + { initialEntries: ['/123'] }, ); + renderInReduxProvider(, { + initialState, + }); + expect(window.location.replace.called).to.be.true; expect(window.location.replace.firstCall.args[0]).to.eq('/'); }); it('feature flags are still loading', () => { - const screen = renderInReduxProvider( - - - - - , - { - initialState: { - ...initialState, - featureToggles: { - loading: true, - }, - user: { - login: { - currentlyLoggedIn: true, - }, + // Temporarily tested with cypress. + const router = createMemoryRouter( + [ + { + path: '/:id', + element: , + loader: avsLoader, + }, + ], + { initialEntries: ['/123'] }, + ); + const screen = renderInReduxProvider(, { + initialState: { + ...initialState, + featureToggles: { + loading: true, + }, + user: { + login: { + currentlyLoggedIn: true, }, }, }, - ); + }); expect(screen.getByTestId('avs-loading-indicator')); }); @@ -163,34 +192,79 @@ describe('Avs container', () => { }); it('API request fails', async () => { + // Temporarily tested with cypress. mockApiRequest({}, false); - const screen = renderInReduxProvider( - - - - - - - , - { - initialState: { - ...initialState, - featureToggles: { - // eslint-disable-next-line camelcase - avs_enabled: true, - loading: false, + const router = createMemoryRouter( + [ + { + path: '/:id', + element: ( + + + + ), + // loader: loader cannot be used until node has been upgraded above v14. + }, + ], + { initialEntries: ['/9A7AF40B2BC2471EA116891839113252'] }, + ); + const screen = renderInReduxProvider(, { + initialState: { + ...initialState, + featureToggles: { + // eslint-disable-next-line camelcase + avs_enabled: true, + loading: false, + }, + user: { + login: { + currentlyLoggedIn: true, }, - user: { - login: { - currentlyLoggedIn: true, - }, - profile: { - services: [backendServices.USER_PROFILE], - }, + profile: { + services: [backendServices.USER_PROFILE], }, }, }, + }); + await waitFor(() => { + expect( + screen.getByText('We can’t access your after-visit summary right now'), + ); + }); + }); + + it('Happy path', async () => { + // Temporarily tested with cypress. + mockApiRequest({}, false); + const router = createMemoryRouter( + [ + { + path: '/:id', + element: , + // loader: () => Promise.resolve(mockAvs), + }, + ], + { initialEntries: ['/9A7AF40B2BC2471EA116891839113252'] }, ); + const screen = renderInReduxProvider(, { + initialState: { + ...initialState, + featureToggles: { + // eslint-disable-next-line camelcase + avs_enabled: true, + loading: false, + }, + avs: mockAvs, + user: { + login: { + currentlyLoggedIn: true, + }, + profile: { + services: [backendServices.USER_PROFILE], + }, + }, + }, + }); await waitFor(() => { expect( screen.getByText('We can’t access your after-visit summary right now'), diff --git a/src/applications/avs/tests/e2e/avs.cypress.spec.js b/src/applications/avs/tests/e2e/avs.cypress.spec.js index 5b5b8e3dd5a9..f645d37d3285 100644 --- a/src/applications/avs/tests/e2e/avs.cypress.spec.js +++ b/src/applications/avs/tests/e2e/avs.cypress.spec.js @@ -1,15 +1,40 @@ import { notFoundHeading } from '@department-of-veterans-affairs/platform-site-wide/PageNotFound'; import manifest from '../../manifest.json'; +import features from '../fixtures/features'; + const avsId = '9A7AF40B2BC2471EA116891839113252'; const testUrl = `${manifest.rootUrl}/${avsId}`; import avsData from '../fixtures/9A7AF40B2BC2471EA116891839113252.json'; -describe('After-visit Summary', () => { - beforeEach(() => { - cy.intercept('GET', `/avs/v0/avs/*`, avsData); +const setup = ({ + featureToggleDelay = 0, + avsDelay = 0, + avsEnabled = true, + apiStatus = 200, + login = true, +}) => { + cy.intercept('/v0/feature_toggles*', features(avsEnabled)).as('features'); + cy.intercept('GET', '/v0/feature_toggles*', { + statusCode: 200, + delay: featureToggleDelay, + body: features(avsEnabled), + }); + cy.intercept('GET', `/avs/v0/avs/*`, { + statusCode: apiStatus, + delay: avsDelay, + body: apiStatus === 200 ? avsData : {}, + }); + + if (login) { cy.login(); + } +}; + +describe('After-visit Summary - Happy Path', () => { + beforeEach(() => { + setup({}); }); it('is accessible', () => { @@ -18,6 +43,8 @@ describe('After-visit Summary', () => { cy.injectAxeThenAxeCheck(); }); + // Add container behavior tests. + it('only the top accordion is open by default', () => { cy.visit(testUrl); cy.get('h1').contains('After-visit summary'); @@ -78,3 +105,49 @@ describe('After-visit Summary', () => { cy.findByRole('heading', { name: notFoundHeading }).should.exist; }); }); + +describe('After-visit Summary - Feature Toggles', () => { + it('Loading indicator is displayed while feature toggles are loading', () => { + setup({ featureToggleDelay: 10000 }); + cy.visit(testUrl); + // find test id avs-loading-indicator + cy.get('[data-testid="avs-loading-indicator"]').should('exist'); + cy.injectAxeThenAxeCheck(); + }); + + it('Users are redirected to the homepage if the feature toggle is not enabled', () => { + setup({ avsEnabled: false }); + cy.visit(testUrl); + cy.injectAxeThenAxeCheck(); + cy.url().should('match', /\/$/); + }); +}); + +describe('After-visit Summary - Authentication', () => { + it('Users are redirected to login if they are not authenticated', () => { + setup({ login: false }); + cy.visit(testUrl); + cy.injectAxeThenAxeCheck(); + const urlPattern = `\\/\\?next=%2Fmy-health%2Fmedical-records%2Fsummaries-and-notes%2Fvisit-summary%2F${avsId}$`; + cy.url().should('match', new RegExp(urlPattern)); + }); +}); + +describe('After-visit Summary - API', () => { + it('Loading indicator is displayed while AVS data is loading', () => { + setup({ avsDelay: 10000 }); + cy.visit(testUrl); + // find test id avs-loading-indicator + cy.get('[data-testid="avs-loading-indicator"]').should('exist'); + cy.injectAxeThenAxeCheck(); + }); + + it('Error is shown on API failure', () => { + setup({ apiStatus: 500 }); + cy.visit(testUrl); + cy.get('body').contains( + 'We can’t access your after-visit summary right now', + ); + cy.injectAxeThenAxeCheck(); + }); +}); diff --git a/src/applications/avs/tests/fixtures/features.js b/src/applications/avs/tests/fixtures/features.js new file mode 100644 index 000000000000..d204df249c98 --- /dev/null +++ b/src/applications/avs/tests/fixtures/features.js @@ -0,0 +1,8 @@ +const features = (enabled = true) => ({ + data: { + type: 'feature_toggles', + features: [{ name: 'avs_enabled', value: enabled }], + }, +}); + +export default features; diff --git a/src/applications/avs/tests/loaders/avsLoader.unit.spec.js b/src/applications/avs/tests/loaders/avsLoader.unit.spec.js new file mode 100644 index 000000000000..20b24744f512 --- /dev/null +++ b/src/applications/avs/tests/loaders/avsLoader.unit.spec.js @@ -0,0 +1,43 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { environment } from '@department-of-veterans-affairs/platform-utilities/exports'; +import * as platformApiUtils from '@department-of-veterans-affairs/platform-utilities/api'; + +import { avsLoader } from '../../loaders/avsLoader'; + +describe('avsLoader', () => { + const params = { id: '123' }; + const mockData = { attributes: { avs: { id: 123, meta: {} } } }; + const apiBasePath = `${environment.API_URL}/avs/v0`; + const sandbox = sinon.createSandbox(); + let apiRequestStub; + + beforeEach(() => { + apiRequestStub = sandbox.stub(platformApiUtils, 'apiRequest'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return deferred data on successful API request', async () => { + apiRequestStub.resolves({ data: mockData }); + + // We can't use await here because of how defer works. + avsLoader({ params }).then(result => { + expect(apiRequestStub.calledWith(`${apiBasePath}/avs/123`)).to.be.true; + expect(result.data.avs).to.equal(mockData.attributes); + }); + }); + + it('should throw an error on failed API request', async () => { + apiRequestStub.rejects(new Error('API Error')); + + try { + await avsLoader({ params }); + } catch (e) { + expect(e.message).to.equal('Error loading prescription data'); + } + }); +}); diff --git a/src/applications/burials-ez/BurialsApp.jsx b/src/applications/burials-ez/BurialsApp.jsx index 45d087dcdddd..7bd6e239041c 100644 --- a/src/applications/burials-ez/BurialsApp.jsx +++ b/src/applications/burials-ez/BurialsApp.jsx @@ -1,20 +1,19 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import RoutedSavableApp from '@department-of-veterans-affairs/platform-forms/RoutedSavableApp'; -import { useFeatureToggle } from 'platform/utilities/feature-toggles'; import { useBrowserMonitoring } from './hooks/useBrowserMonitoring'; import formConfig from './config/form'; import { NoFormPage } from './components/NoFormPage'; -export default function BurialsEntry({ location, children }) { - const { loading: isLoadingFeatures, burialFormEnabled } = useSelector( - state => state?.featureToggles, - ); - - const { useToggleValue, TOGGLE_NAMES } = useFeatureToggle(); - const burialModuleEnabled = useToggleValue(TOGGLE_NAMES.burialModuleEnabled); +export default function BurialsApp({ location, children }) { + const { + loading: isLoadingFeatures, + burialFormEnabled, + burialDocumentUploadUpdate, + burialModuleEnabled, + } = useSelector(state => state?.featureToggles); // Conditional to use new Burial module path in vets-api if enabled formConfig.submitUrl = burialModuleEnabled @@ -23,6 +22,18 @@ export default function BurialsEntry({ location, children }) { useBrowserMonitoring(); + useEffect( + () => { + if (!isLoadingFeatures) { + window.sessionStorage.setItem( + 'showUploadDocuments', + !!burialDocumentUploadUpdate, + ); + } + }, + [isLoadingFeatures, burialDocumentUploadUpdate], + ); + if (isLoadingFeatures) { return ; } @@ -42,7 +53,7 @@ export default function BurialsEntry({ location, children }) { ); } -BurialsEntry.propTypes = { +BurialsApp.propTypes = { children: PropTypes.node.isRequired, location: PropTypes.object.isRequired, }; diff --git a/src/applications/burials-ez/config/chapters/05-additional-information/deathCertificateV2.js b/src/applications/burials-ez/config/chapters/05-additional-information/deathCertificateV2.js new file mode 100644 index 000000000000..ff95a01084a3 --- /dev/null +++ b/src/applications/burials-ez/config/chapters/05-additional-information/deathCertificateV2.js @@ -0,0 +1,55 @@ +import fullSchemaBurials from 'vets-json-schema/dist/21P-530EZ-schema.json'; +import DeathCertificateUploadMessage from '../../../components/DeathCertificateUploadMessage'; +import { generateTitle } from '../../../utils/helpers'; +import { burialUploadUI } from '../../../utils/upload'; + +const { files } = fullSchemaBurials.definitions; + +export default { + uiSchema: { + 'ui:title': generateTitle('Death certificate'), + 'ui:description': DeathCertificateUploadMessage, + deathCertificate: { + ...burialUploadUI('Upload the Veteran’s death certificate', { + fileUploadNetworkErrorMessage: + 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', + fileUploadNetworkErrorAlert: { + header: 'We couldn’t upload your file', + body: [ + 'We’re sorry. There was a problem with our system and we couldn’t upload your file. Try uploading your file again.', + 'If there are still issues uploading your file, you can submit your documents and a PDF version of this form by mail.', + ], + formName: 'Application for Burial Benefits', + formLink: 'https://www.va.gov/find-forms/about-form-21p-530ez/', + formNumber: '21P-530EZ', + showMailingAddress: true, + hideAlertIfLoggedIn: true, + }, + }), + 'ui:required': form => { + const isClaimingBurialAllowance = + form['view:claimedBenefits']?.burialAllowance; + const serviceRequested = + form.burialAllowanceRequested?.service === true; + const locationIsVaMedicalCenter = + form.locationOfDeath?.location === 'vaMedicalCenter'; + return !( + isClaimingBurialAllowance && + serviceRequested && + locationIsVaMedicalCenter + ); + }, + // Empty items object required for confirmation page + items: {}, + }, + }, + schema: { + type: 'object', + properties: { + deathCertificate: { + ...files, + min: 1, + }, + }, + }, +}; diff --git a/src/applications/burials-ez/config/form.js b/src/applications/burials-ez/config/form.js index 5d591687b0d6..8559d2e829f4 100644 --- a/src/applications/burials-ez/config/form.js +++ b/src/applications/burials-ez/config/form.js @@ -41,9 +41,11 @@ import transportationExpenses from './chapters/04-benefits-selection/transportat import supportingDocuments from './chapters/05-additional-information/supportingDocuments'; import fasterClaimProcessing from './chapters/05-additional-information/fasterClaimProcessing'; import deathCertificate from './chapters/05-additional-information/deathCertificate'; +import deathCertificateV2 from './chapters/05-additional-information/deathCertificateV2'; import transportationReceipts from './chapters/05-additional-information/transportationReceipts'; import additionalEvidence from './chapters/05-additional-information/additionalEvidence'; +import { showUploadDocuments } from '../utils/helpers'; import { submit } from './submit'; import manifest from '../manifest.json'; import migrations from '../migrations'; @@ -383,9 +385,20 @@ const formConfig = {

    Death certificate

    ), path: 'additional-information/death-certificate', + depends: () => !showUploadDocuments(), uiSchema: deathCertificate.uiSchema, schema: deathCertificate.schema, }, + deathCertificateV2: { + title: 'Death certificate', + reviewTitle: () => ( +

    Death certificate

    + ), + path: 'additional-information/upload-death-certificate', + depends: () => showUploadDocuments(), + uiSchema: deathCertificateV2.uiSchema, + schema: deathCertificateV2.schema, + }, transportationReceipts: { title: 'Transportation receipts', reviewTitle: () => ( diff --git a/src/applications/burials-ez/utils/helpers.jsx b/src/applications/burials-ez/utils/helpers.jsx index a846915a6d1a..ed6bcdae9581 100644 --- a/src/applications/burials-ez/utils/helpers.jsx +++ b/src/applications/burials-ez/utils/helpers.jsx @@ -52,3 +52,6 @@ export const isProductionEnv = () => { !window.Mocha ); }; + +export const showUploadDocuments = () => + window.sessionStorage.getItem('showUploadDocuments') === 'true'; diff --git a/src/applications/burials-ez/utils/upload.js b/src/applications/burials-ez/utils/upload.js index 9170c4c65625..eb474673bcf4 100644 --- a/src/applications/burials-ez/utils/upload.js +++ b/src/applications/burials-ez/utils/upload.js @@ -19,7 +19,7 @@ function createPayload(file, _formId, password) { return payload; } -export const burialUploadUI = content => { +export const burialUploadUI = (content, options = {}) => { const findAndFocusLastSelect = () => { const lastSelect = [...document.querySelectorAll('select')].slice(-1); if (lastSelect.length) { @@ -54,5 +54,6 @@ export const burialUploadUI = content => { 'ui:disabled': false, 'ui:webComponentField': VaSelectField, }), + ...options, }); }; diff --git a/src/applications/claims-status/components/ClosedClaimMessage.jsx b/src/applications/claims-status/components/ClosedClaimMessage.jsx deleted file mode 100644 index a6f5dae47597..000000000000 --- a/src/applications/claims-status/components/ClosedClaimMessage.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom-v5-compat'; -import { getUnixTime, isAfter, parseISO, startOfDay, subDays } from 'date-fns'; -import { orderBy } from 'lodash'; -import PropTypes from 'prop-types'; -import { VaAlert } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - -import recordEvent from '@department-of-veterans-affairs/platform-monitoring/record-event'; - -import { appealTypes } from '../utils/appeals-v2-helpers'; -import { - buildDateFormatter, - getClaimType, - isClaimOpen, -} from '../utils/helpers'; - -// HELPERS -const isAppeal = claim => appealTypes.includes(claim.type); -const isBenefitsClaimOrAppeal = claim => - claim.type !== 'education_benefits_claims'; - -const getRecentlyClosedClaims = claims => { - return claims - .filter(isBenefitsClaimOrAppeal) - .filter(claim => { - // Check if this is an appeal, if so we want to filter it out - // if it was closed more than 60 days ago - if (isAppeal(claim)) { - const sixtyDaysAgo = startOfDay(subDays(new Date(), 60)); - const events = orderBy( - claim.attributes.events, - [e => getUnixTime(parseISO(e.date))], - ['desc'], - ); - const lastEvent = events[0]; - const lastEventDate = startOfDay(parseISO(lastEvent.date)); - - return !claim.attributes.active && isAfter(lastEventDate, sixtyDaysAgo); - } - - const { closeDate, status } = claim.attributes; - - const isClosed = !isClaimOpen(status, closeDate); - - // If the claim is not an appeal, we want to filter it out - // if it was closed more than 30 days ago - const thirtyDaysAgo = startOfDay(subDays(new Date(), 30)); - const startOfCloseDate = startOfDay(parseISO(closeDate)); - - return isClosed && isAfter(startOfCloseDate, thirtyDaysAgo); - }) - .map(c => { - if (isAppeal(c)) { - const events = orderBy( - c.attributes.events, - [e => getUnixTime(parseISO(e.date))], - ['desc'], - ); - return { - ...c, - attributes: { - ...c.attributes, - claimDate: events[events.length - 1].date, - closeDate: c.attributes.prior_decision_date || events[0].date, - }, - }; - } - - return c; - }); -}; - -const formatDate = buildDateFormatter(); - -const getLinkText = claim => { - const claimType = isAppeal(claim) - ? 'Compensation Appeal' - : getClaimType(claim).toLowerCase(); - return `Your ${claimType} Received ${formatDate(claim.attributes.claimDate)}`; -}; - -export default function ClosedClaimMessage({ claims, onClose }) { - const closedClaims = getRecentlyClosedClaims(claims); - - return ( - closedClaims.length !== 0 && ( - -

    Recently closed:

    -
    - {closedClaims.map(claim => ( -

    - { - recordEvent({ event: 'claims-closed-alert-clicked' }); - }} - > - {getLinkText(claim)} - {' '} - has been closed as of {formatDate(claim.attributes.closeDate)} -

    - ))} -
    -
    - ) - ); -} - -ClosedClaimMessage.propTypes = { - claims: PropTypes.array, - onClose: PropTypes.func, -}; diff --git a/src/applications/claims-status/containers/YourClaimsPageV2.jsx b/src/applications/claims-status/containers/YourClaimsPageV2.jsx index 854d83b366c5..d54391888eb9 100644 --- a/src/applications/claims-status/containers/YourClaimsPageV2.jsx +++ b/src/applications/claims-status/containers/YourClaimsPageV2.jsx @@ -22,7 +22,6 @@ import ClaimsAppealsUnavailable from '../components/ClaimsAppealsUnavailable'; import ClaimsBreadcrumbs from '../components/ClaimsBreadcrumbs'; import ClaimsListItem from '../components/ClaimsListItem'; import ClaimsUnavailable from '../components/ClaimsUnavailable'; -import ClosedClaimMessage from '../components/ClosedClaimMessage'; import FeaturesWarning from '../components/FeaturesWarning'; import NoClaims from '../components/NoClaims'; import StemClaimListItem from '../components/StemClaimListItem'; @@ -47,15 +46,9 @@ class YourClaimsPageV2 extends React.Component { constructor(props) { super(props); this.changePage = this.changePage.bind(this); - this.hide30DayNotice = this.hide30DayNotice.bind(this); - - if (!sessionStorage.getItem('show30DayNotice')) { - sessionStorage.setItem('show30DayNotice', true); - } this.state = { page: YourClaimsPageV2.getPageFromURL(props), - show30DayNotice: sessionStorage.getItem('show30DayNotice') === 'true', }; } @@ -124,11 +117,6 @@ class YourClaimsPageV2 extends React.Component { setPageFocus('#pagination-info'); } - hide30DayNotice() { - this.setState({ show30DayNotice: false }); - sessionStorage.setItem('show30DayNotice', false); - } - renderListItem(claim) { if (appealTypes.includes(claim.type)) { const { fullName } = this.props; @@ -221,12 +209,6 @@ class YourClaimsPageV2 extends React.Component { content = ( <> - {this.state.show30DayNotice && ( - - )} {pageInfo}
    {atLeastOneRequestLoading && ( diff --git a/src/applications/claims-status/tests/components/ClosedClaimMessage.unit.spec.jsx b/src/applications/claims-status/tests/components/ClosedClaimMessage.unit.spec.jsx deleted file mode 100644 index cab3eeb05cea..000000000000 --- a/src/applications/claims-status/tests/components/ClosedClaimMessage.unit.spec.jsx +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { expect } from 'chai'; -import { format, formatISO, subDays } from 'date-fns'; -import { fireEvent } from '@testing-library/dom'; -import sinon from 'sinon'; - -import { $ } from '@department-of-veterans-affairs/platform-forms-system/ui'; -import * as recordEventModule from '~/platform/monitoring/record-event'; - -import ClosedClaimMessage from '../../components/ClosedClaimMessage'; -import { DATE_FORMATS } from '../../constants'; -import { renderWithRouter } from '../utils'; - -// HELPERS -const formatString = DATE_FORMATS.LONG_DATE; -const getISOString = date => formatISO(date, { representation: 'date' }); - -describe('', () => { - context('Appeals', () => { - it('should render closed appeals within 60 days', () => { - const closeDate = subDays(new Date(), 59); - - const claims = [ - { - id: 1, - type: 'appeal', - attributes: { - active: false, - events: [ - { - type: 'claim_decision', - date: '2020-01-01', - }, - { - type: 'bva_decision', - date: getISOString(closeDate), - }, - ], - }, - }, - ]; - - const screen = renderWithRouter(); - - // Check that the component was rendered - expect(screen.getByText('Recently closed:')).to.exist; - - // Check that the dates match up with what we would expect - expect( - screen.getByText('Your Compensation Appeal Received January 1, 2020'), - ).to.exist; - - const closeDateText = format(closeDate, formatString); - expect(screen.getByText(`has been closed as of ${closeDateText}`)).to - .exist; - }); - - it('should not render closed claims at 60 days', () => { - const closeDate = subDays(new Date(), 60); - const claims = [ - { - id: 2, - type: 'appeal', - attributes: { - active: false, - events: [ - { - type: 'claim_decision', - date: '2021-05-10', - }, - { - type: 'bva_decision', - date: getISOString(closeDate), - }, - ], - }, - }, - ]; - - const screen = render(); - - // Check that the component was rendered - expect(screen.queryByText('Recently closed:')).to.not.exist; - }); - - it('when click Compensation Appeal link, should call record event', () => { - const recordEventStub = sinon.stub(recordEventModule, 'default'); - const closeDate = subDays(new Date(), 59); - const claims = [ - { - id: 1, - type: 'appeal', - attributes: { - active: false, - events: [ - { - type: 'claim_decision', - date: '2020-01-01', - }, - { - type: 'bva_decision', - date: getISOString(closeDate), - }, - ], - }, - }, - ]; - const { container } = renderWithRouter( - , - ); - const compAppealLink = $('a', container); - fireEvent.click(compAppealLink); - expect( - recordEventStub.calledWith({ - event: 'claims-closed-alert-clicked', - }), - ).to.be.true; - recordEventStub.restore(); - }); - }); - - context('Benefits claims', () => { - it('should render closed claims within 30 days', () => { - const closeDate = subDays(new Date(), 29); - const claims = [ - { - id: 1, - type: 'claim', - attributes: { - claimDate: '2023-01-01', - closeDate: getISOString(closeDate), - status: 'COMPLETE', - }, - }, - ]; - - const screen = renderWithRouter(); - - // Check that the component rendered - expect(screen.getByText('Recently closed:')).to.exist; - - // Check that the dates match up with what we would expect - expect( - screen.getByText( - 'Your disability compensation Received January 1, 2023', - ), - ).to.exist; - - const closeDateText = format(closeDate, formatString); - expect(screen.getByText(`has been closed as of ${closeDateText}`)).to - .exist; - }); - - it('should not render closed claims at 30 days', () => { - const closeDate = subDays(new Date(), 30); - const claims = [ - { - id: 1, - type: 'claim', - attributes: { - claimDate: '2023-01-01', - closeDate: getISOString(closeDate), - status: 'COMPLETE', - }, - }, - ]; - - const screen = render(); - expect(screen.queryByText('Recently closed:')).to.not.exist; - }); - - it('should render nothing when no closed claims', () => { - const claims = [ - { - id: 1, - type: 'claim', - attributes: { - claimDate: '2023-01-01', - closeDate: null, - status: 'EVIDENCE_GATHERING_REVIEW_DECISION', - }, - }, - ]; - - const screen = render(); - expect(screen.queryByText('Recently closed:')).to.not.exist; - }); - }); -}); diff --git a/src/applications/claims-status/tests/components/YourClaimsPageV2.unit.spec.jsx b/src/applications/claims-status/tests/components/YourClaimsPageV2.unit.spec.jsx index bd0358577cef..61f5a4572e17 100644 --- a/src/applications/claims-status/tests/components/YourClaimsPageV2.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/YourClaimsPageV2.unit.spec.jsx @@ -146,12 +146,6 @@ describe('', () => { wrapper.unmount(); }); - it('should render a closed claim message if show30DayNotice is true', () => { - const wrapper = shallow(); - expect(wrapper.find('ClosedClaimMessage').length).to.equal(1); - wrapper.unmount(); - }); - it('should render Pagination', () => { const props = { ...defaultProps, @@ -228,20 +222,6 @@ describe('', () => { wrapper.unmount(); }); - it('should render 30 day notice', () => { - const props = set('show30DayNotice', true, defaultProps); - const wrapper = shallow(); - expect(wrapper.find('ClosedClaimMessage').length).to.equal(1); - wrapper.unmount(); - }); - - it('should not render 30 day notice', () => { - sessionStorage.setItem('show30DayNotice', false); - const wrapper = shallow(); - expect(wrapper.find('ClosedClaimMessage').length).to.equal(0); - wrapper.unmount(); - }); - it('should return updated state when page changes in getDerivedStateFromProps', () => { const nextProps = { location: { diff --git a/src/applications/combined-debt-portal/combined/components/ComboAlerts.jsx b/src/applications/combined-debt-portal/combined/components/ComboAlerts.jsx index aaa4b2a63ebc..cf35ee277bbc 100644 --- a/src/applications/combined-debt-portal/combined/components/ComboAlerts.jsx +++ b/src/applications/combined-debt-portal/combined/components/ComboAlerts.jsx @@ -68,7 +68,7 @@ ComboAlert.Zero = () => {
    • For benefit debts, call the Debt Management Center - (DMC) at ( + (DMC) at ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.
    • diff --git a/src/applications/combined-debt-portal/combined/components/MCPAlerts.jsx b/src/applications/combined-debt-portal/combined/components/MCPAlerts.jsx index b16f3a3be116..44e96b50520f 100644 --- a/src/applications/combined-debt-portal/combined/components/MCPAlerts.jsx +++ b/src/applications/combined-debt-portal/combined/components/MCPAlerts.jsx @@ -128,7 +128,7 @@ Alert.NoHealthcare = () => (

      If you think this is incorrect, call our toll-free hotline at{' '} - + , Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.

      diff --git a/src/applications/combined-debt-portal/combined/components/OtherVADebts.jsx b/src/applications/combined-debt-portal/combined/components/OtherVADebts.jsx index 59e315901022..10fb740485c0 100644 --- a/src/applications/combined-debt-portal/combined/components/OtherVADebts.jsx +++ b/src/applications/combined-debt-portal/combined/components/OtherVADebts.jsx @@ -6,7 +6,7 @@ const OtherVADebts = ({ module, subHeading }) => { return ( <>

      @@ -16,37 +16,37 @@ const OtherVADebts = ({ module, subHeading }) => { Our records show you have{' '} {module === APP_TYPES.DEBT && ( - VA benefit debt. You can{' '} - - check the details of your current debt - - - , find out how to pay your debt, and learn how to request - financial assistance. - + VA benefit debt. You can check the details of your current debt, + find out how to pay your debt, and learn how to request financial + assistance. )}{' '} {module === APP_TYPES.COPAY && ( - a VA health care copay bill. You can{' '} - - check the details of your copay balance - - - , find out how to pay your balance, and learn how to request - financial assistance. - + VA health care copay bills. You can check the details of your copay + balance, find out how to pay your balance, and learn how to request + financial assistance. )}

      - - + {module === APP_TYPES.DEBT && ( + + )} + {module === APP_TYPES.COPAY && ( + + )} ); }; diff --git a/src/applications/combined-debt-portal/combined/components/ZeroBalanceCard.jsx b/src/applications/combined-debt-portal/combined/components/ZeroBalanceCard.jsx index b69877df15d6..6d392e4392e6 100644 --- a/src/applications/combined-debt-portal/combined/components/ZeroBalanceCard.jsx +++ b/src/applications/combined-debt-portal/combined/components/ZeroBalanceCard.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import { APP_TYPES } from '../utils/helpers'; const ZeroBalanceCard = ({ appType }) => { @@ -12,7 +13,7 @@ const ZeroBalanceCard = ({ appType }) => { appType === APP_TYPES.DEBT ? (

      If you think this is incorrect, call the Debt Management Center (DMC) at{' '} - ( + ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

      diff --git a/src/applications/combined-debt-portal/combined/containers/OverviewPage.jsx b/src/applications/combined-debt-portal/combined/containers/OverviewPage.jsx index b071bdded5a4..e572d45c207a 100644 --- a/src/applications/combined-debt-portal/combined/containers/OverviewPage.jsx +++ b/src/applications/combined-debt-portal/combined/containers/OverviewPage.jsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { VaBreadcrumbs } from '@department-of-veterans-affairs/web-components/react-bindings'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import Balances from '../components/Balances'; import ComboAlerts from '../components/ComboAlerts'; import { ALERT_TYPES, setPageFocus } from '../utils/helpers'; @@ -74,7 +75,7 @@ const OverviewPage = () => {

      Questions about benefit debt

      Call the Debt Management Center (DMC) at{' '} - ( + ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

      diff --git a/src/applications/combined-debt-portal/combined/utils/alert-messages.jsx b/src/applications/combined-debt-portal/combined/utils/alert-messages.jsx index 7c1b096d9505..46c6869566fc 100644 --- a/src/applications/combined-debt-portal/combined/utils/alert-messages.jsx +++ b/src/applications/combined-debt-portal/combined/utils/alert-messages.jsx @@ -22,7 +22,7 @@ const alertMessage = (alertType, appType) => {
      • For benefit debts, call the Debt Management - Center (DMC) at ( + Center (DMC) at ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.
      • @@ -96,7 +96,7 @@ const alertMessage = (alertType, appType) => {

        Our records show you don’t have any current debt related to VA benefits. If you think this is incorrect, call the Debt Management - Center (DMC) at ( + Center (DMC) at ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

        diff --git a/src/applications/combined-debt-portal/debt-letters/components/Alerts.jsx b/src/applications/combined-debt-portal/debt-letters/components/Alerts.jsx index 54195857fb60..6db6f7480fa8 100644 --- a/src/applications/combined-debt-portal/debt-letters/components/Alerts.jsx +++ b/src/applications/combined-debt-portal/debt-letters/components/Alerts.jsx @@ -53,7 +53,7 @@ export const DowntimeMessage = () => { If you need help resolving a debt, or you would like to get information about a debt that has been resolved, call the Debt Management Center at{' '} - . + .

        ); @@ -91,7 +91,7 @@ export const ErrorAlert = () => (

        What you can do

        You can check back later or call the Debt Management Center at{' '} - to find out more information about + to find out more information about how to resolve your debt.

        (

        What you can do

        If you need to access debt letters that were mailed to you, call the Debt - Management Center at . + Management Center at .

        (

        Our records show you don’t have any debt letters related to VA benefits. If you think this is an error, please contact the Debt Management Center - at . + at .

        If you have VA health care copay debt, go to our diff --git a/src/applications/combined-debt-portal/debt-letters/components/HowDoIPay.jsx b/src/applications/combined-debt-portal/debt-letters/components/HowDoIPay.jsx index 92b5049002f5..5ee2a119e458 100644 --- a/src/applications/combined-debt-portal/debt-letters/components/HowDoIPay.jsx +++ b/src/applications/combined-debt-portal/debt-letters/components/HowDoIPay.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import { deductionCodes } from '../const/deduction-codes'; export const getDeductionDescription = code => { @@ -75,9 +76,9 @@ const HowDoIPay = ({ userData }) => {

        Pay by phone

        - Call us at ( - from overseas) ( - + Call us at ( + from + overseas) ( ). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

        diff --git a/src/applications/combined-debt-portal/debt-letters/components/NeedHelp.jsx b/src/applications/combined-debt-portal/debt-letters/components/NeedHelp.jsx index 92f20ba14d93..f90edbad73da 100644 --- a/src/applications/combined-debt-portal/debt-letters/components/NeedHelp.jsx +++ b/src/applications/combined-debt-portal/debt-letters/components/NeedHelp.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; const NeedHelp = () => ( <> @@ -47,11 +48,11 @@ const NeedHelp = () => ( If you think a debt was created in error, you can dispute it. Get information about disputing a debt by contacting us online through{' '} Ask VA or calling the Debt - Management Center at ( + Management Center at ( ). For international callers, use{' '} - . We’re here Monday through - Friday, 7:30 a.m. to 7:00 p.m. ET. + . We’re + here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

        @@ -61,11 +62,11 @@ const NeedHelp = () => (

        If you have any questions about your benefit overpayment. Contact us online through Ask VA or call the - Debt Management Center at ( + Debt Management Center at ( ). For international callers, use{' '} - . We’re here Monday through - Friday, 7:30 a.m. to 7:00 p.m. ET. + . We’re + here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

    diff --git a/src/applications/combined-debt-portal/debt-letters/const/deduction-codes/index.js b/src/applications/combined-debt-portal/debt-letters/const/deduction-codes/index.js index b148f10b8f2f..81df8b6c6035 100644 --- a/src/applications/combined-debt-portal/debt-letters/const/deduction-codes/index.js +++ b/src/applications/combined-debt-portal/debt-letters/const/deduction-codes/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; export const deductionCodes = Object.freeze({ '30': 'Disability compensation and pension debt', @@ -13,11 +14,11 @@ export const deductionCodes = Object.freeze({ const ContactInfo = () => (

    If you have questions about your VA debt, call us at{' '} - ( + ( ). If you’re outside the U.S., call{' '} - . We’re here Monday - through Friday, 7:30 a.m. to 7:00 p.m. ET. + . We’re here + Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

    ); diff --git a/src/applications/combined-debt-portal/debt-letters/const/diary-codes/debtSummaryCardContent.js b/src/applications/combined-debt-portal/debt-letters/const/diary-codes/debtSummaryCardContent.js index 597449830512..1ebcecafd6dd 100644 --- a/src/applications/combined-debt-portal/debt-letters/const/diary-codes/debtSummaryCardContent.js +++ b/src/applications/combined-debt-portal/debt-letters/const/diary-codes/debtSummaryCardContent.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import { endDate } from '../../utils/helpers'; const WarningIcon = () => ( @@ -164,7 +165,7 @@ export const debtSummaryText = (diaryCode, dateOfLetter, balance) => { return ( Contact the U.S. Department of the Treasury’s Debt Management Services - at , 8:30 a.m. to 6:30 p.m. ET. + at , 8:30 a.m. to 6:30 p.m. ET. to pay this debt. ); diff --git a/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersDownload.jsx b/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersDownload.jsx index 3ba39e2a89f5..efbb2ed7c6a1 100644 --- a/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersDownload.jsx +++ b/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersDownload.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import { VaBreadcrumbs } from '@department-of-veterans-affairs/web-components/react-bindings'; import { setPageFocus, @@ -76,11 +77,11 @@ const DebtLettersDownload = () => { If you’ve received a letter about a VA debt that isn’t listed here, call us at{' '} - + {' '} (or{' '} - + {' '} from overseas). You can also call us to get information about your resolved debts. diff --git a/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersSummary.jsx b/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersSummary.jsx index 35f572681f21..0cdfe8753a28 100644 --- a/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersSummary.jsx +++ b/src/applications/combined-debt-portal/debt-letters/containers/DebtLettersSummary.jsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import { VaBreadcrumbs } from '@department-of-veterans-affairs/web-components/react-bindings'; import { setPageFocus, @@ -53,7 +54,7 @@ const renderOtherVA = (mcpLength, mcpError) => { if (mcpError) { return ( <> -

    Your other VA bills

    +

    Your VA copay bills

    {alertInfo.header} @@ -146,11 +147,11 @@ const DebtLettersSummary = () => { think your debt was created in an error, you can dispute it. Contact us online through Ask VA{' '} or call the Debt Management Center at{' '} - ( + ( ). For international callers, use{' '} - . We’re here Monday through - Friday, 7:30 a.m. to 7:00 p.m. ET. + . + We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

    diff --git a/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.js b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.js new file mode 100644 index 000000000000..f99e8251007f --- /dev/null +++ b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.js @@ -0,0 +1,126 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mockApiRequest } from 'platform/testing/unit/helpers'; +import { + fetchDebtLetters, + DEBTS_FETCH_INITIATED, + DEBTS_FETCH_SUCCESS, + DEBTS_FETCH_FAILURE, + setActiveDebt, + DEBTS_SET_ACTIVE_DEBT, +} from '../../combined/actions/debts'; +import { + getStatements, + MCP_STATEMENTS_FETCH_INIT, + MCP_STATEMENTS_FETCH_SUCCESS, +} from '../../combined/actions/copays'; + +describe('Debt Portal Actions', () => { + describe('fetchDebtLetters', () => { + let dispatch; + + beforeEach(() => { + dispatch = sinon.spy(); + }); + + it('should handle successful response', () => { + const mockResponse = { + debts: [ + { + deductionCode: '30', + currentAr: 100, + debtHistory: [ + { + date: '2024-01-01', + }, + ], + }, + ], + hasDependentDebts: false, + }; + + mockApiRequest(mockResponse); + + return fetchDebtLetters(dispatch, true).then(() => { + const allCalls = dispatch.getCalls().map(call => call.args[0]); + + // First call: DEBTS_FETCH_INITIATED + expect(allCalls[0]).to.eql({ + type: DEBTS_FETCH_INITIATED, + }); + + // Last call: DEBTS_FETCH_SUCCESS with data + expect(allCalls[allCalls.length - 1]).to.eql({ + type: DEBTS_FETCH_SUCCESS, + debts: mockResponse.debts, + hasDependentDebts: mockResponse.hasDependentDebts, + }); + }); + }); + + it('should handle error response', () => { + // Provide null response with `false` to indicate failure + mockApiRequest(null, false); + + return fetchDebtLetters(dispatch, true).then(() => { + const allCalls = dispatch.getCalls().map(call => call.args[0]); + + expect(allCalls[0]).to.eql({ + type: DEBTS_FETCH_INITIATED, + }); + // Next call: DEBTS_FETCH_FAILURE + expect(allCalls[1]).to.contain({ + type: DEBTS_FETCH_FAILURE, + }); + }); + }); + }); + + describe('setActiveDebt', () => { + it('should return the correct action', () => { + const debt = { id: 1, amount: 100 }; + const action = setActiveDebt(debt); + expect(action).to.eql({ + type: DEBTS_SET_ACTIVE_DEBT, + debt, + }); + }); + }); +}); + +describe('Copay Actions', () => { + let dispatch; + + beforeEach(() => { + dispatch = sinon.spy(); + }); + + it('should handle successful copay statements fetch', () => { + const mockResponse = { + data: [ + { + station: { + facilitYNum: '123', + city: 'Washington', + facilityName: '123', + }, + }, + ], + }; + mockApiRequest(mockResponse); + return getStatements(dispatch).then(() => { + const allCalls = dispatch.getCalls().map(call => call.args[0]); + + // First call to MCP_STATEMENTS_FETCH_INIT + expect(allCalls[0]).to.eql({ + type: MCP_STATEMENTS_FETCH_INIT, + }); + + // Last call to MCP_STATEMENTS_FETCH_SUCCESS with response data + expect(allCalls[allCalls.length - 1]).to.eql({ + type: MCP_STATEMENTS_FETCH_SUCCESS, + response: mockResponse.data, + }); + }); + }); +}); diff --git a/src/applications/disability-benefits/686c-674/containers/ConfirmationPage.jsx b/src/applications/disability-benefits/686c-674/containers/ConfirmationPage.jsx index 850fddf98ebf..d1c4d4bda1dc 100644 --- a/src/applications/disability-benefits/686c-674/containers/ConfirmationPage.jsx +++ b/src/applications/disability-benefits/686c-674/containers/ConfirmationPage.jsx @@ -13,7 +13,8 @@ export default function ConfirmationPage() { const form = useSelector(state => state?.form); const { submission, data } = form; - const formSubmissionId = submission?.response?.formSubmissionId; + const response = submission?.response ?? {}; + const confirmationNumber = response?.attributes?.confirmationNumber; const veteranFirstName = data?.veteranInformation?.fullName?.first || ''; const veteranLastName = data?.veteranInformation?.fullName?.last || ''; @@ -29,11 +30,11 @@ export default function ConfirmationPage() { return ( <> -

    Form submission started on August 15, 2024

    +

    Form submission started on {dateSubmitted}

    Your submission is in progress.

    It can take up to 10 days for us to receive your form. Your - confirmation number is {formSubmissionId}. + confirmation number is {confirmationNumber}.

    = this.props.longWaitTime) { - this.setState({ longWait: true }); - } setTimeout(this.poll, waitTime); } }) - .catch(response => { + .catch(() => { // Don't process the request once it comes back if the component is no longer mounted if (!this.__isMounted) { return; @@ -85,8 +77,6 @@ export class ConfirmationPoll extends React.Component { } else { this.setState({ submissionStatus: submissionStatuses.apiFailure, - // NOTE: I don't know that it'll always take this shape. - failureCode: get('errors[0].status', response), }); } }); @@ -95,21 +85,11 @@ export class ConfirmationPoll extends React.Component { render() { const { submissionStatus, claimId } = this.state; if (submissionStatus === submissionStatuses.pending) { - setTimeout(() => focusElement('.loading-indicator-container')); return ( - - - - - - {pendingMessage(this.state.longWait)} - - + ); } @@ -167,7 +147,6 @@ ConfirmationPoll.propTypes = { }), isSubmittingBDD: PropTypes.bool, jobId: PropTypes.string, - longWaitTime: PropTypes.number, pollRate: PropTypes.number, route: PropTypes.shape({ formConfig: PropTypes.object, @@ -179,7 +158,6 @@ ConfirmationPoll.propTypes = { ConfirmationPoll.defaultProps = { pollRate: 5000, delayFailure: 6000, // larger than pollRate - longWaitTime: 30000, }; export default connect(mapStateToProps)(ConfirmationPoll); diff --git a/src/applications/disability-benefits/all-claims/config/form0781/index.js b/src/applications/disability-benefits/all-claims/config/form0781/index.js index 81c0d60137af..1e5fc48dcd51 100644 --- a/src/applications/disability-benefits/all-claims/config/form0781/index.js +++ b/src/applications/disability-benefits/all-claims/config/form0781/index.js @@ -50,12 +50,6 @@ export const form0781PagesConfig = { uiSchema: eventType.uiSchema, schema: eventType.schema, }, - additionalInformationPage: { - path: 'additional-forms/mental-health-statement/additional-information', - depends: formData => isCompletingForm0781(formData), - uiSchema: additionalInformationPage.uiSchema, - schema: additionalInformationPage.schema, - }, // Behavioral Changes Pages behaviorIntroPage: { path: 'additional-forms/mental-health-statement/behavior-changes', @@ -75,10 +69,17 @@ export const form0781PagesConfig = { uiSchema: behaviorListPage.uiSchema, schema: behaviorListPage.schema, }, + // Conclusion Pages consentPage: { path: 'additional-forms/mental-health-statement/consent', depends: formData => isRelatedToMST(formData), uiSchema: consentPage.uiSchema, schema: consentPage.schema, }, + additionalInformationPage: { + path: 'additional-forms/mental-health-statement/additional-information', + depends: formData => isCompletingForm0781(formData), + uiSchema: additionalInformationPage.uiSchema, + schema: additionalInformationPage.schema, + }, }; diff --git a/src/applications/disability-benefits/all-claims/constants.js b/src/applications/disability-benefits/all-claims/constants.js index 9c9c713ef6ea..721f5a510bee 100644 --- a/src/applications/disability-benefits/all-claims/constants.js +++ b/src/applications/disability-benefits/all-claims/constants.js @@ -1,7 +1,7 @@ import constants from 'vets-json-schema/dist/constants.json'; import fullSchema from 'vets-json-schema/dist/21-526EZ-ALLCLAIMS-schema.json'; -const { pciuStates: PCIU_STATES } = constants; +const { formProfileStates: FORM_PROFILE_STATES } = constants; import { VA_FORM_IDS, @@ -38,10 +38,10 @@ export const RESERVE_GUARD_TYPES = { reserve: 'Reserve', }; -export { PCIU_STATES }; +export { FORM_PROFILE_STATES }; -export const STATE_LABELS = PCIU_STATES.map(state => state.label); -export const STATE_VALUES = PCIU_STATES.map(state => state.value); +export const STATE_LABELS = FORM_PROFILE_STATES.map(state => state.label); +export const STATE_VALUES = FORM_PROFILE_STATES.map(state => state.value); export const MILITARY_STATE_VALUES = ['AA', 'AE', 'AP']; export const MILITARY_STATE_LABELS = [ diff --git a/src/applications/disability-benefits/all-claims/containers/ConfirmationPage.jsx b/src/applications/disability-benefits/all-claims/containers/ConfirmationPage.jsx index 21a0b5f26e05..a33b16ec13f8 100644 --- a/src/applications/disability-benefits/all-claims/containers/ConfirmationPage.jsx +++ b/src/applications/disability-benefits/all-claims/containers/ConfirmationPage.jsx @@ -1,11 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Toggler } from 'platform/utilities/feature-toggles'; -import { - focusElement, - scrollToTop, -} from '@department-of-veterans-affairs/platform-utilities/ui'; +import { focusElement } from '@department-of-veterans-affairs/platform-utilities/ui'; import { ConfirmationView } from 'platform/forms-system/src/js/components/ConfirmationView'; import { submissionStatuses, @@ -14,9 +10,6 @@ import { SAVED_SEPARATION_DATE, } from '../constants'; import { - retryableErrorContent, - successfulSubmitContent, - submitErrorContent, howLongForDecision, dependentsAdditionalBenefits, } from '../content/confirmation-page'; @@ -25,67 +18,38 @@ import { ClaimConfirmationInfo } from '../components/ClaimConfirmationInfo'; import { BddConfirmationAlert } from '../content/bddConfirmationAlert'; export default class ConfirmationPage extends React.Component { - constructor(props) { - super(props); - this.state = { isExpanded: false }; - } - componentDidMount() { - scrollToTop(); setTimeout(() => focusElement('va-alert h2'), 100); } - // the legacy 526 confirmation page that has 3 states - LegacyConfirmationPage = props => { - switch (props.submissionStatus) { - case submissionStatuses.succeeded: - return successfulSubmitContent(props); - case submissionStatuses.retry: - case submissionStatuses.exhausted: - case submissionStatuses.apiFailure: - return retryableErrorContent(props); - default: - return submitErrorContent(props); - } - }; - - // the new 526 submission confirmation that has one state ConfirmationPageContent = props => ( - - - - } - content={alertBody} - /> - {props.isSubmittingBDD && } - - - } - item1Actions={<>} - item2Header="Next we’ll send you a letter to let you know we have your claim" - item2Content="You should get this letter in about 1 week, plus mailing time, after we receive your claim." - /> - - {howLongForDecision} - {dependentsAdditionalBenefits} - - - - - {this.LegacyConfirmationPage(props)} - + + } content={alertBody} /> + {props.isSubmittingBDD && } + + + } + item1Actions={<>} + item2Header="Next we’ll send you a letter to let you know we have your claim" + item2Content="You should get this letter in about 1 week, plus mailing time, after we receive your claim." + /> + + {howLongForDecision} + {dependentsAdditionalBenefits} + + + ); render() { diff --git a/src/applications/disability-benefits/all-claims/content/confirmation-page.jsx b/src/applications/disability-benefits/all-claims/content/confirmation-page.jsx index 1ee47853e94e..d95a5c0a04a1 100644 --- a/src/applications/disability-benefits/all-claims/content/confirmation-page.jsx +++ b/src/applications/disability-benefits/all-claims/content/confirmation-page.jsx @@ -1,286 +1,5 @@ import React from 'react'; -import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; - -import DownloadPDF from '../components/DownloadPDF'; -import { capitalizeEachWord, formatDate } from '../utils'; -import { - successMessage, - checkLaterMessage, - errorMessage, -} from './confirmation-poll'; - -import { NULL_CONDITION_STRING } from '../constants'; -import { BddConfirmationAlert } from './bddConfirmationAlert'; - -const template = (props, title, content, submissionMessage, messageType) => { - const { fullName, disabilities, submittedAt, isSubmittingBDD } = props; - const { first, last, middle, suffix } = fullName; - // This is easier than passing down props and checking if the form type - const pageTitle = document.title.includes('Benefits') - ? 'Benefits Delivery at Discharge Claim' - : 'Disability Compensation Claim'; - - const backButtonContent = ( - - ); - - if (messageType === 'error') { - return ( - <> - -

    {title}

    - {content} -
    - {isSubmittingBDD && } - {backButtonContent} - - ); - } - - return ( -
    -
    - VA logo -

    {pageTitle}

    -
    - - -

    {title}

    - {content} -
    - {isSubmittingBDD && } - -

    - We’ll send you an email to confirm that we received your claim.{' '} - - You can also print this page for your records. - -

    - -
    -

    - {pageTitle} (Form 21-526EZ) -

    - - For {first} {middle} {last} {suffix} - -
      -
    • - Date submitted -
      - {formatDate(submittedAt)} -
    • -
    • - Conditions claimed -
      -
        - {disabilities.map((disability, i) => ( -
      • - {typeof disability === 'string' - ? capitalizeEachWord(disability) - : NULL_CONDITION_STRING} -
      • - ))} -
      - {submissionMessage} -
    • -
    - -
    - -
    -

    - How long will it take VA to make a decision on my claim? -

    -

    - We process applications in the order we receive them. The amount of - time it takes us to review you claim depends on: -

    -
      -
    • The type of claim you filed
    • -
    • - How many injuries or conditions you claimed and how complex they are -
    • -
    • - How long it takes us to collect the evidence needed to decide your - claim We may contact you if we have questions or need more - information -
    • -
    - -

    - How can I check the status of my claim? -

    -

    - You can check the status of your claim online. Please allow 24 hours - for your disability claim to show up there. If you don’t see your - disability claim online after 24 hours, please call Veterans Benefits - Assistance at , Monday - through Friday, 8:00 a.m. to 9:00 p.m. ET. -

    -

    - Check the status of your claim -

    - - {!isSubmittingBDD && ( - <> -

    - What should I do next? -

    -

    - If you have a spouse or child, you may be - entitled to additional payments. -

    - - Apply online to add a dependent - -

    - Or you can fill out and submit an Application Request to Add - and/or Remove Dependents (VA Form 21-686c) -

    -

    - -

    -

    - Note: If you’re claiming your child who became - permanently disabled before they turned 18, you’ll need to submit - all military and private medical records relating to the child’s - disabilities with your application. -

    -

    - - If you’re claiming a child who’s between 18 and 23 years old and - attending school full time - - , you’ll need to fill out and submit a Request for Approval of - School Attendance (VA Form 21-674) so we can verify their - attendance. -

    -

    - -

    -

    - If you have dependent parents, you may be - entitled to additional payments. Fill out and submit a Statement - of Dependency of Parent(s) (VA Form 21P-509). -

    -

    - -

    - - )} - -

    - What happens after I file a claim for disability compensation? -

    -

    - - Learn more about what happens after you file a disability claim - -

    -
    - - {backButtonContent} -
    - ); -}; - -export const retryableErrorContent = props => - template( - props, - "It's taking us longer than expected to submit your claim.", -
    -

    - This delay should be resolved within a few hours. We’ll keep trying to - submit your claim. You can check the status of your claim online after - 24 hours. -

    -

    - Check the status of your claim -

    -

    - - If you don’t see your disability claim online after 24 hours, - {' '} - please call Veterans Benefits Assistance at{' '} - , Monday through Friday, - 8:00 a.m. to 9:00 p.m. ET and provide this reference number{' '} - {props.jobId}. -

    -
    , - checkLaterMessage(props.jobId), - 'warning', - ); - -export const successfulSubmitContent = props => - template( - props, - 'Your claim has successfully been submitted.', - <>, - successMessage(props.claimId), - 'success', - ); - -export const submitErrorContent = props => { - const submissionIdContent = props.submissionId - ? ` and provide this reference number ${props.submissionId}` - : ''; - - return template( - props, - 'We’re sorry. Something went wrong when we tried to submit your claim.', -
    -

    For help submitting your claim:

    -
      -
    • - Please call Veterans Benefits Assistance at{' '} - , Monday through - Friday, 8:00 a.m. to 9:00 p.m. ET - {submissionIdContent}, or -
    • -
    • - Get in touch with your nearest Veterans Service Officer (VSO).{' '} - - Find out how to contact your nearest VSO - -
    • -
    -
    , - errorMessage(), - 'error', - ); -}; - export const howLongForDecision = ( <>

    How long will it take VA to make a decision on my claim?

    diff --git a/src/applications/disability-benefits/all-claims/content/confirmation-poll.jsx b/src/applications/disability-benefits/all-claims/content/confirmation-poll.jsx index db36696194be..64fd609f35ca 100644 --- a/src/applications/disability-benefits/all-claims/content/confirmation-poll.jsx +++ b/src/applications/disability-benefits/all-claims/content/confirmation-poll.jsx @@ -11,16 +11,6 @@ export const checkLaterMessage = () =>
    ; export const errorMessage = () =>
    ; -export const pendingMessage = longWait => { - const message = !longWait - ? 'Please wait while we submit your application and give you a confirmation number.' - : 'We’re sorry. It’s taking us longer than expected to submit your application. Thank you for your patience.'; - const label = longWait - ? 'we’re still trying to submit your application' - : 'submitting your application'; - return ; -}; - export const alertBody = ( <>

    Your submission is in progress.

    diff --git a/src/applications/disability-benefits/all-claims/migrations/02-convert-country-code.js b/src/applications/disability-benefits/all-claims/migrations/02-convert-country-code.js index 9b87a70282e4..b2040efb55a6 100644 --- a/src/applications/disability-benefits/all-claims/migrations/02-convert-country-code.js +++ b/src/applications/disability-benefits/all-claims/migrations/02-convert-country-code.js @@ -33,7 +33,7 @@ export default function convertCountryCode(savedData) { }, ]; - // If they've got a 3-letter country code, try to match it to the list of PCIU countries + // If they've got a 3-letter country code, try to match it to the list of form data countries let earliestReturnUrl = ''; let { formData: newData } = savedData; addressPaths.forEach(({ path, returnUrl }) => { diff --git a/src/applications/disability-benefits/all-claims/tests/components/ConfirmationPoll.unit.spec.jsx b/src/applications/disability-benefits/all-claims/tests/components/ConfirmationPoll.unit.spec.jsx index 77f917383f90..d878f6902cba 100644 --- a/src/applications/disability-benefits/all-claims/tests/components/ConfirmationPoll.unit.spec.jsx +++ b/src/applications/disability-benefits/all-claims/tests/components/ConfirmationPoll.unit.spec.jsx @@ -1,12 +1,10 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; import { expect } from 'chai'; - import { mockApiRequest, mockMultipleApiRequests, } from 'platform/testing/unit/helpers'; -import { Toggler } from 'platform/utilities/feature-toggles'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-redux'; @@ -20,11 +18,7 @@ import formConfig from '../../config/form'; const middleware = [thunk]; const mockStore = configureStore(middleware); -const getData = ({ - renderName = true, - suffix = 'Esq.', - disability526NewConfirmationPage = false, -} = {}) => ({ +const getData = ({ renderName = true, suffix = 'Esq.' } = {}) => ({ user: { profile: { userFullName: renderName @@ -32,11 +26,6 @@ const getData = ({ : {}, }, }, - featureToggles: { - loading: false, - [Toggler.TOGGLE_NAMES - .disability526NewConfirmationPage]: disability526NewConfirmationPage, - }, }); const pendingResponse = { @@ -173,28 +162,6 @@ describe('ConfirmationPoll', () => { }, 50); }); - it('should render long wait alert', done => { - mockMultipleApiRequests([ - pendingResponse, - pendingResponse, - pendingResponse, - successResponse, - ]); - - const form = mount( - - , - , - ); - setTimeout(() => { - expect(global.fetch.callCount).to.equal(4); - const alert = form.find('va-loading-indicator'); - expect(alert.html()).to.contain('longer than expected'); - form.unmount(); - done(); - }, 50); - }); - it('should ignore immediate api failures', done => { mockMultipleApiRequests([ errorResponse, @@ -209,10 +176,9 @@ describe('ConfirmationPoll', () => { , ); setTimeout(() => { - form.update(); expect(global.fetch.callCount).to.equal(4); - const confirmationPage = form.find('ConfirmationPage'); - expect(confirmationPage.length).to.equal(1); + const loadingIndicator = form.find('va-loading-indicator'); + expect(loadingIndicator.length).to.equal(1); form.unmount(); done(); }, 50); diff --git a/src/applications/disability-benefits/all-claims/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/disability-benefits/all-claims/tests/containers/ConfirmationPage.unit.spec.jsx index 3ddea2e84b65..cdfe3315d0ea 100644 --- a/src/applications/disability-benefits/all-claims/tests/containers/ConfirmationPage.unit.spec.jsx +++ b/src/applications/disability-benefits/all-claims/tests/containers/ConfirmationPage.unit.spec.jsx @@ -1,28 +1,15 @@ import React from 'react'; import { expect } from 'chai'; import { render } from '@testing-library/react'; -import { Toggler } from 'platform/utilities/feature-toggles'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; import ConfirmationPage from '../../containers/ConfirmationPage'; -import { - submissionStatuses, - WIZARD_STATUS, - FORM_STATUS_BDD, - SAVED_SEPARATION_DATE, -} from '../../constants'; +import { submissionStatuses } from '../../constants'; import { bddConfirmationHeadline } from '../../content/bddConfirmationAlert'; import formConfig from '../../config/form'; -const retryableErrorTitle = - "It's taking us longer than expected to submit your claim."; - -const getData = ({ - renderName = true, - suffix = 'Esq.', - disability526NewConfirmationPage = false, -} = {}) => ({ +const getData = ({ renderName = true, suffix = 'Esq.' } = {}) => ({ user: { profile: { userFullName: renderName @@ -33,11 +20,6 @@ const getData = ({ form: { data: {}, }, - featureToggles: { - loading: false, - [Toggler.TOGGLE_NAMES - .disability526NewConfirmationPage]: disability526NewConfirmationPage, - }, }); describe('ConfirmationPage', () => { @@ -59,259 +41,103 @@ describe('ConfirmationPage', () => { const middleware = [thunk]; const mockStore = configureStore(middleware); - const testPage = (status, otherProps) => - render( - - - , - , - ); - - it('should render success status', () => { - const tree = testPage(submissionStatuses.succeeded); - tree.getByText('Claim ID number'); - tree.getByText('Your claim has successfully been submitted.'); - tree.getByText('Date submitted'); - - tree.getByText('Conditions claimed'); - tree.getByText('Something Something'); - tree.getByText('Unknown Condition'); - }); - - it('should not render success with BDD SHA alert when not submitting BDD claim', () => { - const { queryByText } = render( - - - , - , + /** + * Utility to verify confirmation page + * @param {string} claimId - if claimId has a value, verify the label and value are on the page + * @param {boolean} isBdd - if true, verify BDD alert is present, otherwise verify it is not present + * @param {string} submissionStatus - used to verify logic based on success or non success status + */ + const verifyConfirmationPage = (claimId, isBdd = false, submissionStatus) => { + const store = mockStore( + getData({ + disability526NewConfirmationPage: true, + }), ); - - expect(queryByText(bddConfirmationHeadline)).to.not.exist; - }); - - it('should render success with BDD SHA alert', () => { - const tree = render( - - - , - , - ); - tree.getByText('Claim ID number'); - tree.getByText(bddConfirmationHeadline); - }); - - it('should render retry status', () => { - const tree = testPage(submissionStatuses.retry); - tree.getByText(retryableErrorTitle); - }); - - it('should render exhausted status', () => { - const tree = testPage(submissionStatuses.exhausted); - tree.getByText(retryableErrorTitle); - }); - - it('should render apiFailure status', () => { - const tree = testPage(submissionStatuses.apiFailure); - tree.getByText(retryableErrorTitle); - }); - - it('should render retryable failure with BDD SHA alert', () => { - const tree = render( - - - , - , - ); - - tree.getByText( - 'Submit your Separation Health Assessment - Part A Self-Assessment now if you haven’t already', - ); - tree.getByText( - 'Separation Health Assessment - Part A Self-Assessment (opens in new tab)', - ); - }); - - it('should render other status', () => { - const tree = testPage(submissionStatuses.failed, { submissionId: '123' }); - tree.getByText( - 'We’re sorry. Something went wrong when we tried to submit your claim.', - ); - tree.getByText('and provide this reference number 123', { exact: false }); - }); - - it('should render note about email', () => { const props = { ...defaultProps, + submissionStatus, }; - - const tree = render( - - - , + if (claimId) { + props.claimId = claimId; + } + if (isBdd) { + props.isSubmittingBDD = true; + } + + const { container, getByText, queryByText } = render( + + , ); - tree.getByText( - 'We’ll send you an email to confirm that we received your claim.', - ); - }); + if (isBdd) { + getByText(bddConfirmationHeadline); + } else { + expect(queryByText(bddConfirmationHeadline)).to.not.exist; + } - it('should not render email message when there is an error', () => { - const props = { - ...defaultProps, - submissionStatus: submissionStatuses.failed, - }; + // success alert + getByText('Form submission started on', { exact: false }); + getByText('Your submission is in progress.', { + exact: false, + }); - const tree = render( - - - , + // summary box with claim info + getByText('Disability Compensation Claim'); + getByText('For Hector Lee Brooks Sr.'); + getByText('Date submitted'); + getByText('November 7, 2024'); + getByText('Conditions claimed'); + getByText('Something Something'); + getByText('Unknown Condition'); + + if (claimId) { + getByText('Claim ID number'); + getByText(claimId); + } else { + expect(queryByText('Claim ID number')).to.not.exist; + } + + // rest of sections are present + getByText('Print this confirmation page'); + getByText('What to expect'); + getByText('How to contact us if you have questions'); + getByText('How long will it take VA to make a decision on my claim?'); + getByText('If I have dependents, how can I receive additional benefits?'); + getByText('Need help?'); + + // links + expect(container.querySelectorAll('va-link')).to.have.lengthOf(5); + const link = container.querySelectorAll('va-link')[1]; + expect(link.getAttribute('download')).to.exist; + expect(link.getAttribute('filetype')).to.equal('PDF'); + expect(link.getAttribute('href')).to.equal( + 'https://www.vba.va.gov/pubs/forms/VBA-21-686c-ARE.pdf', + ); + expect(link.getAttribute('pages')).to.equal('15'); + expect(link.getAttribute('text')).to.equal( + 'Download VA Form 21-686c (opens in new tab)', ); + }; - expect( - tree.queryByText( - 'We’ll send you an email to confirm that we received your claim.', - ), - ).to.be.null; + it('should render confirmation page when submission succeeded with claim id', () => { + verifyConfirmationPage('12345678', false, submissionStatuses.succeeded); }); - it('should reset wizard state & values', () => { - sessionStorage.setItem(WIZARD_STATUS, 'a'); - sessionStorage.setItem(FORM_STATUS_BDD, 'b'); - sessionStorage.setItem(SAVED_SEPARATION_DATE, 'c'); - - const tree = testPage(submissionStatuses.succeeded); - tree.getByText('Claim ID number'); - expect(sessionStorage.getItem(WIZARD_STATUS)).to.be.null; - expect(sessionStorage.getItem(FORM_STATUS_BDD)).to.be.null; - expect(sessionStorage.getItem(SAVED_SEPARATION_DATE)).to.be.null; - tree.unmount(); + it('should render confirmation page when submission succeeded with no claim id', () => { + verifyConfirmationPage(undefined, false, submissionStatuses.succeeded); }); - describe('new confirmation page (toggle enabled)', () => { - // new confirmation page toggle on - - /** - * Utility to verify confirmation page - * @param {string} claimId - if claimId has a value, verify the label and value are on the page - * @param {boolean} isBdd - if true, verify BDD alert is present, otherwise verify it is not present - * @param {string} submissionStatus - used to verify logic based on success or non success status - */ - const verifyConfirmationPage = ( - claimId, - isBdd = false, - submissionStatus, - ) => { - const store = mockStore( - getData({ - disability526NewConfirmationPage: true, - }), - ); - const props = { - ...defaultProps, - submissionStatus, - }; - if (claimId) { - props.claimId = claimId; - } - if (isBdd) { - props.isSubmittingBDD = true; - } - - const { container, getByText, queryByText } = render( - - - , - ); - - if (isBdd) { - getByText(bddConfirmationHeadline); - } else { - expect(queryByText(bddConfirmationHeadline)).to.not.exist; - } - - // success alert - getByText('Form submission started on', { exact: false }); - getByText('Your submission is in progress.', { - exact: false, - }); - - // summary box with claim info - getByText('Disability Compensation Claim'); - getByText('For Hector Lee Brooks Sr.'); - getByText('Date submitted'); - getByText('November 7, 2024'); - getByText('Conditions claimed'); - getByText('Something Something'); - getByText('Unknown Condition'); - - if (claimId) { - getByText('Claim ID number'); - getByText(claimId); - } else { - expect(queryByText('Claim ID number')).to.not.exist; - } - - // rest of sections are present - getByText('Print this confirmation page'); - getByText('What to expect'); - getByText('How to contact us if you have questions'); - getByText('How long will it take VA to make a decision on my claim?'); - getByText('If I have dependents, how can I receive additional benefits?'); - getByText('Need help?'); - - // links - expect(container.querySelectorAll('va-link')).to.have.lengthOf(5); - const link = container.querySelectorAll('va-link')[1]; - expect(link.getAttribute('download')).to.exist; - expect(link.getAttribute('filetype')).to.equal('PDF'); - expect(link.getAttribute('href')).to.equal( - 'https://www.vba.va.gov/pubs/forms/VBA-21-686c-ARE.pdf', - ); - expect(link.getAttribute('pages')).to.equal('15'); - expect(link.getAttribute('text')).to.equal( - 'Download VA Form 21-686c (opens in new tab)', - ); - }; - - it('should render confirmation page when submission succeeded with claim id', () => { - verifyConfirmationPage('12345678', false, submissionStatuses.succeeded); - }); - - it('should render confirmation page when submission succeeded with no claim id', () => { - verifyConfirmationPage(undefined, false, submissionStatuses.succeeded); - }); - - it('should render success with BDD SHA alert when submission succeeded with claim id for BDD', () => { - verifyConfirmationPage('12345678', true, submissionStatuses.succeeded); - }); + it('should render success with BDD SHA alert when submission succeeded with claim id for BDD', () => { + verifyConfirmationPage('12345678', true, submissionStatuses.succeeded); + }); - it('should render success when form submitted successfully but submission status has api failure', () => { - verifyConfirmationPage('', false, submissionStatuses.apiFailure); // 500 - }); + it('should render success when form submitted successfully but submission status has api failure', () => { + verifyConfirmationPage('', false, submissionStatuses.apiFailure); // 500 + }); - it('should render success when form submitted successfully but submission status has non retryable error', () => { - // status code 200, but response has "status: non_retryable_error" - verifyConfirmationPage('', false, submissionStatuses.failed); - }); + it('should render success when form submitted successfully but submission status has non retryable error', () => { + // status code 200, but response has "status: non_retryable_error" + verifyConfirmationPage('', false, submissionStatuses.failed); }); }); diff --git a/src/applications/disability-benefits/all-claims/tests/utils/form0781.unit.spec.js b/src/applications/disability-benefits/all-claims/tests/utils/form0781.unit.spec.js index e2983f71d6df..cf0245864732 100644 --- a/src/applications/disability-benefits/all-claims/tests/utils/form0781.unit.spec.js +++ b/src/applications/disability-benefits/all-claims/tests/utils/form0781.unit.spec.js @@ -5,11 +5,14 @@ import { showBehaviorIntroPage, showBehaviorIntroCombatPage, showBehaviorListPage, + isCompletingForm0781, + isRelatedToMST, } from '../../utils/form0781'; +import { form0781WorkflowChoices } from '../../content/form0781'; describe('showForm0781Pages', () => { describe('when the flipper is on and a user is claiming a new condition', () => { - it('should should return true', () => { + it('should return true', () => { const formData = { syncModern0781Flow: true, mentalHealth: { @@ -23,7 +26,7 @@ describe('showForm0781Pages', () => { }); describe('when the flipper is off and a user is claiming a new condition', () => { - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: false, mentalHealth: { @@ -37,7 +40,7 @@ describe('showForm0781Pages', () => { }); describe('when the flipper is on and a user is not claiming a new condition', () => { - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: true, mentalHealth: { @@ -49,7 +52,7 @@ describe('showForm0781Pages', () => { expect(showForm0781Pages(formData)).to.eq(false); }); - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: true, mentalHealth: { @@ -63,11 +66,106 @@ describe('showForm0781Pages', () => { }); }); +// Flipper is on AND user is claiming a new condition +describe('isCompletingForm0781', () => { + describe('when the user selects to optIn to completing the form online', () => { + it('should return true', () => { + const formData = { + syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, + mentalHealth: { + conditions: { + someCondition: true, + }, + }, + }; + expect(isCompletingForm0781(formData)).to.eq(true); + }); + }); + describe('when the user selects to submit a paper form', () => { + it('should return false', () => { + const formData = { + syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.SUBMIT_PAPER_FORM, + mentalHealth: { + conditions: { + someCondition: true, + }, + }, + }; + expect(isCompletingForm0781(formData)).to.eq(false); + }); + }); + describe('when the user selects to opt out', () => { + it('should return false', () => { + const formData = { + syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.OPT_OUT_OF_FORM0781, + mentalHealth: { + conditions: { + someCondition: true, + }, + }, + }; + expect(isCompletingForm0781(formData)).to.eq(false); + }); + }); +}); + +// Flipper is on AND user is claiming a new condition AND user opts into completing online form +describe('isRelatedToMST', () => { + describe('when a user has selected MST', () => { + it('should return true', () => { + const formData = { + syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, + mentalHealth: { + conditions: { + someCondition: true, + }, + eventTypes: { + combat: true, + mst: true, + }, + }, + }; + + expect(isRelatedToMST(formData)).to.eq(true); + }); + }); + describe('when a user has NOT selected MST', () => { + it('should return false', () => { + const formData = { + syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, + mentalHealth: { + conditions: { + someCondition: true, + }, + eventTypes: { + combat: true, + mst: false, + }, + }, + }; + expect(isRelatedToMST(formData)).to.eq(false); + }); + }); +}); + +// Flipper is on AND user is claiming a new condition AND user opts into completing online form describe('showBehaviorIntroCombatPage', () => { describe('when a user has selected ONLY combat related events', () => { - it('should should return true', () => { + it('should return true', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, @@ -84,9 +182,11 @@ describe('showBehaviorIntroCombatPage', () => { }); describe('when a user has selected combat related AND non-combat related events', () => { - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, @@ -103,9 +203,11 @@ describe('showBehaviorIntroCombatPage', () => { }); describe('when a user has not selected combat related events', () => { - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, @@ -124,9 +226,11 @@ describe('showBehaviorIntroCombatPage', () => { describe('showBehaviorIntroPage', () => { describe('when a user has not selected ONLY combat related events', () => { - it('should should return true', () => { + it('should return true', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, @@ -142,9 +246,11 @@ describe('showBehaviorIntroPage', () => { }); describe('when a user has selected ONLY combat related events', () => { - it('should should return false', () => { + it('should return false', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, @@ -161,10 +267,12 @@ describe('showBehaviorIntroPage', () => { }); describe('showBehaviorListPage', () => { - describe('when a user has selected ONLY combat related events and opted in', () => { - it('should should return true', () => { + describe('when a user has selected ONLY combat related events', () => { + it('should return true', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, 'view:answerCombatBehaviorQuestions': 'true', mentalHealth: { conditions: { @@ -180,29 +288,12 @@ describe('showBehaviorListPage', () => { }); }); - describe('when a user has selected ONLY combat related events and opted OUT', () => { - it('should should return false', () => { - const formData = { - syncModern0781Flow: true, - 'view:answerCombatBehaviorQuestions': 'false', - mentalHealth: { - conditions: { - someCondition: true, - }, - eventTypes: { - combat: true, - nonMst: false, - }, - }, - }; - expect(showBehaviorListPage(formData)).to.eq(false); - }); - }); - describe('when a user has not selected ONLY combat related events', () => { - it('should should return true', () => { + it('should return true', () => { const formData = { syncModern0781Flow: true, + 'view:mentalHealthWorkflowChoice': + form0781WorkflowChoices.COMPLETE_ONLINE_FORM, mentalHealth: { conditions: { someCondition: true, diff --git a/src/applications/disability-benefits/all-claims/utils/form0781.js b/src/applications/disability-benefits/all-claims/utils/form0781.js index b3f5bf7c0a22..8ec9f0b9896b 100644 --- a/src/applications/disability-benefits/all-claims/utils/form0781.js +++ b/src/applications/disability-benefits/all-claims/utils/form0781.js @@ -78,7 +78,7 @@ export function isRelatedToMST(formData) { * - AND is not seeing the 'Combat Only' version of this page */ export function showBehaviorIntroPage(formData) { - return showForm0781Pages(formData) && !combatOnlySelection(formData); + return isCompletingForm0781(formData) && !combatOnlySelection(formData); } /* @@ -91,7 +91,7 @@ export function showBehaviorIntroPage(formData) { * - in all other cases */ export function showBehaviorIntroCombatPage(formData) { - return showForm0781Pages(formData) && combatOnlySelection(formData); + return isCompletingForm0781(formData) && combatOnlySelection(formData); } /* @@ -108,7 +108,7 @@ export function showBehaviorListPage(formData) { _.get('view:answerCombatBehaviorQuestions', formData, 'false') === 'true'; return ( - showForm0781Pages(formData) && + isCompletingForm0781(formData) && ((showBehaviorIntroCombatPage(formData) && answerQuestions) || !combatOnlySelection(formData)) ); diff --git a/src/applications/discharge-wizard/components/questions/Reason.jsx b/src/applications/discharge-wizard/components/questions/Reason.jsx index fc79b2d92772..e6bdac2aae59 100644 --- a/src/applications/discharge-wizard/components/questions/Reason.jsx +++ b/src/applications/discharge-wizard/components/questions/Reason.jsx @@ -16,8 +16,15 @@ const Reason = ({ formResponses, setReason, router, viewedIntroPage }) => { const shortName = SHORT_NAME_MAP.REASON; const H1 = QUESTION_MAP[shortName]; const reason = formResponses[shortName]; - const hint = - 'Note: If more than one of these descriptions matches your situation, choose the one that started the events that led to your discharge. For example, if you sustained a traumatic brain injury (TBI), which led to posttraumatic stress disorder (PTSD), choose the option associated with TBI.'; + const hint = ( + <> + Note: If more than one of these descriptions matches + your situation, choose the one that started the events that led to your + discharge. For example, if you sustained a traumatic brain injury (TBI), + which led to posttraumatic stress disorder (PTSD), choose the option + associated with TBI. + + ); const { REASON_PTSD, REASON_TBI, diff --git a/src/applications/discover-your-benefits/config/form.js b/src/applications/discover-your-benefits/config/form.js index aedc827981df..91f0880feebb 100644 --- a/src/applications/discover-your-benefits/config/form.js +++ b/src/applications/discover-your-benefits/config/form.js @@ -33,9 +33,6 @@ export const isOnConfirmationPage = currentLocation => { }; export const formConfig = { - formOptions: { - fullWidth: true, - }, rootUrl: manifest.rootUrl, urlPrefix: '/', // submitUrl: '/v0/api', diff --git a/src/applications/discover-your-benefits/constants/benefits.js b/src/applications/discover-your-benefits/constants/benefits.js index ed005ef8c361..40ebff704aea 100644 --- a/src/applications/discover-your-benefits/constants/benefits.js +++ b/src/applications/discover-your-benefits/constants/benefits.js @@ -404,10 +404,7 @@ export const BENEFITS_LIST = [ characterOfDischargeTypes.UNCHARACTERIZED, characterOfDischargeTypes.NOT_SURE, ], - [mappingTypes.DISABILITY_RATING]: [ - disabilityTypes.APPLIED_AND_RECEIVED, - disabilityTypes.STARTED, - ], + [mappingTypes.DISABILITY_RATING]: [anyType.ANY], [mappingTypes.GI_BILL]: [anyType.ANY], }, learnMoreURL: @@ -596,10 +593,7 @@ export const BENEFITS_LIST = [ characterOfDischargeTypes.BAD_CONDUCT, characterOfDischargeTypes.NOT_SURE, ], - [mappingTypes.DISABILITY_RATING]: [ - disabilityTypes.STARTED, - disabilityTypes.NOT_APPLIED, - ], + [mappingTypes.DISABILITY_RATING]: [anyType.ANY], [mappingTypes.GI_BILL]: [anyType.ANY], }, learnMoreURL: 'https://www.va.gov/disability/', diff --git a/src/applications/discover-your-benefits/sass/discover-your-benefits.scss b/src/applications/discover-your-benefits/sass/discover-your-benefits.scss index 0670ccbe900a..ffcf762c96e3 100644 --- a/src/applications/discover-your-benefits/sass/discover-your-benefits.scss +++ b/src/applications/discover-your-benefits/sass/discover-your-benefits.scss @@ -31,6 +31,10 @@ margin-right: 0; } +.discover-your-benefits .usa-width-two-thirds { + width: auto !important; +} + #filters-section-desktop { @media (min-width: $medium-screen) { border-right: 2px solid var(--vads-color-gray-medium); diff --git a/src/applications/dispute-debt/actions/index.js b/src/applications/dispute-debt/actions/index.js new file mode 100644 index 000000000000..ba595d220b05 --- /dev/null +++ b/src/applications/dispute-debt/actions/index.js @@ -0,0 +1,88 @@ +import * as Sentry from '@sentry/browser'; +import { isValid } from 'date-fns'; +import { head } from 'lodash'; +import { formatDateShort } from 'platform/utilities/date'; +import { apiRequest } from 'platform/utilities/api'; +import environment from 'platform/utilities/environment'; +import { + deductionCodes, + DEBT_TYPES, + DEBTS_FETCH_INITIATED, + DEBTS_FETCH_SUCCESS, + DEBTS_FETCH_FAILURE, +} from '../constants'; +import { currency, endDate } from '../utils'; + +// helper functions to get debt and copay labels and descriptions +const getDebtLabel = debt => + `${currency(debt?.currentAr)} overpayment for ${deductionCodes[ + debt.deductionCode + ] || debt.benefitType}`; + +const getDebtDescription = debt => { + // most recent debt history entry + const dates = debt?.debtHistory?.map(m => new Date(m.date)) ?? []; + const sortedHistory = dates.sort((a, b) => Date.parse(b) - Date.parse(a)); + const mostRecentDate = isValid(head(sortedHistory)) + ? formatDateShort(head(sortedHistory)) + : ''; + const dateby = endDate(mostRecentDate, 30); + return dateby ? `Pay or request help by ${dateby}` : ''; +}; + +export const fetchDebts = async dispatch => { + dispatch({ + type: DEBTS_FETCH_INITIATED, + pending: true, + }); + const getDebts = () => { + const options = { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-Key-Inflection': 'camel', + 'Source-App-Name': window.appName, + }, + }; + + return apiRequest(`${environment.API_URL}/v0/debts`, options); + }; + + try { + const response = await getDebts(); + const approvedDeductionCodes = Object.keys(deductionCodes); + // filter approved deductionCodes && + // remove debts that have a current amount owed of 0 + const filteredResponse = response.debts + .filter(debt => approvedDeductionCodes.includes(debt.deductionCode)) + .filter(debt => debt.currentAr > 0) + .map((debt, index) => ({ + ...debt, + id: index, + debtType: DEBT_TYPES.DEBT, + })); + + const simplifiedResponse = filteredResponse.map(debt => ({ + compositeDebtId: debt.compositeDebtId, + label: getDebtLabel(debt), + description: getDebtDescription(debt), + debtType: DEBT_TYPES.DEBT, + })); + + return dispatch({ + type: DEBTS_FETCH_SUCCESS, + debts: simplifiedResponse, + }); + } catch (error) { + Sentry.withScope(scope => { + scope.setExtra('error', error); + Sentry.captureMessage(`FSR fetchDebts failed: ${error.detail}`); + }); + dispatch({ + type: DEBTS_FETCH_FAILURE, + error, + }); + throw new Error(error); + } +}; diff --git a/src/applications/dispute-debt/app-entry.jsx b/src/applications/dispute-debt/app-entry.jsx new file mode 100644 index 000000000000..b22da2fc3702 --- /dev/null +++ b/src/applications/dispute-debt/app-entry.jsx @@ -0,0 +1,15 @@ +import '@department-of-veterans-affairs/platform-polyfills'; +import './sass/dispute-debt.scss'; + +import { startAppFromIndex } from '@department-of-veterans-affairs/platform-startup/exports'; + +import routes from './routes'; +import reducer from './reducers'; +import manifest from './manifest.json'; + +startAppFromIndex({ + entryName: manifest.entryName, + url: manifest.rootUrl, + reducer, + routes, +}); diff --git a/src/applications/dispute-debt/components/AlertCard.jsx b/src/applications/dispute-debt/components/AlertCard.jsx new file mode 100644 index 000000000000..afb33e0a7066 --- /dev/null +++ b/src/applications/dispute-debt/components/AlertCard.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import environment from 'platform/utilities/environment'; +import { DEBT_TYPES } from '../constants'; + +const AlertCard = ({ debtType }) => { + return ( + <> + +

    + We can’t access your{' '} + {`${debtType === DEBT_TYPES.DEBT ? 'debt' : 'copay'}`} records right + now +

    +

    + We’re sorry. Information about{' '} + {`${debtType === DEBT_TYPES.DEBT ? 'debts' : 'copays'}`} you might + have is unavailable because something went wrong on our end. Please + check back soon. +

    +

    + If you continue having trouble viewing information about your{' '} + {`${debtType === DEBT_TYPES.DEBT ? 'debts' : 'copays'}`}, contact us + online through Ask VA. +

    +
    + + Go back to VA.gov + + + ); +}; +AlertCard.propTypes = { + debtType: PropTypes.string, +}; + +export default AlertCard; diff --git a/src/applications/dispute-debt/components/DebtSelection.jsx b/src/applications/dispute-debt/components/DebtSelection.jsx new file mode 100644 index 000000000000..62b5013510f5 --- /dev/null +++ b/src/applications/dispute-debt/components/DebtSelection.jsx @@ -0,0 +1,121 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { setData } from 'platform/forms-system/src/js/actions'; +import { VaCheckboxGroup } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { setFocus } from '../utils'; + +import AlertCard from './AlertCard'; +import { DEBT_TYPES } from '../constants'; + +const DebtSelection = ({ formContext }) => { + const { availableDebts, isDebtError } = useSelector( + state => state.availableDebts, + ); + const { data } = useSelector(state => state.form); + const { selectedDebts = [] } = data; + const dispatch = useDispatch(); + + const [selectionError, setSelectionError] = useState(null); + + useEffect( + () => { + if (formContext.submitted && !selectedDebts?.length) { + setSelectionError('Choose at least one debt'); + setFocus('va-checkbox-group'); + return; + } + setSelectionError(null); + }, + [dispatch, formContext.submitted, selectedDebts?.length], + ); + + // nothing to actually display so we short circuit and return just the error (no question info) + if (isDebtError || !availableDebts.length) { + return ; + } + + const onGroupChange = ({ detail, target }) => { + // adding new prop selectedDebtId to selectedDebts so it's easier to filter on uncheck + if (detail.checked) { + // debts and copays use different unique identifier props, so we need to check the data-debt-type to pull the correct one + let selectedDebt; + if (target.dataset.debtType === DEBT_TYPES.DEBT) { + selectedDebt = availableDebts.find( + debt => debt.compositeDebtId === target.dataset.index, + ); + } + + // including new selectedDebtId prop + const newlySelectedDebts = [ + ...selectedDebts, + { ...selectedDebt, selectedDebtId: target.dataset.index }, + ]; + + return dispatch( + setData({ + ...data, + selectedDebts: newlySelectedDebts, + }), + ); + } + + // uncheck by new selectedDebtId prop + const combinedChecked = selectedDebts?.filter( + selection => selection.selectedDebtId !== target.dataset.index, + ); + + return dispatch( + setData({ + ...data, + selectedDebts: combinedChecked, + }), + ); + }; + + return ( +
    + + {availableDebts.map(debt => ( + currDebt.selectedDebtId === debt.compositeDebtId, + )} + checkbox-description={debt.description} + data-debt-type={DEBT_TYPES.DEBT} + data-index={debt.compositeDebtId} + data-testid="debt-selection-checkbox" + key={debt.compositeDebtId} + label={debt.label} + /> + ))} + + + If you received a letter about a VA benefit debt that isn’t listed here, + call us at (or{' '} + from overseas). + We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET. + +
    + ); +}; + +DebtSelection.propTypes = { + availableDebts: PropTypes.array, + data: PropTypes.shape({ + selectedDebts: PropTypes.array, + }), + formContext: PropTypes.shape({ + submitted: PropTypes.bool, + }), + isDebtError: PropTypes.bool, +}; + +export default DebtSelection; diff --git a/src/applications/dispute-debt/components/VeteranInformation.jsx b/src/applications/dispute-debt/components/VeteranInformation.jsx new file mode 100644 index 000000000000..31359a839406 --- /dev/null +++ b/src/applications/dispute-debt/components/VeteranInformation.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { isValid, format } from 'date-fns'; + +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; + +import { selectProfile } from '~/platform/user/selectors'; + +import { srSubstitute } from '~/platform/forms-system/src/js/utilities/ui/mask-string'; + +import { FORMAT_YMD_DATE_FNS, FORMAT_READABLE_DATE_FNS } from '../constants'; + +import { parseDateToDateObj } from '../utils'; + +// separate each number so the screenreader reads "number ending with 1 2 3 4" +// instead of "number ending with 1,234" +const mask = value => { + const number = (value || '').toString().slice(-4); + return srSubstitute( + `●●●–●●–${number}`, + `ending with ${number.split('').join(' ')}`, + ); +}; + +const VeteranInformation = ({ formData }) => { + const { ssnLastFour, vaFileLastFour } = formData?.veteranInformation || {}; + const { dob, userFullName = {} } = useSelector(selectProfile); + const { first, middle, last, suffix } = userFullName; + + const dobDateObj = parseDateToDateObj(dob || null, FORMAT_YMD_DATE_FNS); + + return ( + <> +

    + Confirm the personal information we have on file for you. +

    +
    + + {`${first || ''} ${middle || ''} ${last || ''}`} + {suffix ? `, ${suffix}` : null} + + {ssnLastFour ? ( +

    + Social Security number:{' '} + + {mask(ssnLastFour)} + +

    + ) : null} + {vaFileLastFour ? ( +

    + VA file number:{' '} + + {mask(vaFileLastFour)} + +

    + ) : null} +

    + Date of birth:{' '} + {isValid(dobDateObj) ? ( + + {format(dobDateObj, FORMAT_READABLE_DATE_FNS)} + + ) : null} +

    +
    + +
    + +

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

    + + ); +}; + +VeteranInformation.propTypes = { + formData: PropTypes.shape({ + veteranInformation: PropTypes.shape({ + ssnLastFour: PropTypes.string, + vaFileLastFour: PropTypes.string, + }), + }), +}; + +export default VeteranInformation; diff --git a/src/applications/dispute-debt/config/form.js b/src/applications/dispute-debt/config/form.js new file mode 100644 index 000000000000..a92e2b7ec678 --- /dev/null +++ b/src/applications/dispute-debt/config/form.js @@ -0,0 +1,116 @@ +import footerContent from 'platform/forms/components/FormFooter'; +import { VA_FORM_IDS } from 'platform/forms/constants'; + +import profileContactInfo from 'platform/forms-system/src/js/definitions/profileContactInfo'; +import { + veteranInformation, + debtSelection, + disputeReason, + supportStatement, +} from '../pages'; + +import ConfirmationPage from '../containers/ConfirmationPage'; +import IntroductionPage from '../containers/IntroductionPage'; + +import prefillTransformer from './prefill-transformer'; +import { TITLE, SUBTITLE } from '../constants'; +import manifest from '../manifest.json'; + +/** @type {FormConfig} */ +const formConfig = { + rootUrl: manifest.rootUrl, + urlPrefix: '/', + submitUrl: '/debts_api/v0/digital_disputes', + submit: () => + Promise.resolve({ attributes: { confirmationNumber: '123123123' } }), + trackingPrefix: 'dispute-debt', + introduction: IntroductionPage, + confirmation: ConfirmationPage, + dev: { + showNavLinks: true, + collapsibleNavLinks: true, + }, + formId: VA_FORM_IDS.FORM_DISPUTE_DEBT, + saveInProgress: { + // messages: { + // inProgress: 'Your digital dispute for debts application (DISPUTE-DEBT) is in progress.', + // expired: 'Your saved digital dispute for debts application (DISPUTE-DEBT) has expired. If you want to apply for digital dispute for debts, please start a new application.', + // saved: 'Your digital dispute for debts application has been saved.', + // }, + }, + version: 0, + prefillEnabled: true, + prefillTransformer, + savedFormMessages: { + notFound: 'Please start over to apply for digital dispute for debts.', + noAuth: + 'Please sign in again to continue your application for digital dispute for debts.', + }, + title: TITLE, + subTitle: SUBTITLE, + defaultDefinitions: {}, + chapters: { + personalInformationChapter: { + title: 'Veteran information', + pages: { + veteranInformation: { + title: 'Your personal information', + path: 'personal-information', + uiSchema: veteranInformation.uiSchema, + schema: veteranInformation.schema, + }, + ...profileContactInfo({ + contactInfoPageKey: 'confirmContactInfo2', + contactPath: 'contact-information', + contactInfoRequiredKeys: [ + 'mobilePhone', + 'homePhone', + 'mailingAddress', + 'email', + ], + included: ['mobilePhone', 'homePhone', 'mailingAddress', 'email'], // default + wrapperKey: 'veteranInformation', + }), + }, + }, + debtSelectionChapter: { + title: 'Debt Selection', + pages: { + selectDebt: { + path: 'select-debt', + title: 'Which debt are you disputing?', + uiSchema: debtSelection.uiSchema, + schema: debtSelection.schema, + initialData: { + selectedDebts: [], + }, + }, + }, + }, + reasonForDisputeChapter: { + title: 'Reason for dispute', + pages: { + disputeReason: { + path: 'existence-or-amount/:index', + title: 'Debt X of Y: Name of debt', + uiSchema: disputeReason.uiSchema, + schema: disputeReason.schema, + showPagePerItem: true, + arrayPath: 'selectedDebts', + }, + supportStatement: { + path: 'dispute-reason/:index', + title: 'Debt X of Y: Name of debt', + uiSchema: supportStatement.uiSchema, + schema: supportStatement.schema, + showPagePerItem: true, + arrayPath: 'selectedDebts', + }, + }, + }, + }, + // getHelp, + footerContent, +}; + +export default formConfig; diff --git a/src/applications/dispute-debt/config/prefill-transformer.js b/src/applications/dispute-debt/config/prefill-transformer.js new file mode 100644 index 000000000000..4d440ed06549 --- /dev/null +++ b/src/applications/dispute-debt/config/prefill-transformer.js @@ -0,0 +1,16 @@ +/* vets-api/config/form_profile_mappings/DISPUTE-DEBT.yml */ + +export default function prefillTransformer(pages, formData, metadata) { + const { fileNumber = '', ssn = '' } = formData?.veteranInformation || {}; + + return { + pages, + formData: { + veteranInformation: { + ssnLastFour: ssn, + vaFileLastFour: fileNumber, + }, + }, + metadata, + }; +} diff --git a/src/applications/dispute-debt/constants/index.js b/src/applications/dispute-debt/constants/index.js new file mode 100644 index 000000000000..0505b86b0837 --- /dev/null +++ b/src/applications/dispute-debt/constants/index.js @@ -0,0 +1,26 @@ +export const TITLE = 'Dispute your VA debt'; +export const SUBTITLE = 'Process to dispute debt'; + +// Date formats +export const FORMAT_YMD_DATE_FNS = 'yyyy-MM-dd'; +export const FORMAT_READABLE_DATE_FNS = 'MMMM d, yyyy'; + +// Debt deduction codes +export const deductionCodes = Object.freeze({ + '30': 'Disability compensation and pension debt', + '41': 'Chapter 34 education debt', + '44': 'Chapter 35 education debt', + '71': 'Post-9/11 GI Bill debt for books and supplies', + '72': 'Post-9/11 GI Bill debt for housing', + '74': 'Post-9/11 GI Bill debt for tuition', + '75': 'Post-9/11 GI Bill debt for tuition (school liable)', +}); + +export const DEBTS_FETCH_INITIATED = 'DEBTS_FETCH_INITIATED'; +export const DEBTS_FETCH_SUCCESS = 'DEBTS_FETCH_SUCCESS'; +export const DEBTS_FETCH_FAILURE = 'DEBTS_FETCH_FAILURE'; + +export const DEBT_TYPES = Object.freeze({ + DEBT: 'DEBT', + COPAY: 'COPAY', +}); diff --git a/src/applications/dispute-debt/containers/App.jsx b/src/applications/dispute-debt/containers/App.jsx new file mode 100644 index 000000000000..bcc224264e9d --- /dev/null +++ b/src/applications/dispute-debt/containers/App.jsx @@ -0,0 +1,45 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { isLoggedIn } from 'platform/user/selectors'; + +import RoutedSavableApp from 'platform/forms/save-in-progress/RoutedSavableApp'; +import formConfig from '../config/form'; +import { fetchDebts } from '../actions'; + +export default function App({ children, location }) { + const dispatch = useDispatch(); + const { isDebtPending } = useSelector(state => state.availableDebts); + const userLoggedIn = useSelector(state => isLoggedIn(state)); + + useEffect( + () => { + if (userLoggedIn) { + fetchDebts(dispatch); + } + }, + [dispatch, userLoggedIn], + ); + + if (isDebtPending) { + return ( + + ); + } + + return ( + + {children} + + ); +} + +App.propTypes = { + children: PropTypes.element, + isDebtPending: PropTypes.bool, + location: PropTypes.object, +}; diff --git a/src/applications/dispute-debt/containers/ConfirmationPage.jsx b/src/applications/dispute-debt/containers/ConfirmationPage.jsx new file mode 100644 index 000000000000..f878a4a4cc6b --- /dev/null +++ b/src/applications/dispute-debt/containers/ConfirmationPage.jsx @@ -0,0 +1,100 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { format, isValid } from 'date-fns'; +import { useSelector } from 'react-redux'; +import { scrollTo, waitForRenderThenFocus } from 'platform/utilities/ui'; + +export const ConfirmationPage = () => { + const alertRef = useRef(null); + const form = useSelector(state => state.form || {}); + const { submission, formId, data = {} } = form; + const { fullName } = data; + const submitDate = submission?.timestamp; + const confirmationNumber = submission?.response?.confirmationNumber; + + useEffect( + () => { + if (alertRef?.current) { + scrollTo('topScrollElement'); + waitForRenderThenFocus('h2', alertRef.current); + } + }, + [alertRef], + ); + + return ( +
    +
    + VA logo +
    + + +

    Your application has been submitted

    +
    + +

    We may contact you for more information or documents.

    +

    Please print this page for your records.

    +
    +

    + Dispute your VA debt Claim{' '} + (Form {formId}) +

    + {fullName ? ( + + for {fullName.first} {fullName.middle} {fullName.last} + {fullName.suffix ? `, ${fullName.suffix}` : null} + + ) : null} + + {confirmationNumber ? ( + <> +

    Confirmation number

    +

    {confirmationNumber}

    + + ) : null} + + {isValid(submitDate) ? ( +

    + Date submitted +
    + {format(submitDate, 'MMMM d, yyyy')} +

    + ) : null} + + +
    + + Go back to VA.gov + + + {/*
    +

    Need help?

    + +
    */} +
    + ); +}; + +ConfirmationPage.propTypes = { + form: PropTypes.shape({ + data: PropTypes.shape({ + fullName: { + first: PropTypes.string, + middle: PropTypes.string, + last: PropTypes.string, + suffix: PropTypes.string, + }, + }), + formId: PropTypes.string, + submission: PropTypes.shape({ + timestamp: PropTypes.string, + }), + }), + name: PropTypes.string, +}; + +export default ConfirmationPage; diff --git a/src/applications/dispute-debt/containers/IntroductionPage.jsx b/src/applications/dispute-debt/containers/IntroductionPage.jsx new file mode 100644 index 000000000000..91414dbaaedd --- /dev/null +++ b/src/applications/dispute-debt/containers/IntroductionPage.jsx @@ -0,0 +1,137 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { focusElement, scrollToTop } from 'platform/utilities/ui'; +import environment from 'platform/utilities/environment'; +import FormTitle from 'platform/forms-system/src/js/components/FormTitle'; +import SaveInProgressIntro from 'platform/forms/save-in-progress/SaveInProgressIntro'; +import { useSelector } from 'react-redux'; +import { isLOA3, isLoggedIn } from 'platform/user/selectors'; +import { useFeatureToggle } from 'platform/utilities/feature-toggles'; +import { TITLE, SUBTITLE } from '../constants'; + +const OMB_RES_BURDEN = 60; +const OMB_NUMBER = ''; +const OMB_EXP_DATE = '12/31/2025'; + +const ProcessList = () => { + return ( + + +

    To fill out this application, you’ll need your:

    +
      +
    • Social Security number (required)
    • +
    +

    + What if I need help filling out my application? An + accredited representative, like a Veterans Service Officer (VSO), can + help you fill out your claim.{' '} + + Get help filing your claim + +

    +
    + +

    Complete this benefits form.

    +

    + After submitting the form, you’ll get a confirmation message. You can + print this for your records. +

    +
    + +

    + We process claims within a week. If more than a week has passed since + you submitted your application and you haven’t heard back, please + don’t apply again. Call us at. +

    +
    + +

    + Once we’ve processed your claim, you’ll get a notice in the mail with + our decision. +

    +
    +
    + ); +}; + +export const IntroductionPage = props => { + const { route } = props; + const { formConfig, pageList } = route; + const { useToggleValue, TOGGLE_NAMES } = useFeatureToggle(); + const disputeDebtActive = useToggleValue(TOGGLE_NAMES.disputeDebt); + + /* Dev Note: + The following are variables for confirming LOA3 and logged in status + we can use these to display alert to direct users to complete their profile + before proceeding with the application + */ + const userLoggedIn = useSelector(state => isLoggedIn(state)); + const userIdVerified = useSelector(state => isLOA3(state)); + const showVerifyIdentify = userLoggedIn && !userIdVerified; + + useEffect(() => { + scrollToTop(); + focusElement('h1'); + }, []); + + return ( +
    + + {disputeDebtActive ? ( + <> +

    + Follow the steps below to apply for digital dispute for debts. +

    + + {showVerifyIdentify ? ( +

    Update your profile

    + ) : ( + + )} + + + ) : ( + +

    We’re sorry. This application is unavailable.

    +

    + We are currently working on this application +

    + + Go back to VA.gov + +
    + )} +
    + ); +}; + +IntroductionPage.propTypes = { + route: PropTypes.shape({ + formConfig: PropTypes.shape({ + prefillEnabled: PropTypes.bool.isRequired, + savedFormMessages: PropTypes.object.isRequired, + }).isRequired, + pageList: PropTypes.arrayOf(PropTypes.object).isRequired, + }).isRequired, + location: PropTypes.shape({ + basename: PropTypes.string, + }), +}; + +export default IntroductionPage; diff --git a/src/applications/dispute-debt/manifest.json b/src/applications/dispute-debt/manifest.json new file mode 100644 index 000000000000..9ae928d72b01 --- /dev/null +++ b/src/applications/dispute-debt/manifest.json @@ -0,0 +1,7 @@ +{ + "appName": "Dispute your VA debt", + "entryFile": "./app-entry.jsx", + "entryName": "dispute-debt", + "rootUrl": "/dispute-debt", + "productId": "908a02e6-c63d-42e0-87f9-ed208e47034e" +} diff --git a/src/applications/dispute-debt/pages/debtSelection.js b/src/applications/dispute-debt/pages/debtSelection.js new file mode 100644 index 000000000000..9d0a9a4f5db2 --- /dev/null +++ b/src/applications/dispute-debt/pages/debtSelection.js @@ -0,0 +1,41 @@ +import React from 'react'; +import DebtSelection from '../components/DebtSelection'; + +const debtSelection = { + uiSchema: { + 'ui:title': () => ( + <> + +

    What debt do you need help with?

    +
    + + ), + selectedDebts: { + 'ui:field': DebtSelection, + 'ui:options': { + hideOnReview: true, + }, + 'ui:validations': [ + (errors, debts) => { + if (!debts.length) { + errors.addError('Please select at least one debt.'); + } + }, + ], + }, + }, + schema: { + type: 'object', + properties: { + selectedDebts: { + type: 'array', + items: { + type: 'object', + properties: {}, + }, + }, + }, + }, +}; + +export default debtSelection; diff --git a/src/applications/dispute-debt/pages/disputeReason.js b/src/applications/dispute-debt/pages/disputeReason.js new file mode 100644 index 000000000000..c0c21ecbebf8 --- /dev/null +++ b/src/applications/dispute-debt/pages/disputeReason.js @@ -0,0 +1,50 @@ +import VaRadioField from 'platform/forms-system/src/js/web-component-fields/VaRadioField'; + +const EXISTENCE = `I don't think I owe this debt to VA`; +const AMOUNT = `I don’t think the amount is correct on this debt`; + +const disputeReason = { + uiSchema: { + selectedDebts: { + items: { + 'ui:title': 'Need Debt X of Y: Name of debt', + disputeReason: { + 'ui:title': 'Select the reason you’re disputing this debt.', + 'ui:webComponentField': VaRadioField, + 'ui:required': () => true, + 'ui:options': { + labels: { + // eslint-disable-next-line object-shorthand + EXISTENCE: EXISTENCE, + // eslint-disable-next-line object-shorthand + AMOUNT: AMOUNT, + }, + }, + 'ui:errorMessages': { + required: 'Please select a reason for disputing this debt', + }, + }, + }, + }, + }, + schema: { + type: 'object', + properties: { + selectedDebts: { + type: 'array', + items: { + type: 'object', + properties: { + disputeReason: { + type: 'string', + enum: [EXISTENCE, AMOUNT], + }, + }, + required: ['disputeReason'], + }, + }, + }, + }, +}; + +export default disputeReason; diff --git a/src/applications/dispute-debt/pages/index.js b/src/applications/dispute-debt/pages/index.js new file mode 100644 index 000000000000..4b11fb008765 --- /dev/null +++ b/src/applications/dispute-debt/pages/index.js @@ -0,0 +1,6 @@ +import veteranInformation from './veteranInformation'; +import debtSelection from './debtSelection'; +import disputeReason from './disputeReason'; +import supportStatement from './supportStatement'; + +export { veteranInformation, debtSelection, disputeReason, supportStatement }; diff --git a/src/applications/dispute-debt/pages/supportStatement.js b/src/applications/dispute-debt/pages/supportStatement.js new file mode 100644 index 000000000000..ccf82b03828f --- /dev/null +++ b/src/applications/dispute-debt/pages/supportStatement.js @@ -0,0 +1,38 @@ +import VaTextareaField from 'platform/forms-system/src/js/web-component-fields/VaTextareaField'; + +const supportStatement = { + uiSchema: { + selectedDebts: { + items: { + 'ui:title': 'Need Debt X of Y: Name of debt', + supportStatement: { + 'ui:title': `Tell us why you're disputing this debt: `, + 'ui:webComponentField': VaTextareaField, + 'ui:required': () => true, + 'ui:errorMessages': { + required: 'Please provide a response', + }, + }, + }, + }, + }, + schema: { + type: 'object', + properties: { + selectedDebts: { + type: 'array', + items: { + type: 'object', + properties: { + supportStatement: { + type: 'string', + }, + }, + required: ['supportStatement'], + }, + }, + }, + }, +}; + +export default supportStatement; diff --git a/src/applications/dispute-debt/pages/veteranInformation.js b/src/applications/dispute-debt/pages/veteranInformation.js new file mode 100644 index 000000000000..8d324790c900 --- /dev/null +++ b/src/applications/dispute-debt/pages/veteranInformation.js @@ -0,0 +1,16 @@ +import VeteranInformation from '../components/VeteranInformation'; + +const veteranInformation = { + uiSchema: { + 'ui:description': VeteranInformation, + 'ui:options': { + hideOnReview: true, + }, + }, + schema: { + type: 'object', + properties: {}, + }, +}; + +export default veteranInformation; diff --git a/src/applications/dispute-debt/reducers/index.js b/src/applications/dispute-debt/reducers/index.js new file mode 100644 index 000000000000..7f1bd810c212 --- /dev/null +++ b/src/applications/dispute-debt/reducers/index.js @@ -0,0 +1,44 @@ +import { createSaveInProgressFormReducer } from 'platform/forms/save-in-progress/reducers'; +import formConfig from '../config/form'; + +import { + DEBTS_FETCH_SUCCESS, + DEBTS_FETCH_FAILURE, + DEBTS_FETCH_INITIATED, +} from '../constants'; + +const initialState = { + isDebtError: false, + debtError: '', + availableDebts: [], +}; + +const availableDebts = (state = initialState, action) => { + switch (action.type) { + case DEBTS_FETCH_INITIATED: + return { + ...state, + isDebtPending: true, + }; + case DEBTS_FETCH_SUCCESS: + return { + ...state, + availableDebts: action.debts, + isDebtPending: false, + }; + case DEBTS_FETCH_FAILURE: + return { + ...state, + isDebtError: true, + debtError: action.error, + isDebtPending: false, + }; + default: + return state; + } +}; + +export default { + form: createSaveInProgressFormReducer(formConfig), + availableDebts, +}; diff --git a/src/applications/dispute-debt/routes.jsx b/src/applications/dispute-debt/routes.jsx new file mode 100644 index 000000000000..8b56e82cbf9a --- /dev/null +++ b/src/applications/dispute-debt/routes.jsx @@ -0,0 +1,12 @@ +import { createRoutesWithSaveInProgress } from 'platform/forms/save-in-progress/helpers'; +import formConfig from './config/form'; +import App from './containers/App'; + +const route = { + path: '/', + component: App, + indexRoute: { onEnter: (nextState, replace) => replace('/introduction') }, + childRoutes: createRoutesWithSaveInProgress(formConfig), +}; + +export default route; diff --git a/src/applications/dispute-debt/sass/dispute-debt.scss b/src/applications/dispute-debt/sass/dispute-debt.scss new file mode 100644 index 000000000000..80433ce82d02 --- /dev/null +++ b/src/applications/dispute-debt/sass/dispute-debt.scss @@ -0,0 +1,6 @@ +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-process-list"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-form-process"; +@import "../../../platform/forms/sass/m-schemaform"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-modal"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-omb-info"; +@import "../../../platform/forms/sass/m-form-confirmation"; diff --git a/src/applications/dispute-debt/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/dispute-debt/tests/containers/ConfirmationPage.unit.spec.jsx new file mode 100644 index 000000000000..5bfb439956cb --- /dev/null +++ b/src/applications/dispute-debt/tests/containers/ConfirmationPage.unit.spec.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import ConfirmationPage from '../../containers/ConfirmationPage'; + +const storeBase = { + form: { + formId: formConfig.formId, + submission: { + response: { + confirmationNumber: '123456', + }, + timestamp: Date.now(), + }, + data: { + fullName: { + first: 'John', + middle: '', + last: 'Doe', + }, + }, + }, +}; + +describe('Confirmation page', () => { + const middleware = [thunk]; + const mockStore = configureStore(middleware); + + it('it should show status success and the correct name of person', () => { + const { container, getByText } = render( + + + , + ); + expect(container.querySelector('va-alert')).to.have.attr( + 'status', + 'success', + ); + getByText(/John Doe/); + }); +}); diff --git a/src/applications/dispute-debt/tests/containers/IntroductionPage.unit.spec.jsx b/src/applications/dispute-debt/tests/containers/IntroductionPage.unit.spec.jsx new file mode 100644 index 000000000000..96236de1f574 --- /dev/null +++ b/src/applications/dispute-debt/tests/containers/IntroductionPage.unit.spec.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import IntroductionPage from '../../containers/IntroductionPage'; + +const props = { + route: { + path: 'introduction', + pageList: [], + formConfig, + }, + userLoggedIn: false, + userIdVerified: true, +}; + +const mockStore = { + getState: () => ({ + user: { + login: { + currentlyLoggedIn: false, + }, + profile: { + savedForms: [], + prefillsAvailable: [], + loa: { + current: 3, + highest: 3, + }, + verified: true, + dob: '2000-01-01', + claims: { + appeals: false, + }, + }, + }, + form: { + formId: formConfig.formId, + loadedStatus: 'success', + savedStatus: '', + loadedData: { + metadata: {}, + }, + data: {}, + }, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: { get() {} }, + dismissedDowntimeWarnings: [], + }, + }), + subscribe: () => {}, + dispatch: () => {}, +}; + +describe('IntroductionPage', () => { + it('should render', () => { + const { container } = render( + + + , + ); + expect(container).to.exist; + }); +}); diff --git a/src/applications/dispute-debt/tests/dispute-debt.cypress.spec.js b/src/applications/dispute-debt/tests/dispute-debt.cypress.spec.js new file mode 100644 index 000000000000..906270a8aace --- /dev/null +++ b/src/applications/dispute-debt/tests/dispute-debt.cypress.spec.js @@ -0,0 +1,37 @@ +import path from 'path'; +import testForm from 'platform/testing/e2e/cypress/support/form-tester'; +import { createTestConfig } from 'platform/testing/e2e/cypress/support/form-tester/utilities'; +import mockUser from './fixtures/mocks/user.json'; +import formConfig from '../config/form'; +import manifest from '../manifest.json'; + +const testConfig = createTestConfig( + { + dataPrefix: 'data', + dataDir: path.join(__dirname, 'fixtures', 'data'), + dataSets: ['minimal-test'], + pageHooks: { + introduction: ({ afterHook }) => { + afterHook(() => { + cy.findAllByText(/^start/i, { selector: 'a[href="#start"]' }) + .last() + .click({ force: true }); + }); + }, + }, + + setupPerTest: () => { + cy.intercept('GET', '/v0/user', mockUser); + cy.intercept('POST', formConfig.submitUrl, { status: 200 }); + cy.login(mockUser); + }, + + // Skip tests in CI until the form is released. + // Remove this setting when the form has a content page in production. + skip: Cypress.env('CI'), + }, + manifest, + formConfig, +); + +testForm(testConfig); diff --git a/src/applications/dispute-debt/tests/fixtures/data/minimal-test.json b/src/applications/dispute-debt/tests/fixtures/data/minimal-test.json new file mode 100644 index 000000000000..b57dee5b87cf --- /dev/null +++ b/src/applications/dispute-debt/tests/fixtures/data/minimal-test.json @@ -0,0 +1,9 @@ +{ + "data": { + "fullName": { + "first": "John", + "last": "Doe" + }, + "dateOfBirth": "1980-01-01" + } +} diff --git a/src/applications/dispute-debt/tests/fixtures/mocks/local-mock-responses.js b/src/applications/dispute-debt/tests/fixtures/mocks/local-mock-responses.js new file mode 100644 index 000000000000..e6c6e27268da --- /dev/null +++ b/src/applications/dispute-debt/tests/fixtures/mocks/local-mock-responses.js @@ -0,0 +1,8 @@ +// yarn mock-api --responses ./src/applications/{application}/tests/e2e/fixtures/mocks/local-mock-responses.js +const mockUser = require('./user.json'); + +const responses = { + 'GET /v0/user': mockUser, +}; + +module.exports = responses; diff --git a/src/applications/dispute-debt/tests/fixtures/mocks/user.json b/src/applications/dispute-debt/tests/fixtures/mocks/user.json new file mode 100644 index 000000000000..c707324756e1 --- /dev/null +++ b/src/applications/dispute-debt/tests/fixtures/mocks/user.json @@ -0,0 +1,56 @@ +{ + "data": { + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "john.doe@example.com", + "loa": { "current": 3 }, + "first_name": "John", + "middle_name": "", + "last_name": "Doe", + "gender": "M", + "birth_date": "1985-01-01", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true, + "served_in_military": true + }, + "in_progress_forms": [], + "prefills_available": [], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "form526", + "user-profile", + "health-records", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "birth_date": "19850101", + "family_name": "Doe", + "gender": "M", + "given_names": ["John", ""], + "active_status": "active", + "facilities": [ + { + "facility_id": "983", + "is_cerner": false + }, + { + "facility_id": "984", + "is_cerner": false + } + ] + } + } + }, + "meta": { "errors": null } +} diff --git a/src/applications/dispute-debt/utils/index.js b/src/applications/dispute-debt/utils/index.js new file mode 100644 index 000000000000..c16e594d30a4 --- /dev/null +++ b/src/applications/dispute-debt/utils/index.js @@ -0,0 +1,50 @@ +import { addDays, parse, parseISO, isValid } from 'date-fns'; +import { formatDateLong } from 'platform/utilities/date'; +/** + * parseDateToDateObj from ISO8601 or JS number date (not unix time) + * @param {string, number, Date} date - date to format + * @returns {dateObj|null} date object + */ +export const parseDateToDateObj = (date, template) => { + let newDate = date; + if (typeof date === 'string') { + if (date.includes('T')) { + newDate = parseISO((date || '').split('T')[0]); + } else if (template) { + newDate = parse(date, template, new Date()); + } + } else if (date instanceof Date && isValid(date)) { + // Remove timezone offset - the only time we pass in a date object is for + // unit tests (see https://stackoverflow.com/a/67599505) + newDate.setMinutes(newDate.getMinutes() + newDate.getTimezoneOffset()); + } + return isValid(newDate) ? newDate : null; +}; + +export const endDate = (date, days) => { + return isValid(new Date(date)) + ? formatDateLong(addDays(new Date(date), days)) + : ''; +}; + +export const currency = amount => { + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + }); + const value = + typeof amount === 'number' + ? amount + : parseFloat(amount?.replaceAll(/[^0-9.-]/g, '') ?? 0); + return formatter.format(value); +}; + +export const setFocus = selector => { + const el = + typeof selector === 'string' ? document.querySelector(selector) : selector; + if (el) { + el.setAttribute('tabIndex', -1); + el.focus(); + } +}; diff --git a/src/applications/edu-benefits/10215/components/GetFormHelp.jsx b/src/applications/edu-benefits/10215/components/GetFormHelp.jsx new file mode 100644 index 000000000000..68142c59d7e0 --- /dev/null +++ b/src/applications/edu-benefits/10215/components/GetFormHelp.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; + +const GetFormHelp = () => ( + <> +

    + If you have trouble using this online form, call us at{' '} + . +

    +

    + + If you need help gathering your information or filling out your form,{' '} + + +

    + +); + +export default GetFormHelp; diff --git a/src/applications/edu-benefits/10215/config/form.js b/src/applications/edu-benefits/10215/config/form.js index 034a7c015bd9..267cc24692d2 100644 --- a/src/applications/edu-benefits/10215/config/form.js +++ b/src/applications/edu-benefits/10215/config/form.js @@ -1,10 +1,17 @@ import React from 'react'; + import { arrayBuilderPages } from '~/platform/forms-system/src/js/patterns/array-builder'; +import FormFooter from 'platform/forms/components/FormFooter'; + import commonDefinitions from 'vets-json-schema/dist/definitions.json'; + import manifest from '../manifest.json'; import transform from './transform'; import { getFTECalcs } from '../helpers'; +// Components +import GetFormHelp from '../components/GetFormHelp'; + // Pages import IntroductionPage from '../containers/IntroductionPage'; import ConfirmationPage from '../containers/ConfirmationPage'; @@ -61,6 +68,8 @@ const formConfig = { 22-10215)

    ), + footerContent: FormFooter, + getHelp: GetFormHelp, defaultDefinitions: { date, }, diff --git a/src/applications/edu-benefits/10215/containers/ConfirmationPage.jsx b/src/applications/edu-benefits/10215/containers/ConfirmationPage.jsx index 08bf4e7623af..b2cdc4e3851c 100644 --- a/src/applications/edu-benefits/10215/containers/ConfirmationPage.jsx +++ b/src/applications/edu-benefits/10215/containers/ConfirmationPage.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { ConfirmationView } from 'platform/forms-system/src/js/components/ConfirmationView'; +import GetFormHelp from '../components/GetFormHelp'; const childContent = (
    @@ -90,7 +91,7 @@ export const ConfirmationPage = props => { pdfUrl={submission?.response?.pdfUrl} > {childContent} - + } /> ); }; diff --git a/src/applications/edu-benefits/10215/sass/10215-edu-benefits.scss b/src/applications/edu-benefits/10215/sass/10215-edu-benefits.scss index d2bc1e353aa9..73073cb374ce 100644 --- a/src/applications/edu-benefits/10215/sass/10215-edu-benefits.scss +++ b/src/applications/edu-benefits/10215/sass/10215-edu-benefits.scss @@ -29,9 +29,11 @@ fieldset.schemaform-field-template.schemaform-first-field { } } } -.list-style{ - list-style-type: disc; + +.list-style { + list-style-type: disc; } -.bottom-border{ + +.bottom-border { border-bottom: 1px solid var(--vads-color-base-lighter); -} \ No newline at end of file +} diff --git a/src/applications/edu-benefits/10215/tests/components/GetFormHelp.unit.spec.jsx b/src/applications/edu-benefits/10215/tests/components/GetFormHelp.unit.spec.jsx new file mode 100644 index 000000000000..bb000c587673 --- /dev/null +++ b/src/applications/edu-benefits/10215/tests/components/GetFormHelp.unit.spec.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; + +import GetFormHelp from '../../components/GetFormHelp'; + +describe('', () => { + it('should render without crashing', () => { + const wrapper = shallow(); + + expect(wrapper.exists()).to.be.ok; + + wrapper.unmount(); + }); + + it('should contain va-telephone component', () => { + const wrapper = shallow(); + + expect(wrapper.find('va-telephone').length).to.equal(1); + expect(wrapper.find('va-telephone').props().contact).to.contain( + '8884424551', + ); + expect(wrapper.find('va-telephone').props().international).to.be.true; + expect(wrapper.find('va-telephone').props()['not-clickable']).to.be.true; + + wrapper.unmount(); + }); + + it('should contain va-link component', () => { + const wrapper = shallow(); + + expect(wrapper.find('va-link').length).to.equal(1); + expect(wrapper.find('va-link').props().text).to.contain( + 'visit Education Liaison Representatives - Education and Training.', + ); + expect(wrapper.find('va-link').props().href).to.contain( + 'https://www.benefits.va.gov/gibill/resources/education_resources/school_certifying_officials/elr.asp', + ); + + wrapper.unmount(); + }); +}); diff --git a/src/applications/edu-benefits/10216/components/GetFormHelp.jsx b/src/applications/edu-benefits/10216/components/GetFormHelp.jsx index 7caa3d279d37..68142c59d7e0 100644 --- a/src/applications/edu-benefits/10216/components/GetFormHelp.jsx +++ b/src/applications/edu-benefits/10216/components/GetFormHelp.jsx @@ -1,29 +1,22 @@ import React from 'react'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; -export function GetFormHelp() { - return ( - <> -

    - If you have trouble using this online form, call us at{' '} - ( - - ). -
    - We’re here 24/7. -

    -

    - - If you need help gathering your information or filling out your form, - {' '} -
    - contact a local Veterans Service Organization (VSO). -

    +const GetFormHelp = () => ( + <> +

    + If you have trouble using this online form, call us at{' '} + . +

    +

    + + If you need help gathering your information or filling out your form,{' '} + - - ); -} +

    + +); export default GetFormHelp; diff --git a/src/applications/edu-benefits/10216/config/form.js b/src/applications/edu-benefits/10216/config/form.js index 8bd995b99fb2..16e8663e05a6 100644 --- a/src/applications/edu-benefits/10216/config/form.js +++ b/src/applications/edu-benefits/10216/config/form.js @@ -1,31 +1,23 @@ -// In a real app this would not be imported directly; instead the schema you -// imported above would import and use these common definitions: import React from 'react'; -import commonDefinitions from 'vets-json-schema/dist/definitions.json'; -// Example of an imported schema: -// In a real app this would be imported from `vets-json-schema`: -// import fullSchema from 'vets-json-schema/dist/22-10216-schema.json'; -// import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/currentOrPastDate'; - -// import fullSchema from 'vets-json-schema/dist/22-10216-schema.json'; - -import manifest from '../manifest.json'; - -import IntroductionPage from '../containers/IntroductionPage'; -import ConfirmationPage from '../containers/ConfirmationPage'; -// const { } = fullSchema.properties; +import FormFooter from 'platform/forms/components/FormFooter'; -// const { } = fullSchema.definitions; +import commonDefinitions from 'vets-json-schema/dist/definitions.json'; -// pages -import studentRatioCalc from '../pages/studentRatioCalc'; +import manifest from '../manifest.json'; import { validateFacilityCode } from '../utilities'; -import Alert from '../components/Alert'; -import InstitutionDetails from '../pages/institutionDetails'; import { transform } from './submit-transformer'; + +// Components +import Alert from '../components/Alert'; +import GetFormHelp from '../components/GetFormHelp'; import SubmissionInstructions from '../components/SubmissionInstructions'; -// import submitForm from './submitForm'; + +// Pages +import ConfirmationPage from '../containers/ConfirmationPage'; +import IntroductionPage from '../containers/IntroductionPage'; +import InstitutionDetails from '../pages/institutionDetails'; +import studentRatioCalc from '../pages/studentRatioCalc'; const { date, dateRange } = commonDefinitions; @@ -66,6 +58,8 @@ const formConfig = { }, title: 'Request exemption from the 85/15 Rule reporting requirements', subTitle, + footerContent: FormFooter, + getHelp: GetFormHelp, defaultDefinitions: { date, dateRange, diff --git a/src/applications/edu-benefits/10216/containers/ConfirmationPage.jsx b/src/applications/edu-benefits/10216/containers/ConfirmationPage.jsx index 86bbb5d6b662..4ef6c947a758 100644 --- a/src/applications/edu-benefits/10216/containers/ConfirmationPage.jsx +++ b/src/applications/edu-benefits/10216/containers/ConfirmationPage.jsx @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect, useSelector } from 'react-redux'; import { ConfirmationView } from 'platform/forms-system/src/js/components/ConfirmationView'; -import { GetFormHelp } from '../components/GetFormHelp'; import Alert from '../components/Alert'; +import GetFormHelp from '../components/GetFormHelp'; export const ConfirmationPage = props => { const form = useSelector(state => state.form || {}); @@ -91,11 +91,7 @@ export const ConfirmationPage = props => { pdfUrl={submission?.response?.pdfUrl} > {childContent} - -
    - -
    -
    + } /> ); }; diff --git a/src/applications/edu-benefits/10216/containers/IntroductionPage.jsx b/src/applications/edu-benefits/10216/containers/IntroductionPage.jsx index 3049e3617fb7..e1cd54a9d138 100644 --- a/src/applications/edu-benefits/10216/containers/IntroductionPage.jsx +++ b/src/applications/edu-benefits/10216/containers/IntroductionPage.jsx @@ -130,7 +130,7 @@ const IntroductionPage = ({ router }) => { />{' '} of jurisdiction.

    -

    +

    Note: The numbers on the instructions match the item numbers on the form. Items not shown are self-explanatory.
      @@ -141,7 +141,7 @@ const IntroductionPage = ({ router }) => {
    • Provide your institution’s VA assigned facility code.
    -

    +

    Note: Separate exemption requests (and calculations) are required for the main campus and any branch diff --git a/src/applications/edu-benefits/10216/sass/10216-edu-benefits.scss b/src/applications/edu-benefits/10216/sass/10216-edu-benefits.scss index 2e3907217d43..9d072f0495cc 100644 --- a/src/applications/edu-benefits/10216/sass/10216-edu-benefits.scss +++ b/src/applications/edu-benefits/10216/sass/10216-edu-benefits.scss @@ -6,31 +6,40 @@ @import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-omb-info"; @import "../../../../platform/forms/sass/m-form-confirmation"; - .schemaform-field-template { - &:has(label[id="root_studentRatioCalcChapter_studentPercentageCalc_calculatedPercentage-label"]) { - display: none; - } + &:has(label[id="root_studentRatioCalcChapter_studentPercentageCalc_calculatedPercentage-label"]) { + display: none; + } } + va-accordion-item[data-chapter="submissionInstructionsChapter"] { - display: none; + display: none; } @media (max-width: $small-screen) { - .form-22-10216-container{ - padding-inline: 0.5rem; - } - .form-22-10216-container .schemaform-buttons, - .form-22-10216-container .form-progress-buttons { - display: flex; - flex-direction: column !important; - - div { - width: 100%; - } - - div:first-of-type { - order: 2; - } + body[data-location="confirmation"] .form-22-10216-container { + padding-inline: 1rem; + } + + .form-22-10216-container { + padding-inline: 0.5rem; + } + + .form-22-10216-container .schemaform-buttons, + .form-22-10216-container .form-progress-buttons { + display: flex; + flex-direction: column !important; + + div { + width: 100%; + } + + div:first-of-type { + order: 2; } - } \ No newline at end of file + } + + va-need-help { + margin: 0 0.5rem; + } +} diff --git a/src/applications/edu-benefits/10216/tests/components/BreadcrumbAboutPage.unit.spec.jsx b/src/applications/edu-benefits/10216/tests/components/BreadcrumbAboutPage.unit.spec.jsx new file mode 100644 index 000000000000..b7621b7b5bab --- /dev/null +++ b/src/applications/edu-benefits/10216/tests/components/BreadcrumbAboutPage.unit.spec.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import BreadcrumbAboutPage from '../../components/BreadcrumbAboutPage'; + +describe('BreadcrumbAboutPage', () => { + it('renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper.exists()).to.be.ok; + wrapper.unmount(); + }); +}); diff --git a/src/applications/edu-benefits/10216/tests/components/BreadcrumbUsedInForm.unit.spec.jsx b/src/applications/edu-benefits/10216/tests/components/BreadcrumbUsedInForm.unit.spec.jsx new file mode 100644 index 000000000000..6207c143c7ca --- /dev/null +++ b/src/applications/edu-benefits/10216/tests/components/BreadcrumbUsedInForm.unit.spec.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import BreadcrumbUsedInForm from '../../components/BreadcrumbUsedInForm'; + +describe('BreadcrumbUsedInForm', () => { + it('renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper.exists()).to.be.ok; + wrapper.unmount(); + }); +}); diff --git a/src/applications/edu-benefits/10216/tests/components/GetFormHelp.unit.spec.jsx b/src/applications/edu-benefits/10216/tests/components/GetFormHelp.unit.spec.jsx new file mode 100644 index 000000000000..bb000c587673 --- /dev/null +++ b/src/applications/edu-benefits/10216/tests/components/GetFormHelp.unit.spec.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; + +import GetFormHelp from '../../components/GetFormHelp'; + +describe('', () => { + it('should render without crashing', () => { + const wrapper = shallow(); + + expect(wrapper.exists()).to.be.ok; + + wrapper.unmount(); + }); + + it('should contain va-telephone component', () => { + const wrapper = shallow(); + + expect(wrapper.find('va-telephone').length).to.equal(1); + expect(wrapper.find('va-telephone').props().contact).to.contain( + '8884424551', + ); + expect(wrapper.find('va-telephone').props().international).to.be.true; + expect(wrapper.find('va-telephone').props()['not-clickable']).to.be.true; + + wrapper.unmount(); + }); + + it('should contain va-link component', () => { + const wrapper = shallow(); + + expect(wrapper.find('va-link').length).to.equal(1); + expect(wrapper.find('va-link').props().text).to.contain( + 'visit Education Liaison Representatives - Education and Training.', + ); + expect(wrapper.find('va-link').props().href).to.contain( + 'https://www.benefits.va.gov/gibill/resources/education_resources/school_certifying_officials/elr.asp', + ); + + wrapper.unmount(); + }); +}); diff --git a/src/applications/financial-status-report/components/debtsAndCopays/AvailableDebtsAndCopays.jsx b/src/applications/financial-status-report/components/debtsAndCopays/AvailableDebtsAndCopays.jsx index d3acdfc43523..b40eeb39c581 100644 --- a/src/applications/financial-status-report/components/debtsAndCopays/AvailableDebtsAndCopays.jsx +++ b/src/applications/financial-status-report/components/debtsAndCopays/AvailableDebtsAndCopays.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; +import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; import PropTypes from 'prop-types'; import { uniqBy, head } from 'lodash'; import { isValid } from 'date-fns'; @@ -212,9 +213,9 @@ const AvailableDebtsAndCopays = ({ formContext }) => { )} If you received a letter about a VA benefit debt that isn’t listed here, - call us at (or{' '} - from overseas). - We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET. + call us at (or{' '} + from + overseas). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

    ); diff --git a/src/applications/financial-status-report/components/shared/GetFormHelp.jsx b/src/applications/financial-status-report/components/shared/GetFormHelp.jsx index cb10a69fcad7..91e0b8606fff 100644 --- a/src/applications/financial-status-report/components/shared/GetFormHelp.jsx +++ b/src/applications/financial-status-report/components/shared/GetFormHelp.jsx @@ -6,7 +6,7 @@ const GetFormHelp = () => { <>

    If you have trouble using this online form, call our MyVA411 main - information line at ( + information line at ( ).

    @@ -20,13 +20,9 @@ const GetFormHelp = () => {

    If you have questions about your benefit overpayments, call us at{' '} - (or{' '} - {' '} - from overseas). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. - ET. + (or{' '} + from + overseas). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET.

    If you have questions about your copay bills, call us at{' '} diff --git a/src/applications/financial-status-report/constants/diary-codes/index.js b/src/applications/financial-status-report/constants/diary-codes/index.js index 506546927320..df6957b47d6c 100644 --- a/src/applications/financial-status-report/constants/diary-codes/index.js +++ b/src/applications/financial-status-report/constants/diary-codes/index.js @@ -5,12 +5,9 @@ import { endDate } from '../../utils/helpers'; const ContactDMC = () => ( - (or{' '} - {' '} - from overseas) + (or{' '} + from + overseas) ); diff --git a/src/applications/financial-status-report/constants/index.js b/src/applications/financial-status-report/constants/index.js index 978d9807688d..898d86d4111f 100644 --- a/src/applications/financial-status-report/constants/index.js +++ b/src/applications/financial-status-report/constants/index.js @@ -3,8 +3,12 @@ import constants from 'vets-json-schema/dist/constants.json'; export const COUNTRY_LABELS = constants.countries.map(country => country.label); export const COUNTRY_VALUES = constants.countries.map(country => country.value); -export const STATE_LABELS = constants.pciuStates.map(state => state.label); -export const STATE_VALUES = constants.pciuStates.map(state => state.value); +export const STATE_LABELS = constants.formProfileStates.map( + state => state.label, +); +export const STATE_VALUES = constants.formProfileStates.map( + state => state.value, +); export const MILITARY_CITY_CODES = ['APO', 'DPO', 'FPO']; export const MILITARY_STATE_CODES = ['AA', 'AE', 'AP']; diff --git a/src/applications/financial-status-report/wizard/components/Contacts.jsx b/src/applications/financial-status-report/wizard/components/Contacts.jsx index c6fa85973909..a2792e0cf317 100644 --- a/src/applications/financial-status-report/wizard/components/Contacts.jsx +++ b/src/applications/financial-status-report/wizard/components/Contacts.jsx @@ -4,14 +4,10 @@ import { VaTelephone } from '@department-of-veterans-affairs/component-library/d const ContactDMC = () => ( <> - Call us at (or{' '} - {' '} - from overseas). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET. - If you have hearing loss, call ( + Call us at (or{' '} + from + overseas). We’re here Monday through Friday, 7:30 a.m. to 7:00 p.m. ET. If + you have hearing loss, call ( ). ); diff --git a/src/applications/gi/components/LicenseCertificationAdminInfo.jsx b/src/applications/gi/components/LicenseCertificationAdminInfo.jsx index b03a50aa8fb3..5f3bfe86708f 100644 --- a/src/applications/gi/components/LicenseCertificationAdminInfo.jsx +++ b/src/applications/gi/components/LicenseCertificationAdminInfo.jsx @@ -1,24 +1,39 @@ +import { + VaIcon, + VaLink, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import React from 'react'; function LicenseCertificationAdminInfo({ institution }) { const { name, mailingAddress } = institution; return ( -

    -

    Admin Info

    +
    +

    Admin Info

    +
    + +

    {name}

    +
    +

    The following is the headquarters address.

    +

    - {name}
    -
    {mailingAddress.address1}
    {mailingAddress.city}, {mailingAddress.state} {mailingAddress.zip}

    - -

    - Fill out the form Request for Reimbursement of Licensing or - Certification Test Fees. -

    -
    {' '} +

    + Print and fill out form Request for Reimbursement of Licensing or + Certification Test Fees. Send the completed application to the Regional + Processing Office for your region listed in the form. +

    +
    ); } diff --git a/src/applications/gi/components/LicenseCertificationAlert.jsx b/src/applications/gi/components/LicenseCertificationAlert.jsx index 1d03bbd0ba91..f92c1391506e 100644 --- a/src/applications/gi/components/LicenseCertificationAlert.jsx +++ b/src/applications/gi/components/LicenseCertificationAlert.jsx @@ -1,3 +1,4 @@ +import { VaAlert } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import React from 'react'; function LicenseCertificationAlert({ @@ -10,8 +11,8 @@ function LicenseCertificationAlert({ type, }) { return ( - {changeStateAlert && - `The state field has been updated to ${state} becuase + `The state field has been updated to ${state} because the ${name} ${ type === 'prep' ? 'prep course' : type } is specific to that state.`} @@ -29,7 +30,7 @@ function LicenseCertificationAlert({ `State options have been changed to reflect only those states where ${name} is available`} {changeStateToAllAlert && `Certifications are nationwide. State does not apply`} - + ); } diff --git a/src/applications/gi/components/LicenseCertificationKeywordSearch.jsx b/src/applications/gi/components/LicenseCertificationKeywordSearch.jsx index f31ce80c7c4a..636007a08a10 100644 --- a/src/applications/gi/components/LicenseCertificationKeywordSearch.jsx +++ b/src/applications/gi/components/LicenseCertificationKeywordSearch.jsx @@ -3,6 +3,7 @@ import Downshift from 'downshift'; import classNames from 'classnames'; import PropTypes from 'prop-types'; +import { VaAdditionalInfo } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; export default function LicenseCertificationKeywordSearch({ inputValue, @@ -56,6 +57,16 @@ export default function LicenseCertificationKeywordSearch({ > License/Certification Name +
    + + Using more specific keywords can help narrow down your search + results. For example, searching for "Microsoft Azure" will give + you more targeted results than searching for only "Microsoft." + +
    { - return { optionValue: state[0], optionLabel: state[1] }; -}); - -export const updateDropdowns = ( - category = 'all', - location = 'all', - multiples = [], -) => { - const initialDropdowns = [ - { - label: 'category', - options: [ - { optionValue: 'all', optionLabel: 'All' }, - { optionValue: 'License', optionLabel: 'License' }, - { - optionValue: 'Certification', - optionLabel: 'Certification', - }, - { - optionValue: 'Prep Course', - optionLabel: 'Prep Course', - }, - ], - alt: 'category type', - current: { optionValue: 'all', optionLabel: 'All' }, - }, - { - label: 'state', - options: - multiples.length === 0 - ? [{ optionValue: 'all', optionLabel: 'All' }, ...mappedStates] - : [ - { optionValue: 'all', optionLabel: 'All' }, - ...mappedStates.filter(mappedState => - multiples.find( - multiple => multiple.state === mappedState.optionValue, - ), - ), - ], - alt: 'state', - current: { optionValue: 'all', optionLabel: 'All' }, - }, - ]; - - return initialDropdowns.map(dropdown => { - if (dropdown.label === 'category') { - return { - ...dropdown, - current: dropdown.options.find( - option => option.optionValue === category, - ), - }; - } - - if (dropdown.label === 'state') { - return { - ...dropdown, - current: dropdown.options.find( - option => option.optionValue === location, - ) ?? { ...dropdown.current }, - }; - } - - return dropdown; - }); -}; - -export const showMultipleNames = (suggestions, nameInput) => { - return suggestions.filter( - suggestion => suggestion.lacNm.toLowerCase() === nameInput?.toLowerCase(), - ); -}; - -export const categoryCheck = type => { - if (type === 'License') { - return true; - } - if (type === 'Prep Course') return true; - - return false; -}; - -export const checkAlert = (type, multiples, currentLocation, newLocation) => { - if (multiples.length > 1 && type !== 'Certification') { - return true; - } - - if (categoryCheck(type) && currentLocation !== newLocation) { - return true; - } - - if (type === 'Certification' && currentLocation !== 'all') { - return true; - } - - return false; -}; - export default function LicenseCertificationSearchForm({ suggestions, handleSearch, @@ -296,7 +201,7 @@ export default function LicenseCertificationSearchForm({ value={categoryDropdown.current.optionValue} onChange={handleChange} alt={categoryDropdown.alt} - selectClassName="lc-dropdown-filter" + selectClassName="dropdown-filter" required={categoryDropdown.label === 'category'} /> @@ -309,7 +214,7 @@ export default function LicenseCertificationSearchForm({ value={locationDropdown.current.optionValue ?? 'all'} onChange={handleChange} alt={locationDropdown.alt} - selectClassName="lc-dropdown-filter" + selectClassName="dropdown-filter" required={locationDropdown.label === 'category'} > {showAlert ? ( @@ -347,8 +252,9 @@ export default function LicenseCertificationSearchForm({ />
    -
    +
    handleSearch( @@ -360,7 +266,7 @@ export default function LicenseCertificationSearchForm({ /> handleReset(() => { diff --git a/src/applications/gi/components/LicenseCertificationTestInfo.jsx b/src/applications/gi/components/LicenseCertificationTestInfo.jsx index 6e00c8654a32..ac57b73ecc75 100644 --- a/src/applications/gi/components/LicenseCertificationTestInfo.jsx +++ b/src/applications/gi/components/LicenseCertificationTestInfo.jsx @@ -3,21 +3,25 @@ import { formatCurrency } from '../utils/helpers'; function LcTestInfo({ tests }) { return ( - - - Test - Fees - - {tests && - tests.map((test, index) => { - return ( - - {test.name} - {formatCurrency(test.fee)} - - ); - })} - + <> +
    + + + Test Name + Fees + + {tests && + tests.map((test, index) => { + return ( + + {test.name} + {formatCurrency(test.fee)} + + ); + })} + +
    + ); } diff --git a/src/applications/gi/constants.js b/src/applications/gi/constants.js index 6d3aae053778..578e1795b75b 100644 --- a/src/applications/gi/constants.js +++ b/src/applications/gi/constants.js @@ -444,6 +444,7 @@ export const yellowRibbonDegreeLevelTypeHash = { 'AAS in Accounting': ['Associates'], 'AAS/BS - Undergraduate': ['Associates', 'Undergraduate'], 'AOS - Undergraduate': ['Undergraduate'], + ALL: ['All'], All: ['All'], 'All (Arts & Science)': ['All'], 'All (Bachelor of Arts)': ['All'], diff --git a/src/applications/gi/containers/LicenseCertificationSearchPage.jsx b/src/applications/gi/containers/LicenseCertificationSearchPage.jsx index 64a07bf3c4e5..d02ff59eebab 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchPage.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchPage.jsx @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import { VaAccordion, VaAccordionItem, + VaLink, VaLoadingIndicator, VaModal, } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; @@ -15,29 +16,67 @@ import { fetchLicenseCertificationResults } from '../actions'; const faqs = [ { question: 'What is the difference between a license and certification?', - answer: - 'A license is granted by the state or a governing authority; whereas, a certification is granted by professional organizations or associations.', + answer: ( +

    + A license is granted by the state or a governing authority; whereas, a + certification is granted by professional organizations or associations. +

    + ), }, { question: 'What will my benefits cover?', - answer: - "Part of your entitlement can be used to cover the costs of tests, up to $2000, for a job that requires a license or certification—even if you're already receiving other education benefits. Your benefits will only cover tests approved for the GI Bill.", + answer: ( +

    + Part of your entitlement can be used to cover the costs of tests, up to + \$2000, for a job that requires a license or certification—even if + you’re already receiving other education benefits. Your benefits will + only cover tests approved for the GI Bill. +

    + ), }, { question: 'How do I get reimbursed for the licenses, certifications, and prep courses?', - answer: - 'Print and fill out form Request for Reimbursement of Licensing or Certification Test Fees. Send the completed application to the Regional Processing Office for your region listed in the form. Get VA Form 22-0803 to print.', + answer: ( + <> + +
    +
    +

    + Print and fill out form Request for Reimbursement of Licensing or + Certification Test Fees. Send the completed application to the + Regional Processing Office for your region listed in the form. +

    +
    +
    + + + ), }, { question: 'What is a prep course?', - answer: - 'A preparatory course (prep course) is a course that prepares students for success tied to a specific license or certification.', + answer: ( +

    + A preparatory course (prep course) is a course that prepares students + for success tied to a specific license or certification. +

    + ), }, { question: 'Can I get paid to take a test more than once?', - answer: - "If you fail a license or certification test, we will pay again. If the license or certification expires, you can take it again and we'll pay for the renewal.", + answer: ( +

    + If you fail a license or certification test, we will pay again. If the + license or certification expires, you can take it again and we’ll pay + for the renewal. +

    + ), }, ]; @@ -96,7 +135,7 @@ function LicenseCertificationSearchPage({ }; return ( -
    +
    {fetchingLc && (
    -

    - Licenses, Certifications, and Prep courses +

    + Licenses, certifications, and prep courses

    -

    +

    Use the search tool to find out which tests or related prep courses are reimbursable. If you don’t see a test or prep course listed, it may be a valid test that’s not yet approved. We @@ -125,7 +164,7 @@ function LicenseCertificationSearchPage({ you’re trying to obtain and whether or not it is state-specific.

    -
    +
    +

    {lacNm}

    {eduLacTypeNm}

    diff --git a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx index aab22c472d2e..c6be2e45a5b9 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx @@ -93,29 +93,24 @@ function LicenseCertificationSearchResults({ hasFetchedOnce && ( <>
    -

    - Licenses, Certifications, and Prep courses Search Results +

    + Search Results

    -

    +

    Showing{' '} {filteredResults.length === 0 && ' 0 results for:'} - {filteredResults.length !== 0 && - `${ - filteredResults.length > itemsPerPage - ? `${formatResultCount( - filteredResults, - currentPage, - itemsPerPage, - )} of ${filteredResults.length} results for: ` - : `${filteredResults.length} - of ${filteredResults.length} results for: ` - }`} + {`${formatResultCount( + filteredResults, + currentPage, + itemsPerPage, + )} of ${filteredResults.length} results for:`}

    {filteredResults.length > 0 ? ( -
      +
        {currentResults.map((result, index) => { return (
      • {result.lacNm}

        -

        +

        {result.eduLacTypeNm}

        + {result.eduLacTypeNm !== 'Certification' && ( +

        + {ADDRESS_DATA.states[result.state]} +

        + )} { const dispatch = useDispatch(); const { examId } = useParams(); const [isMobile, setIsMobile] = useState(false); - const { examDetails, loadingDetails, error } = useSelector( state => state.nationalExams, ); - useEffect( () => { window.scrollTo(0, 0); @@ -55,50 +55,53 @@ const NationalExamDetails = () => { }, []); // Remove this once the table width is updated in the component + useLayoutEffect( + // eslint-disable-next-line consistent-return () => { - const observer = new MutationObserver(() => { - const vaTableInner = document.querySelector( - '.exams-table va-table-inner', - ); - if (vaTableInner?.shadowRoot) { - const { shadowRoot } = vaTableInner; - const usaTable = shadowRoot.querySelector('.usa-table'); - if (usaTable) { - usaTable.style.width = '100%'; + if (!error) { + const observer = new MutationObserver(() => { + const vaTableInner = document.querySelector( + '.exams-table va-table-inner', + ); + if (vaTableInner?.shadowRoot) { + const { shadowRoot } = vaTableInner; + const usaTable = shadowRoot.querySelector('.usa-table'); + if (usaTable) { + usaTable.style.width = '100%'; + } } - } - }); - - const vaTable = document.querySelector('.exams-table va-table'); - if (vaTable) { - observer.observe(vaTable, { - attributes: true, - childList: true, - subtree: true, }); - } - return () => observer.disconnect(); + const vaTable = document.querySelector('.exams-table va-table'); + if (vaTable) { + observer.observe(vaTable, { + attributes: true, + childList: true, + subtree: true, + }); + } + return () => observer.disconnect(); + } }, - [examDetails], + [examDetails, error], ); if (error) { return (
        -

        - We can’t load the National Exam Details right now + We can’t load the national exam details right now

        We’re sorry. There’s a problem with our system. Try again later.

        -
        +
        ); } @@ -108,7 +111,7 @@ const NationalExamDetails = () => {
        ); @@ -118,32 +121,32 @@ const NationalExamDetails = () => { return (
        -

        {name}

        +

        + {formatNationalExamName(name)} +

        Admin Info

        - + - {institution?.name} + {toTitleCase(institution?.name)} + + + + {institution?.webAddress} - {/* - - {institution?.web_address} - */}
        The following is the headquarters address.

        - {institution?.physicalAddress?.address1} + {formatAddress(institution?.physicalAddress?.address1)}
        - {institution.physicalAddress?.city}, - {institution.physicalAddress?.state} + {formatAddress(institution.physicalAddress?.city)},{' '} + {institution.physicalAddress?.state}{' '} {institution.physicalAddress?.zip} -
        - {institution.physicalAddress?.country}

        @@ -154,9 +157,9 @@ const NationalExamDetails = () => { for your region listed in the form.

        -
        @@ -175,8 +178,9 @@ const NationalExamDetails = () => { return ( {test.name} - - {test.beginDate} - {test.endDate} + + {moment(test.beginDate).format('MM/DD/YY')} -{' '} + {moment(test.endDate).format('MM/DD/YY')} {Number(test.fee).toLocaleString('en-US', { @@ -194,4 +198,30 @@ const NationalExamDetails = () => { ); }; +NationalExamDetails.propTypes = { + examDetails: PropTypes.shape({ + name: PropTypes.string, + tests: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + beginDate: PropTypes.string, + endDate: PropTypes.string, + fee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }), + ), + institution: PropTypes.shape({ + name: PropTypes.string, + physicalAddress: PropTypes.shape({ + address1: PropTypes.string, + city: PropTypes.string, + state: PropTypes.string, + zip: PropTypes.string, + country: PropTypes.string, + }), + }), + }), + loadingDetails: PropTypes.bool, + error: PropTypes.string, +}; + export default NationalExamDetails; diff --git a/src/applications/gi/containers/NationalExamsList.jsx b/src/applications/gi/containers/NationalExamsList.jsx index 544f8914d2e4..9b61c03e8903 100644 --- a/src/applications/gi/containers/NationalExamsList.jsx +++ b/src/applications/gi/containers/NationalExamsList.jsx @@ -4,12 +4,10 @@ import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import { VaPagination, - VaCard, VaLinkAction, - VaAlert, - VaLink, } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { fetchNationalExams } from '../actions'; +import { formatNationalExamName } from '../utils/helpers'; const NationalExamsList = () => { const dispatch = useDispatch(); @@ -65,7 +63,7 @@ const NationalExamsList = () => { actual amount of the fee charged for the test. The amount covered by VA may differ from the actual cost of the exam.

        - { return (
        -

        - We can’t load the National Exams list right now + We can’t load the national exams list right now

        We’re sorry. There’s a problem with our system. Try again later.

        -
        +
        ); } @@ -98,7 +96,7 @@ const NationalExamsList = () => {
    ); @@ -121,18 +119,22 @@ const NationalExamsList = () => {
      {currentExams.map(exam => (
    • - -

      {exam.name}

      + +

      + {formatNationalExamName(exam.name)} +

      -
      +
    • ))}
    diff --git a/src/applications/gi/sass/gi.scss b/src/applications/gi/sass/gi.scss index 1eec1f7c5ac0..4b15f2f1da1d 100644 --- a/src/applications/gi/sass/gi.scss +++ b/src/applications/gi/sass/gi.scss @@ -310,30 +310,72 @@ padding-left: 0; } -.lc-dropdown-filter { - max-width: 30rem; +.lc-page-wrapper { + .faq-answer { + display: inline; + } + + .button-wrapper { + display: flex; + flex-direction: column; + gap: $units-1; + margin: inherit; + padding: $units-1 0; + + @media (min-width: $mobile-lg) { + flex-direction: row; + gap: 0; + padding: $units-6 0; + } + + .va-button { + @media (min-width: $mobile-lg) { + margin-right: $units-2; + } + } + } } -.lc-card-subheader { - text-transform: capitalize; +.lc-form-wrapper { + $input-width: 30rem; + + .dropdown-filter { + max-width: $input-width; + } + + .input-container { + display: flex; + border: 1px solid $color-gray-dark; + margin: 6px 0px; + height: 2.625rem; + max-width: $input-width; + + input { + border: none; + height: unset; + } + + .clear-icon { + padding: 0 3px; + } + } + + .additional-info-wrapper { + max-width: $input-width; + padding-top: 3px; + } } -// .input-container { -// display: flex; -// border: 1px solid $color-gray-dark; -// margin: 6px 0px; -// height: 2.625rem; -// max-width: 30rem; - -// input { -// border: none; -// height: unset; -// } - -// .clear-icon { -// padding: 0 3px; -// } -// } +.lc-result-cards-wrapper { + list-style: none; + padding-left: 0; + + .back-link { + @media (max-width: $mobile-lg) { + display: none; + } + } +} .inst-remove-btn::part(button) { background-color: transparent; diff --git a/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx b/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx index b401ca8e08b0..16ad64c1f33e 100644 --- a/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx +++ b/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx @@ -54,7 +54,7 @@ describe('NationalExamDetails', () => { const wrapper = mountComponent(); expect(wrapper.find('va-loading-indicator').exists()).to.be.true; expect(wrapper.find('va-loading-indicator').prop('message')).to.equal( - 'Loading your National Exam Details...', + 'Loading your national exam details...', ); wrapper.unmount(); }); @@ -80,7 +80,7 @@ describe('NationalExamDetails', () => { expect(alert.exists()).to.be.true; expect(alert.prop('status')).to.equal('error'); expect(alert.find('h2[slot="headline"]').text()).to.equal( - 'We can’t load the National Exam Details right now', + 'We can’t load the national exam details right now', ); expect(alert.find('p').text()).to.include( 'We’re sorry. There’s a problem with our system. Try again later.', @@ -129,13 +129,13 @@ describe('NationalExamDetails', () => { expect(institutionSpan.exists()).to.be.true; const addressBlock = wrapper.find('.va-address-block'); expect(addressBlock.text()).to.contain('123 Main St'); - expect(addressBlock.text()).to.contain('Anytown,VA12345'); + expect(addressBlock.text()).to.contain('Anytown, VA 12345'); const formLink = wrapper.find( 'va-link[href="https://www.va.gov/find-forms/about-form-22-0810/"]', ); expect(formLink.exists()).to.be.true; expect(formLink.prop('text')).to.equal( - 'Get link to VA Form 22-0810 to print', + 'Get link to VA Form 22-0810 to download', ); const tableRows = wrapper.find('va-table-row'); @@ -143,9 +143,8 @@ describe('NationalExamDetails', () => { const testRow = tableRows.at(1); expect(testRow.text()).to.contain('Test A'); - expect(testRow.text()).to.contain('2020-01-01 - 2020-12-31'); + expect(testRow.text()).to.contain('01/01/20 - 12/31/20'); expect(testRow.text()).to.contain('$100'); - wrapper.unmount(); }); @@ -161,7 +160,7 @@ describe('NationalExamDetails', () => { const wrapper = mountComponent(); expect(wrapper.find('va-loading-indicator').exists()).to.be.true; expect(wrapper.find('va-loading-indicator').prop('message')).to.equal( - 'Loading your National Exam Details...', + 'Loading your national exam details...', ); wrapper.unmount(); }); diff --git a/src/applications/gi/tests/containers/NationalExams.unit.spec.jsx b/src/applications/gi/tests/containers/NationalExams.unit.spec.jsx index bc619785c74b..acde4baedd34 100644 --- a/src/applications/gi/tests/containers/NationalExams.unit.spec.jsx +++ b/src/applications/gi/tests/containers/NationalExams.unit.spec.jsx @@ -7,6 +7,7 @@ import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import NationalExamsList from '../../containers/NationalExamsList'; +import { formatNationalExamName } from '../../utils/helpers'; const mockExams = [ { @@ -167,28 +168,22 @@ describe('NationalExamsList', () => { const newPage = 2; const itemsPerPage = 10; - // Trigger a page change to the second page wrapper.find('VaPagination').prop('onPageSelect')({ detail: { page: newPage }, }); wrapper.update(); - - // Wait a tick for asynchronous updates await new Promise(resolve => setTimeout(resolve, 0)); - - // Determine which exam names we expect on page 2 const expectedItems = initialState.nationalExams.nationalExams .slice((newPage - 1) * itemsPerPage, newPage * itemsPerPage) .map(exam => exam.name); - - // Grab the displayed exam names from the UI + const expectedItemsFormatted = expectedItems.map(name => + formatNationalExamName(name), + ); const displayedItems = wrapper.find('li h3').map(node => node.text()); - - // Confirm that the displayed items match what we expect - expect(displayedItems).to.deep.equal(expectedItems); - + expect(displayedItems).to.deep.equal(expectedItemsFormatted); wrapper.unmount(); }); + it('displays the loading indicator when loading is true', () => { // Mount the component with loading state set to true store = mockStore({ @@ -210,7 +205,7 @@ describe('NationalExamsList', () => { expect(loadingIndicator.exists()).to.be.true; expect(loadingIndicator.prop('label')).to.equal('Loading'); expect(loadingIndicator.prop('message')).to.equal( - 'Loading your National Exams...', + 'Loading your national exams...', ); wrapper.unmount(); }); @@ -264,7 +259,7 @@ describe('NationalExamsList', () => { expect(alert.exists()).to.be.true; expect(alert.prop('status')).to.equal('error'); expect(alert.find('h2[slot="headline"]').text()).to.equal( - 'We can’t load the National Exams list right now', + 'We can’t load the national exams list right now', ); expect(alert.find('p').text()).to.include( 'We’re sorry. There’s a problem with our system. Try again later.', diff --git a/src/applications/gi/tests/utils/helpers.unit.spec.js b/src/applications/gi/tests/utils/helpers.unit.spec.js index ac4ee6b978ea..ee6003b48257 100644 --- a/src/applications/gi/tests/utils/helpers.unit.spec.js +++ b/src/applications/gi/tests/utils/helpers.unit.spec.js @@ -31,6 +31,9 @@ import { deriveEligibleStudents, capitalizeFirstLetter, getAbbreviationsAsArray, + formatNationalExamName, + formatAddress, + toTitleCase, } from '../../utils/helpers'; describe('GIBCT helpers:', () => { @@ -712,4 +715,230 @@ describe('GIBCT helpers:', () => { expect(getAbbreviationsAsArray('XYZ')).to.deep.equal([]); }); }); + + describe('formatNationalExamName', () => { + it('should return an empty string when name is null', () => { + expect(formatNationalExamName(null)).to.equal(''); + }); + + it('should return an empty string when name is undefined', () => { + expect(formatNationalExamName(undefined)).to.equal(''); + }); + + it('should return an empty string when name is an empty string', () => { + expect(formatNationalExamName('')).to.equal(''); + }); + + it('should return an empty string when name is only whitespace', () => { + expect(formatNationalExamName(' ')).to.equal(''); + }); + + it('should return "DSST-DANTES" unchanged', () => { + expect(formatNationalExamName('DSST-DANTES')).to.equal('DSST-DANTES'); + }); + + it('should format "MAT-MILLER ANALOGIES TEST" to "MAT-MILLER analogies test"', () => { + expect(formatNationalExamName('MAT-MILLER ANALOGIES TEST')).to.equal( + 'MAT-MILLER analogies test', + ); + }); + + it('should return "ECE (4 hours)" unchanged', () => { + expect(formatNationalExamName('ECE (4 hours)')).to.equal('ECE (4 hours)'); + }); + + it('should return "ECE (6 hours)" unchanged', () => { + expect(formatNationalExamName('ECE (6 hours)')).to.equal('ECE (6 hours)'); + }); + + it('should format "ECE 8 HOURS NURSING" to "ECE (8 hours) nursing"', () => { + expect(formatNationalExamName('ECE 8 HOURS NURSING')).to.equal( + 'ECE (8 hours) nursing', + ); + }); + + it('should format "DANTES SPONSORED CLEP EXAMS" to "DANTES sponsored clep exams"', () => { + expect(formatNationalExamName('DANTES SPONSORED CLEP EXAMS')).to.equal( + 'DANTES sponsored clep exams', + ); + }); + + it('should properly split on dash and lowercase the right side', () => { + expect(formatNationalExamName('AP-ADVANCED PLACEMENT EXAMS')).to.equal( + 'AP-advanced placement exams', + ); + expect( + formatNationalExamName('CLEP-COLLEGE LEVEL EXAMINATION PROGRAM'), + ).to.equal('CLEP-college level examination program'); + }); + + it('should return the original name if no other condition is met', () => { + expect(formatNationalExamName('ACT')).to.equal('ACT'); + expect(formatNationalExamName('MCAT')).to.equal('MCAT'); + expect(formatNationalExamName('TOEFL')).to.equal('TOEFL'); + }); + }); + describe('formatAddress', () => { + it('should return the same value if input is not a string', () => { + expect(formatAddress(null)).to.equal(null); + expect(formatAddress(undefined)).to.equal(undefined); + expect(formatAddress(12345)).to.equal(12345); + expect(formatAddress({})).to.deep.equal({}); + expect(formatAddress([])).to.deep.equal([]); + expect(formatAddress(() => {})).to.be.a('function'); + }); + + it('should return the same string if it is empty or only whitespace', () => { + expect(formatAddress('')).to.equal(''); + expect(formatAddress(' ')).to.equal(' '); + expect(formatAddress('\t\n')).to.equal('\t\n'); + }); + + it('should capitalize each word properly', () => { + expect(formatAddress('123 main street')).to.equal('123 Main Street'); + expect(formatAddress('456 elm avenue')).to.equal('456 Elm Avenue'); + expect(formatAddress('789 broadWAY')).to.equal('789 Broadway'); + expect(formatAddress('1010 PINE Boulevard')).to.equal( + '1010 Pine Boulevard', + ); + }); + + it('should keep exceptions in uppercase', () => { + expect(formatAddress('500 nw 25th street')).to.equal( + '500 NW 25th Street', + ); + expect(formatAddress('800 Nw Elm Avenue')).to.equal('800 NW Elm Avenue'); + expect(formatAddress('900 nw Broadway')).to.equal('900 NW Broadway'); + }); + + it('should handle multiple spaces and different whitespace characters', () => { + expect(formatAddress('1600 Pennsylvania Ave')).to.equal( + '1600 Pennsylvania Ave', + ); + expect(formatAddress(' 742 Evergreen Terrace ')).to.equal( + '742 Evergreen Terrace', + ); + expect(formatAddress('221B\tBaker\nStreet')).to.equal( + '221B Baker Street', + ); + }); + + it('should handle mixed case and special characters', () => { + expect(formatAddress('a1b2c3 d4E5F6')).to.equal('A1b2c3 D4e5f6'); + expect(formatAddress('PO BOX 123')).to.equal('PO Box 123'); + expect(formatAddress('UNIT 4567-A')).to.equal('Unit 4567-A'); + }); + + it('should handle words with hyphens correctly', () => { + expect(formatAddress('123 north-west road')).to.equal( + '123 North-West Road', + ); + expect(formatAddress('456 NW-7th Ave')).to.equal('456 NW-7th Ave'); + expect(formatAddress('789 nw-elm street')).to.equal('789 NW-Elm Street'); + expect(formatAddress('PO-BOX-123')).to.equal('PO-Box-123'); + expect(formatAddress('NW-WEST')); + expect(formatAddress('NW-WEST Road')).to.equal('NW-West Road'); + }); + + it('should handle single-word addresses', () => { + expect(formatAddress('Main')).to.equal('Main'); + expect(formatAddress('nw')).to.equal('NW'); + expect(formatAddress('NW')).to.equal('NW'); + expect(formatAddress('PO')).to.equal('PO'); + }); + + it('should handle addresses with numbers and letters', () => { + expect(formatAddress('1234 NW5th Street')).to.equal('1234 NW5th Street'); + expect(formatAddress('5678 nw12th Avenue')).to.equal( + '5678 NW12th Avenue', + ); + expect(formatAddress('91011 NW-13th Blvd')).to.equal( + '91011 NW-13th Blvd', + ); + }); + + it('should not alter the original string structure beyond capitalization', () => { + const input = '123 Main-Street NW'; + const expected = '123 Main-Street NW'; + expect(formatAddress(input)).to.equal(expected); + }); + + it('should trim leading and trailing whitespace', () => { + expect(formatAddress(' 1600 Pennsylvania Ave ')).to.equal( + '1600 Pennsylvania Ave', + ); + expect(formatAddress('\t742 Evergreen Terrace\n')).to.equal( + '742 Evergreen Terrace', + ); + }); + }); + describe('toTitleCase', () => { + it('should return an empty string when input is null,undefined, or an empty string', () => { + expect(toTitleCase(null)).to.equal(''); + expect(toTitleCase(undefined)).to.equal(''); + expect(toTitleCase('')).to.equal(''); + }); + + it('should return an empty string when input is only whitespace', () => { + expect(toTitleCase(' ')).to.equal(''); + expect(toTitleCase('\t\n')).to.equal(''); + }); + + it('should capitalize a single lowercase word', () => { + expect(toTitleCase('hello')).to.equal('Hello'); + }); + + it('should capitalize a single uppercase word', () => { + expect(toTitleCase('HELLO')).to.equal('Hello'); + }); + + it('should capitalize a single mixed-case word', () => { + expect(toTitleCase('hElLo')).to.equal('Hello'); + }); + + it('should capitalize multiple words separated by spaces', () => { + expect(toTitleCase('hello world')).to.equal('Hello World'); + expect(toTitleCase('javaScript is awesome')).to.equal( + 'Javascript Is Awesome', + ); + }); + + it('should handle words with hyphens correctly', () => { + expect(toTitleCase('state-of-the-art')).to.equal('State-Of-The-Art'); + expect(toTitleCase('well-known fact')).to.equal('Well-Known Fact'); + expect(toTitleCase('mother-in-law')).to.equal('Mother-In-Law'); + }); + + it('should handle multiple hyphenated words in a sentence', () => { + expect( + toTitleCase('the state-of-the-art technology is well-known'), + ).to.equal('The State-Of-The-Art Technology Is Well-Known'); + }); + + it('should handle words with numbers correctly', () => { + expect(toTitleCase('version2 update')).to.equal('Version2 Update'); + expect(toTitleCase('room 101')).to.equal('Room 101'); + }); + + it('should handle words with special characters correctly', () => { + expect(toTitleCase('@hello world!')).to.equal('@hello World!'); + expect(toTitleCase('good-morning, everyone')).to.equal( + 'Good-Morning, Everyone', + ); + }); + + it('should handle multiple spaces between words', () => { + expect(toTitleCase('1600 Pennsylvania Ave')).to.equal( + '1600 Pennsylvania Ave', + ); + expect(toTitleCase('742 Evergreen Terrace')).to.equal( + '742 Evergreen Terrace', + ); + }); + + it('should trim leading and trailing whitespace and capitalize correctly', () => { + expect(toTitleCase(' 123 main street ')).to.equal('123 Main Street'); + expect(toTitleCase('\t456 elm avenue\n')).to.equal('456 Elm Avenue'); + }); + }); }); diff --git a/src/applications/gi/utils/helpers.js b/src/applications/gi/utils/helpers.js index 133a4d48b6c2..3074225bf28d 100644 --- a/src/applications/gi/utils/helpers.js +++ b/src/applications/gi/utils/helpers.js @@ -1,6 +1,7 @@ import { snakeCase } from 'lodash'; import URLSearchParams from 'url-search-params'; import { useLocation } from 'react-router-dom'; +import ADDRESS_DATA from 'platform/forms/address/data'; import constants from 'vets-json-schema/dist/constants.json'; import mbxGeo from '@mapbox/mapbox-sdk/services/geocoding'; @@ -604,6 +605,105 @@ export function capitalizeFirstLetter(string) { return null; } +export const mappedStates = Object.entries(ADDRESS_DATA.states).map(state => { + return { optionValue: state[0], optionLabel: state[1] }; +}); + +export const updateDropdowns = ( + category = 'all', + location = 'all', + multiples = [], +) => { + const initialDropdowns = [ + { + label: 'category', + options: [ + { optionValue: 'all', optionLabel: 'All' }, + { optionValue: 'License', optionLabel: 'License' }, + { + optionValue: 'Certification', + optionLabel: 'Certification', + }, + { + optionValue: 'Prep Course', + optionLabel: 'Prep Course', + }, + ], + alt: 'category type', + current: { optionValue: 'all', optionLabel: 'All' }, + }, + { + label: 'state', + options: + multiples.length === 0 + ? [{ optionValue: 'all', optionLabel: 'All' }, ...mappedStates] + : [ + { optionValue: 'all', optionLabel: 'All' }, + ...mappedStates.filter(mappedState => + multiples.find( + multiple => multiple.state === mappedState.optionValue, + ), + ), + ], + alt: 'state', + current: { optionValue: 'all', optionLabel: 'All' }, + }, + ]; + + return initialDropdowns.map(dropdown => { + if (dropdown.label === 'category') { + return { + ...dropdown, + current: dropdown.options.find( + option => option.optionValue === category, + ), + }; + } + + if (dropdown.label === 'state') { + return { + ...dropdown, + current: dropdown.options.find( + option => option.optionValue === location, + ) ?? { ...dropdown.current }, + }; + } + + return dropdown; + }); +}; + +export const showMultipleNames = (suggestions, nameInput) => { + return suggestions.filter( + suggestion => suggestion.lacNm.toLowerCase() === nameInput?.toLowerCase(), + ); +}; + +export const categoryCheck = type => { + if (type === 'License') { + return true; + } + if (type === 'Prep Course') return true; + + return false; +}; + +export const checkAlert = (type, multiples, currentLocation, newLocation) => { + if (multiples.length > 1 && type !== 'Certification') { + return true; + } + + if (categoryCheck(type) && currentLocation !== newLocation) { + return true; + } + + if (type === 'Certification' && currentLocation !== 'all') { + return true; + } + + return false; +}; + export const formatProgramType = (programType = '') => { if (!programType) return ''; @@ -744,3 +844,107 @@ export const getAbbreviationsAsArray = value => { return items.map(item => `${item.abbreviation}: ${item.description}`); }; + +export const formatNationalExamName = name => { + if (!name || !name.trim()) { + return ''; + } + + if (name === 'DSST-DANTES') { + return name; + } + + if (name === 'MAT-MILLER ANALOGIES TEST') { + return 'MAT-MILLER analogies test'; + } + + if (name === 'ECE (4 hours)' || name === 'ECE (6 hours)') { + return name; + } + + const eceNursingMatch = name.match(/^ECE\s+(\d+)\s+HOURS\s+NURSING$/i); + if (eceNursingMatch) { + const hours = eceNursingMatch[1]; + return `ECE (${hours} hours) nursing`; + } + + if (name === 'DANTES SPONSORED CLEP EXAMS') { + return 'DANTES sponsored clep exams'; + } + + if (name.includes('-')) { + const [left, right] = name.split('-', 2); + return `${left}-${right.toLowerCase()}`; + } + + return name; +}; + +export const formatAddress = str => { + if (typeof str !== 'string' || str.trim().length === 0) { + return str; + } + + const exceptionsList = ['NW', 'NE', 'SW', 'SE', 'PO']; + const exceptions = exceptionsList.map(item => item.toUpperCase()); + + return str + .trim() + .split(/\s+/) + .map(word => { + const subWords = word.split('-'); + const formattedSubWords = subWords.map(subWord => { + const upperSubWord = subWord.toUpperCase(); + + if (exceptions.includes(upperSubWord)) { + return upperSubWord; + } + + const matchingException = exceptions.find(ex => + upperSubWord.startsWith(ex), + ); + if (matchingException) { + return matchingException + subWord.slice(matchingException.length); + } + + if (/^\d+[A-Z]+$/.test(subWord)) { + return subWord; + } + + const numberLetterMatch = subWord.match(/^(\d+)([a-zA-Z]+)$/); + if (numberLetterMatch) { + const numbers = numberLetterMatch[1]; + const letters = numberLetterMatch[2]; + return `${numbers}${letters}`; + } + + return subWord.charAt(0).toUpperCase() + subWord.slice(1).toLowerCase(); + }); + + return formattedSubWords.join('-'); + }) + .join(' '); +}; + +export const toTitleCase = str => { + if (typeof str !== 'string') { + return ''; + } + + const trimmedStr = str.trim(); + + if (!trimmedStr) { + return ''; + } + + const words = trimmedStr.split(/\s+/); + + const titled = words.map(word => { + const parts = word.split('-').map(part => { + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + }); + return parts.join('-'); + }); + + return titled.join(' '); +}; diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/enrollment-status.json b/src/applications/hca/tests/e2e/fixtures/mocks/enrollment-status.json new file mode 100644 index 000000000000..18dd175f8809 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/enrollment-status.json @@ -0,0 +1,13 @@ +{ + "statusCode": 404, + "body": { + "errors": [ + { + "title": "Record not found", + "detail": "The record identified by ? could not be found", + "code": "404", + "status": "404" + } + ] + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockFacilitiesV1.json b/src/applications/hca/tests/e2e/fixtures/mocks/facilities.json similarity index 100% rename from src/applications/hca/tests/e2e/fixtures/mocks/mockFacilitiesV1.json rename to src/applications/hca/tests/e2e/fixtures/mocks/facilities.json diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-insurance.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-insurance.json deleted file mode 100644 index d7d666b32650..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-insurance.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": { - "type": "feature_toggles", - "features": [ - { - "name": "hca_sigi_enabled", - "value": false - }, - { - "name": "hca_insurance_v2_enabled", - "value": true - }, - { - "name": "hca_browser_monitoring_enabled", - "value": false - } - ] - } -} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-reg-only.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-reg-only.json deleted file mode 100644 index 00b6471cc7f9..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-reg-only.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": { - "type": "feature_toggles", - "features": [ - { - "name": "hca_sigi_enabled", - "value": false - }, - { - "name": "hca_reg_only_enabled", - "value": true - }, - { - "name": "hca_browser_monitoring_enabled", - "value": false - } - ] - } -} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-tera.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-tera.json deleted file mode 100644 index ba8fa6a19435..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles-tera.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": { - "type": "feature_toggles", - "features": [ - { - "name": "hca_sigi_enabled", - "value": false - }, - { - "name": "hca_tera_branching_enabled", - "value": true - }, - { - "name": "hca_browser_monitoring_enabled", - "value": false - } - ] - } -} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.insurance.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.insurance.json new file mode 100644 index 000000000000..8b275bd8ab14 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.insurance.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "feature_toggles", + "features": [ + { + "name": "hca_insurance_v2_enabled", + "value": true + } + ] + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.json index 39f30330e6c0..c23acb6592b7 100644 --- a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.json +++ b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.json @@ -1,15 +1,6 @@ { "data": { "type": "feature_toggles", - "features": [ - { - "name": "hca_sigi_enabled", - "value": false - }, - { - "name": "hca_browser_monitoring_enabled", - "value": false - } - ] + "features": [] } } \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.reg-only.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.reg-only.json new file mode 100644 index 000000000000..b827edb377ae --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.reg-only.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "feature_toggles", + "features": [ + { + "name": "hca_reg_only_enabled", + "value": true + } + ] + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.tera.json b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.tera.json new file mode 100644 index 000000000000..c22a0e399195 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/feature-toggles.tera.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "feature_toggles", + "features": [ + { + "name": "hca_tera_branching_enabled", + "value": true + } + ] + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/maintenance-windows.json b/src/applications/hca/tests/e2e/fixtures/mocks/maintenance-windows.json new file mode 100644 index 000000000000..3a4b0de104ea --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/maintenance-windows.json @@ -0,0 +1,3 @@ +{ + "data": [] +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockEnrollmentStatus.json b/src/applications/hca/tests/e2e/fixtures/mocks/mockEnrollmentStatus.json deleted file mode 100644 index b420a4c6b858..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockEnrollmentStatus.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "errors": [ - { - "title": "Record not found", - "detail": "The record identified by ? could not be found", - "code": "404", - "status": "404" - } - ] -} diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockFacilities.json b/src/applications/hca/tests/e2e/fixtures/mocks/mockFacilities.json deleted file mode 100644 index f8a068d10db7..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockFacilities.json +++ /dev/null @@ -1,2288 +0,0 @@ -{ - "data": [ - { - "id": "vha_518", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 46.897435, - "established": 11.844327 - }, - { - "service": "Dermatology", - "new": 8.258064, - "established": 16.333333 - }, - { - "service": "Gynecology", - "new": 63.3, - "established": 49.826086 - }, - { - "service": "MentalHealthCare", - "new": 27.222222, - "established": 8.278294 - }, - { - "service": "Optometry", - "new": 27.782258, - "established": 18.410142 - }, - { - "service": "PrimaryCare", - "new": 23.170212, - "established": 7.951906 - }, - { - "service": "WomensHealth", - "new": 20, - "established": 13.793103 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01730-1114", - "city": "Bedford", - "state": "MA", - "address1": "200 Springs Road", - "address2": null, - "address3": null - } - }, - "classification": "VA Medical Center (VAMC)", - "detailedServices": [ - { - "name": "COVID-19 vaccines", - "descriptionFacility": null, - "appointmentLeadin": "Your VA health care team will contact you if you’re eligible to get a vaccine during this time. As the supply of vaccine increases, we'll work with our care teams to let Veterans know their options.", - "appointmentPhones": [ - { - "extension": null, - "label": "Main phone", - "number": "781-687-2000", - "type": "tel" - } - ], - "onlineSchedulingAvailable": null, - "referralRequired": "true", - "walkInsAccepted": "false", - "serviceLocations": null, - "path": "https://www.va.gov/bedford-health-care/programs/covid-19-vaccines/" - } - ], - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.8899999856948853, - "primaryCareRoutine": 0.9700000286102295, - "specialtyCareUrgent": 0.6499999761581421, - "specialtyCareRoutine": 0.8700000047683716 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "24/7", - "monday": "24/7", - "sunday": "24/7", - "tuesday": "24/7", - "saturday": "24/7", - "thursday": "24/7", - "wednesday": "24/7" - }, - "id": "vha_518", - "lat": 42.50367478, - "long": -71.2726766, - "mobile": false, - "name": "Edith Nourse Rogers Memorial Veterans' Hospital", - "operatingStatus": { - "code": "NORMAL", - "supplementalStatus": [ - { - "id": "COVID_HIGH", - "label": "COVID-19 health protection: Levels high" - } - ] - }, - "operationalHoursSpecialInstructions": "Normal business hours are Monday through Friday, 8:00 a.m. to 4:30 p.m. |", - "phone": { - "fax": "781-687-2101", - "main": "781-687-2000", - "pharmacy": "800-838-6331 x2210", - "afterHours": "800-838-6331", - "patientAdvocate": "781-687-2612", - "mentalHealthClinic": "781-687-2347", - "enrollmentCoordinator": "781-687-2275", - "healthConnect": "417-644-7714" - }, - "services": { - "other": [], - "health": [ - "Audiology", - "CaregiverSupport", - "Covid19Vaccine", - "DentalServices", - "Dermatology", - "Gynecology", - "MentalHealthCare", - "Nutrition", - "Optometry", - "Podiatry", - "PrimaryCare", - "UrgentCare", - "WomensHealth" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "518", - "visn": "1", - "website": "https://www.va.gov/bedford-health-care/locations/edith-nourse-rogers-memorial-veterans-hospital/" - } - }, - { - "id": "vha_518GA", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Dermatology", - "new": 4, - "established": null - }, - { - "service": "MentalHealthCare", - "new": 22, - "established": 15.6 - }, - { - "service": "PrimaryCare", - "new": 11.333333, - "established": 4.46124 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01904-3123", - "city": "Lynn", - "state": "MA", - "address1": "225 Boston Street", - "address2": null, - "address3": "Suite 107" - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.949999988079071 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_518GA", - "lat": 42.470514, - "long": -70.960023, - "mobile": false, - "name": "Lynn VA Clinic", - "operatingStatus": { - "code": "NORMAL", - "supplementalStatus": [ - { - "id": "COVID_HIGH", - "label": "COVID-19 health protection: Levels high" - } - ] - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "781-596-2036", - "main": "800-838-6331", - "pharmacy": "800-838-6331 x2210", - "afterHours": "800-838-6331", - "patientAdvocate": "781-687-2612", - "mentalHealthClinic": "781-687-2347", - "enrollmentCoordinator": "781-687-2275", - "healthConnect": "417-644-7714" - }, - "services": { - "other": [], - "health": [ - "Dermatology", - "MentalHealthCare", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "518GA", - "visn": "1", - "website": "https://www.va.gov/bedford-health-care/locations/lynn-va-clinic/" - } - }, - { - "id": "vha_518GB", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Dermatology", - "new": 5, - "established": null - }, - { - "service": "MentalHealthCare", - "new": null, - "established": 19.727272 - }, - { - "service": "PrimaryCare", - "new": 10.583333, - "established": 11.07588 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01830-6315", - "city": "Haverhill", - "state": "MA", - "address1": "209 Summer Street", - "address2": null, - "address3": null - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.8500000238418579 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_518GB", - "lat": 42.776001, - "long": -71.0679465, - "mobile": false, - "name": "Haverhill VA Clinic", - "operatingStatus": { - "code": "NOTICE", - "additionalInfo": "Our Primary Care service has expanded to a larger setting and the entrance now faces Hope Avenue, on the opposite side of Summer Street, at the back of the Medical Arts Center. Specialty Care services have moved into the former Primary Care location facing Mill Stree.", - "supplementalStatus": [ - { - "id": "COVID_HIGH", - "label": "COVID-19 health protection: Levels high" - } - ] - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "781-687-3981", - "main": "800-838-6331", - "pharmacy": "800-838-6331 x2210", - "afterHours": "800-838-6331", - "patientAdvocate": "781-687-2612", - "mentalHealthClinic": "781-687-2347", - "enrollmentCoordinator": "781-687-2275", - "healthConnect": "417-644-7714" - }, - "services": { - "other": [], - "health": [ - "Dermatology", - "MentalHealthCare", - "Podiatry", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "518GB", - "visn": "1", - "website": "https://www.va.gov/bedford-health-care/locations/haverhill-va-clinic/" - } - }, - { - "id": "vha_518GE", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Dermatology", - "new": 5.833333, - "established": 0 - }, - { - "service": "MentalHealthCare", - "new": 69, - "established": 4.964912 - }, - { - "service": "PrimaryCare", - "new": 18.25, - "established": 9.146118 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01930-6029", - "city": "Gloucester", - "state": "MA", - "address1": "199 Main Street", - "address2": null, - "address3": null - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.9599999785423279 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_518GE", - "lat": 42.61311041, - "long": -70.66075311, - "mobile": false, - "name": "Gloucester VA Clinic", - "operatingStatus": { - "code": "NORMAL", - "supplementalStatus": [ - { - "id": "COVID_HIGH", - "label": "COVID-19 health protection: Levels high" - } - ] - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "781-687-3958", - "main": "800-838-6331", - "pharmacy": "800-838-6331 x2210", - "afterHours": "800-838-6331", - "patientAdvocate": "781-687-2612", - "enrollmentCoordinator": "781-687-2275", - "healthConnect": "417-644-7714" - }, - "services": { - "other": [], - "health": [ - "Dermatology", - "MentalHealthCare", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "518GE", - "visn": "1", - "website": "https://www.va.gov/bedford-health-care/locations/gloucester-va-clinic/" - } - }, - { - "id": "vha_523", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 13.170886, - "established": 0.85672 - }, - { - "service": "Cardiology", - "new": null, - "established": 1.555555 - }, - { - "service": "Dermatology", - "new": 49.546099, - "established": 20.897759 - }, - { - "service": "Gastroenterology", - "new": 34.064285, - "established": 14.952595 - }, - { - "service": "Gynecology", - "new": 13.933333, - "established": 20.059523 - }, - { - "service": "MentalHealthCare", - "new": 22.568627, - "established": 6.877522 - }, - { - "service": "Ophthalmology", - "new": 46.928571, - "established": 9.399251 - }, - { - "service": "Optometry", - "new": 29.2, - "established": 10.205179 - }, - { - "service": "Orthopedics", - "new": 36.930481, - "established": 8.776416 - }, - { - "service": "PrimaryCare", - "new": 38.105263, - "established": 13.448614 - }, - { - "service": "Urology", - "new": 28.477477, - "established": 5.689507 - }, - { - "service": "WomensHealth", - "new": 38.875, - "established": 8.017857 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02130-4817", - "city": "Boston", - "state": "MA", - "address1": "150 South Huntington Avenue", - "address2": null, - "address3": null - } - }, - "classification": "VA Medical Center (VAMC)", - "detailedServices": [ - { - "name": "COVID-19 vaccines", - "descriptionFacility": null, - "appointmentLeadin": "Walk-in clinic for 1st, 2nd, 3rd, 4th and booster COVID-19 vaccine doses.", - "appointmentPhones": [ - { - "extension": null, - "label": "Main phone", - "number": "617-232-9500", - "type": "tel" - } - ], - "onlineSchedulingAvailable": null, - "referralRequired": "false", - "walkInsAccepted": "true", - "serviceLocations": null, - "path": "https://www.va.gov/boston-health-care/programs/covid-19-vaccines/" - } - ], - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.9300000071525574, - "specialtyCareUrgent": 0.8100000023841858, - "specialtyCareRoutine": 0.9300000071525574 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "730AM-400PM", - "monday": "730AM-400PM", - "sunday": "Closed", - "tuesday": "730AM-400PM", - "saturday": "Closed", - "thursday": "730AM-400PM", - "wednesday": "730AM-400PM" - }, - "id": "vha_523", - "lat": 42.32714719, - "long": -71.1110553, - "mobile": false, - "name": "Jamaica Plain VA Medical Center", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "857-364-4549", - "main": "617-232-9500", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-364-2552", - "mentalHealthClinic": "857-364-5647", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "CaregiverSupport", - "DentalServices", - "Dermatology", - "Gastroenterology", - "Gynecology", - "MentalHealthCare", - "Nutrition", - "Ophthalmology", - "Optometry", - "Orthopedics", - "Orthopedics", - "Podiatry", - "PrimaryCare", - "UrgentCare", - "Urology", - "WomensHealth" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/jamaica-plain-va-medical-center/" - } - }, - { - "id": "vha_523A4", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Cardiology", - "new": 41.404255, - "established": 15.301757 - }, - { - "service": "Dermatology", - "new": 65.611111, - "established": 15.801526 - }, - { - "service": "Gastroenterology", - "new": 26, - "established": 56.464285 - }, - { - "service": "Optometry", - "new": 17.787878, - "established": 1.075528 - }, - { - "service": "Orthopedics", - "new": 14, - "established": 8.34375 - }, - { - "service": "PrimaryCare", - "new": 66.868421, - "established": 16.412296 - }, - { - "service": "Urology", - "new": null, - "established": 18.206896 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02132-4927", - "city": "West Roxbury", - "state": "MA", - "address1": "1400 VFW Parkway", - "address2": null, - "address3": null - } - }, - "classification": "VA Medical Center (VAMC)", - "detailedServices": [ - { - "name": "COVID-19 vaccines", - "descriptionFacility": null, - "appointmentLeadin": "Walk-in clinic for 1st, 2nd, 3rd, 4th and booster COVID-19 vaccine doses.", - "appointmentPhones": [ - { - "extension": null, - "label": "Main phone", - "number": "617-323-7700", - "type": "tel" - } - ], - "onlineSchedulingAvailable": null, - "referralRequired": "false", - "walkInsAccepted": "true", - "serviceLocations": null, - "path": "https://www.va.gov/boston-health-care/programs/covid-19-vaccines/" - } - ], - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.9599999785423279, - "primaryCareRoutine": 0.9399999976158142 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "24/7", - "monday": "24/7", - "sunday": "24/7", - "tuesday": "24/7", - "saturday": "24/7", - "thursday": "24/7", - "wednesday": "24/7" - }, - "id": "vha_523A4", - "lat": 42.274242, - "long": -71.171856, - "mobile": false, - "name": "West Roxbury VA Medical Center", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": "Normal business hours are Monday through Friday, 8:00 a.m. to 4:30 p.m. |", - "phone": { - "fax": "857-203-5500", - "main": "617-323-7700", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-203-6994", - "mentalHealthClinic": "857-364-5647", - "enrollmentCoordinator": "857-203-5471", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Cardiology", - "Dermatology", - "EmergencyCare", - "Gastroenterology", - "Nutrition", - "Optometry", - "Orthopedics", - "Orthopedics", - "PrimaryCare", - "Urology" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523A4", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/west-roxbury-va-medical-center/" - } - }, - { - "id": "vha_523A5", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 16.083333, - "established": 4.27962 - }, - { - "service": "Cardiology", - "new": 53.466666, - "established": 15.20707 - }, - { - "service": "Dermatology", - "new": 69.779411, - "established": 26.876777 - }, - { - "service": "Gastroenterology", - "new": 36.377777, - "established": 24.338983 - }, - { - "service": "Gynecology", - "new": 23.666666, - "established": 39.047619 - }, - { - "service": "MentalHealthCare", - "new": 50.4, - "established": 5.466296 - }, - { - "service": "Optometry", - "new": 15.445378, - "established": 3.987551 - }, - { - "service": "PrimaryCare", - "new": 58.186868, - "established": 13.174477 - }, - { - "service": "Urology", - "new": 51.583333, - "established": 6.072 - }, - { - "service": "WomensHealth", - "new": 26.285714, - "established": 11.698275 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02301-5596", - "city": "Brockton", - "state": "MA", - "address1": "940 Belmont Street", - "address2": null, - "address3": null - } - }, - "classification": "VA Medical Center (VAMC)", - "detailedServices": [ - { - "name": "COVID-19 vaccines", - "descriptionFacility": null, - "appointmentLeadin": "Walk-in clinic for 1st, 2nd, 3rd, 4th and booster COVID-19 vaccine doses.", - "appointmentPhones": [ - { - "extension": null, - "label": "Main phone", - "number": "508-583-4500", - "type": "tel" - } - ], - "onlineSchedulingAvailable": null, - "referralRequired": "false", - "walkInsAccepted": "true", - "serviceLocations": null, - "path": "https://www.va.gov/boston-health-care/programs/covid-19-vaccines/" - } - ], - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.9900000095367432, - "primaryCareRoutine": 0.9300000071525574 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "24/7", - "monday": "24/7", - "sunday": "24/7", - "tuesday": "24/7", - "saturday": "24/7", - "thursday": "24/7", - "wednesday": "24/7" - }, - "id": "vha_523A5", - "lat": 42.060906, - "long": -71.051868, - "mobile": false, - "name": "Brockton VA Medical Center", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": "Normal business hours are Monday through Friday, 8:00 a.m. to 4:30 p.m. |", - "phone": { - "fax": "774-826-0014", - "main": "508-583-4500", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "774-826-2415", - "mentalHealthClinic": "774-826-1778", - "enrollmentCoordinator": "774-826-2500", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "DentalServices", - "Dermatology", - "Gastroenterology", - "Gynecology", - "MentalHealthCare", - "Nutrition", - "Optometry", - "Podiatry", - "PrimaryCare", - "UrgentCare", - "Urology", - "WomensHealth" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523A5", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/brockton-va-medical-center/" - } - }, - { - "id": "vha_523BY", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 32, - "established": 6.09375 - }, - { - "service": "Cardiology", - "new": 65.666666, - "established": 31.221052 - }, - { - "service": "Dermatology", - "new": 133.189189, - "established": 55.240506 - }, - { - "service": "Gastroenterology", - "new": 23.828571, - "established": 31.892857 - }, - { - "service": "MentalHealthCare", - "new": 24.571428, - "established": 1.980769 - }, - { - "service": "Optometry", - "new": 28.185185, - "established": 2.892537 - }, - { - "service": "Orthopedics", - "new": 24.117647, - "established": 7.973684 - }, - { - "service": "PrimaryCare", - "new": 21.960784, - "established": 6.302571 - }, - { - "service": "Urology", - "new": 43.47826, - "established": 2.041666 - }, - { - "service": "WomensHealth", - "new": null, - "established": 0 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01852-5130", - "city": "Lowell", - "state": "MA", - "address1": "130 Marshall Road", - "address2": null, - "address3": null - } - }, - "classification": "Multi-Specialty CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.9399999976158142 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "730AM-400PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_523BY", - "lat": 42.61937432, - "long": -71.31536985, - "mobile": false, - "name": "Lowell VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "978-671-9149", - "main": "800-865-3384", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-364-2552", - "mentalHealthClinic": "978-671-9193", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "Dermatology", - "Gastroenterology", - "MentalHealthCare", - "Nutrition", - "Optometry", - "Orthopedics", - "Orthopedics", - "Podiatry", - "PrimaryCare", - "Urology", - "WomensHealth" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523BY", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/lowell-va-clinic/" - } - }, - { - "id": "vha_523BZ", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "MentalHealthCare", - "new": 14.307692, - "established": 0.980392 - }, - { - "service": "PrimaryCare", - "new": 21.5, - "established": 15.36923 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02114-2104", - "city": "Boston", - "state": "MA", - "address1": "251 Causeway Street", - "address2": null, - "address3": null - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.800000011920929 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_523BZ", - "lat": 42.36659056, - "long": -71.05901821, - "mobile": false, - "name": "Causeway VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "617-248-1282", - "main": "617-248-1000", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-364-2552", - "mentalHealthClinic": "617-248-1071", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "MentalHealthCare", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523BZ", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/causeway-va-clinic/" - } - }, - { - "id": "vha_523GA", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": null, - "established": 5.75 - }, - { - "service": "Gastroenterology", - "new": 58.833333, - "established": 17.6875 - }, - { - "service": "MentalHealthCare", - "new": 32.2, - "established": 5.801801 - }, - { - "service": "PrimaryCare", - "new": 22.216216, - "established": 9.375 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01702-8264", - "city": "Framingham", - "state": "MA", - "address1": "61 Lincoln Street", - "address2": null, - "address3": "Suite 112" - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.8799999952316284 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_523GA", - "lat": 42.28301175, - "long": -71.41777479, - "mobile": false, - "name": "Framingham VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-626-8224", - "main": "800-865-3384", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-203-6994", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Gastroenterology", - "MentalHealthCare", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523GA", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/framingham-va-clinic/" - } - }, - { - "id": "vha_523GC", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": null, - "established": 10.333333 - }, - { - "service": "PrimaryCare", - "new": 11.181818, - "established": 5.031468 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02171-2122", - "city": "Quincy", - "state": "MA", - "address1": "110 West Squantam Street", - "address2": null, - "address3": null - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.9900000095367432, - "primaryCareRoutine": 0.9700000286102295 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_523GC", - "lat": 42.272541, - "long": -71.030808, - "mobile": false, - "name": "Quincy VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "617-376-2015", - "main": "800-865-3384", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "857-203-6994", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523GC", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/quincy-va-clinic/" - } - }, - { - "id": "vha_523GD", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 56, - "established": 4.45 - }, - { - "service": "Dermatology", - "new": 28, - "established": null - }, - { - "service": "MentalHealthCare", - "new": 22.625, - "established": 0.5 - }, - { - "service": "PrimaryCare", - "new": 20.676056, - "established": 8.179487 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02360-2663", - "city": "Plymouth", - "state": "MA", - "address1": "116 Long Pond Road", - "address2": null, - "address3": null - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.9300000071525574 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_523GD", - "lat": 41.923754, - "long": -70.65677635, - "mobile": false, - "name": "Plymouth VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-747-8185", - "main": "800-865-3384", - "pharmacy": "857-364-4419", - "afterHours": "800-865-3384", - "patientAdvocate": "774-826-2415", - "enrollmentCoordinator": "857-364-4849", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Dermatology", - "MentalHealthCare", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "523GD", - "visn": "1", - "website": "https://www.va.gov/boston-health-care/locations/plymouth-va-clinic/" - } - }, - { - "id": "vha_631", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 22.957746, - "established": 2.207142 - }, - { - "service": "Cardiology", - "new": 19.555555, - "established": 4.543478 - }, - { - "service": "Dermatology", - "new": 145.666666, - "established": 31.403508 - }, - { - "service": "Gastroenterology", - "new": 24.25, - "established": 3 - }, - { - "service": "MentalHealthCare", - "new": 6.923076, - "established": 3.493906 - }, - { - "service": "Optometry", - "new": 53.180952, - "established": 7.23564 - }, - { - "service": "PrimaryCare", - "new": 9.275362, - "established": 5.504499 - }, - { - "service": "WomensHealth", - "new": null, - "established": 9.827586 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01053-9764", - "city": "Leeds", - "state": "MA", - "address1": "421 North Main Street", - "address2": null, - "address3": null - } - }, - "classification": "VA Medical Center (VAMC)", - "detailedServices": [ - { - "name": "COVID-19 vaccines", - "descriptionFacility": null, - "appointmentLeadin": "Your VA health care team will contact you if you’re eligible to get a vaccine during this time. As the supply of vaccine increases, we'll work with our care teams to let Veterans know their options.", - "appointmentPhones": [ - { - "extension": null, - "label": "Main phone", - "number": "413-584-4040", - "type": "tel" - } - ], - "onlineSchedulingAvailable": null, - "referralRequired": "true", - "walkInsAccepted": "false", - "serviceLocations": null, - "path": "https://www.va.gov/central-western-massachusetts-health-care/programs/covid-19-vaccines/" - } - ], - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.7599999904632568, - "primaryCareRoutine": 0.9200000166893005, - "specialtyCareUrgent": 0.8799999952316284, - "specialtyCareRoutine": 0.9300000071525574 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "24/7", - "monday": "24/7", - "sunday": "24/7", - "tuesday": "24/7", - "saturday": "24/7", - "thursday": "24/7", - "wednesday": "24/7" - }, - "id": "vha_631", - "lat": 42.3495, - "long": -72.682407, - "mobile": false, - "name": "Edward P. Boland Department of Veterans Affairs Medical Center", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": "Normal business hours are Monday through Friday, 8:00 a.m. to 4:30 p.m. |", - "phone": { - "fax": "413-582-3121", - "main": "413-584-4040", - "pharmacy": "800-893-1522", - "afterHours": "800-893-1522", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3100", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "CaregiverSupport", - "DentalServices", - "Dermatology", - "Gastroenterology", - "MentalHealthCare", - "Nutrition", - "Optometry", - "Podiatry", - "PrimaryCare", - "UrgentCare", - "WomensHealth" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/edward-p-boland-department-of-veterans-affairs-medical-center/" - } - }, - { - "id": "vha_631BY", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 13.5, - "established": 2.043478 - }, - { - "service": "Cardiology", - "new": null, - "established": 2.692307 - }, - { - "service": "Dermatology", - "new": 65.285714, - "established": 6.166666 - }, - { - "service": "Gastroenterology", - "new": 51.5, - "established": 47.75 - }, - { - "service": "MentalHealthCare", - "new": 41.633333, - "established": 9.073187 - }, - { - "service": "PrimaryCare", - "new": 44.085106, - "established": 14.225669 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01104-3401", - "city": "Springfield", - "state": "MA", - "address1": "25 Bond Street", - "address2": null, - "address3": null - } - }, - "classification": "Multi-Specialty CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.800000011920929, - "primaryCareRoutine": 0.9200000166893005 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631BY", - "lat": 42.11021717, - "long": -72.59988122, - "mobile": false, - "name": "Springfield VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "413-788-5560", - "main": "413-731-6000", - "pharmacy": "800-893-1522", - "afterHours": "800-893-1522", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3100", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "Dermatology", - "Gastroenterology", - "MentalHealthCare", - "Nutrition", - "Podiatry", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631BY", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/springfield-va-clinic/" - } - }, - { - "id": "vha_631GC", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "MentalHealthCare", - "new": 21.636363, - "established": 18.817174 - }, - { - "service": "PrimaryCare", - "new": 21.842105, - "established": 4.087193 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01201-5692", - "city": "Pittsfield", - "state": "MA", - "address1": "78 Center Street", - "address2": "Silvio Conte Building", - "address3": "2nd Floor" - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.8899999856948853 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631GC", - "lat": 42.452118, - "long": -73.2553965, - "mobile": false, - "name": "Pittsfield VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "413-447-8825", - "main": "413-499-2672", - "pharmacy": "800-893-1522", - "afterHours": "800-893-1522", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3100", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "MentalHealthCare", - "Nutrition", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631GC", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/pittsfield-va-clinic/" - } - }, - { - "id": "vha_631GD", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Dermatology", - "new": 1, - "established": 3 - }, - { - "service": "MentalHealthCare", - "new": 19, - "established": 2.927777 - }, - { - "service": "PrimaryCare", - "new": 71.727272, - "established": 15.27758 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01301-9694", - "city": "Greenfield", - "state": "MA", - "address1": "143 Munson Street", - "address2": null, - "address3": null - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.8799999952316284, - "primaryCareRoutine": 0.8700000047683716 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631GD", - "lat": 42.57570167, - "long": -72.62435941, - "mobile": false, - "name": "Greenfield VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "413-773-8435", - "main": "413-773-8428", - "pharmacy": "800-893-1522", - "afterHours": "800-893-1522", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3100", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Dermatology", - "MentalHealthCare", - "Nutrition", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631GD", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/greenfield-va-clinic/" - } - }, - { - "id": "vha_631GE", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Cardiology", - "new": 59.388888, - "established": 10.5625 - }, - { - "service": "Dermatology", - "new": 16.125, - "established": 0 - }, - { - "service": "MentalHealthCare", - "new": 50.093023, - "established": 19.876425 - }, - { - "service": "PrimaryCare", - "new": 11.406593, - "established": 7.198167 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01604-1020", - "city": "Worcester", - "state": "MA", - "address1": "403 Belmont Street", - "address2": null, - "address3": "1st Floor" - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.7599999904632568, - "primaryCareRoutine": 0.9599999785423279 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631GE", - "lat": 42.274647, - "long": -71.760798, - "mobile": false, - "name": "Worcester VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-856-7425", - "main": "508-856-0104", - "pharmacy": "800-893-1522", - "afterHours": "508-856-0104", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3091", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Cardiology", - "Dermatology", - "MentalHealthCare", - "Nutrition", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631GE", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/worcester-va-clinic/" - } - }, - { - "id": "vha_631GF", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Dermatology", - "new": 7.5, - "established": null - }, - { - "service": "MentalHealthCare", - "new": 12.5, - "established": 33.350649 - }, - { - "service": "PrimaryCare", - "new": 14.090909, - "established": 8.972727 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01420-3008", - "city": "Fitchburg", - "state": "MA", - "address1": "881 Main Street", - "address2": "Philip J. Philbin Federal Building", - "address3": null - } - }, - "classification": "Primary Care CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.9599999785423279 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631GF", - "lat": 42.586272, - "long": -71.804394, - "mobile": false, - "name": "Fitchburg VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "978-342-9521", - "main": "978-342-9781", - "pharmacy": "800-893-1522", - "afterHours": "413-584-4040", - "patientAdvocate": "413-582-3188", - "mentalHealthClinic": "413-584-4040 x 2336", - "enrollmentCoordinator": "413-582-3091", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Dermatology", - "MentalHealthCare", - "Nutrition", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631GF", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/fitchburg-va-clinic/" - } - }, - { - "id": "vha_631QA", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 11.9, - "established": 0.717616 - }, - { - "service": "Optometry", - "new": 34.340659, - "established": 4.003694 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "01605-2300", - "city": "Worcester", - "state": "MA", - "address1": "377 Plantation Street", - "address2": "Biotech", - "address3": "Building 4" - } - }, - "classification": "Other Outpatient Services (OOS)", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": {}, - "effectiveDate": null - }, - "hours": { - "friday": "800AM-430PM", - "monday": "800AM-430PM", - "sunday": "Closed", - "tuesday": "800AM-430PM", - "saturday": "Closed", - "thursday": "800AM-430PM", - "wednesday": "800AM-430PM" - }, - "id": "vha_631QA", - "lat": 42.2799516, - "long": -71.76438496, - "mobile": false, - "name": "Plantation Street VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-793-7856", - "main": "413-584-4040 x6500", - "pharmacy": "413-584-4040 x17041", - "afterHours": "413-584-4040", - "patientAdvocate": "413-582-3188", - "enrollmentCoordinator": "413-584-4040 x2503", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Optometry", - "Podiatry" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "631QA", - "visn": "1", - "website": "https://www.va.gov/central-western-massachusetts-health-care/locations/plantation-street-va-clinic/" - } - }, - { - "id": "vha_650GA", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Audiology", - "new": 26.153846, - "established": 1.621827 - }, - { - "service": "Cardiology", - "new": 50.5, - "established": 0 - }, - { - "service": "Dermatology", - "new": 0.428571, - "established": 1.428571 - }, - { - "service": "Gastroenterology", - "new": 43, - "established": 11.111111 - }, - { - "service": "MentalHealthCare", - "new": 8.5, - "established": 3.225663 - }, - { - "service": "PrimaryCare", - "new": 50.15625, - "established": 8.549048 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02740-6006", - "city": "New Bedford", - "state": "MA", - "address1": "175 Elm Street", - "address2": null, - "address3": null - } - }, - "classification": "Multi-Specialty CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0, - "primaryCareRoutine": 0.949999988079071 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "730AM-400PM", - "monday": "730AM-400PM", - "sunday": "Closed", - "tuesday": "730AM-400PM", - "saturday": "Closed", - "thursday": "730AM-400PM", - "wednesday": "730AM-400PM" - }, - "id": "vha_650GA", - "lat": 41.636268, - "long": -70.9296255, - "mobile": false, - "name": "New Bedford VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-994-5489", - "main": "508-994-0217", - "pharmacy": "866-400-1241", - "afterHours": "401-273-7100 x13997", - "patientAdvocate": "401-457-3093", - "mentalHealthClinic": "401-273-7100 x 1949", - "enrollmentCoordinator": "401-273-7100 x12496", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Audiology", - "Cardiology", - "Dermatology", - "Gastroenterology", - "MentalHealthCare", - "Nutrition", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "650GA", - "visn": "1", - "website": "https://www.va.gov/providence-health-care/locations/new-bedford-va-clinic/" - } - }, - { - "id": "vha_650GB", - "type": "facility", - "attributes": { - "access": { - "health": [ - { - "service": "Cardiology", - "new": 24.25, - "established": 5.346153 - }, - { - "service": "Dermatology", - "new": 16.3, - "established": 4.25 - }, - { - "service": "Gastroenterology", - "new": null, - "established": 0 - }, - { - "service": "MentalHealthCare", - "new": 18.5, - "established": 2.815352 - }, - { - "service": "Orthopedics", - "new": 39, - "established": 14 - }, - { - "service": "PrimaryCare", - "new": 71.75, - "established": 5.658357 - } - ], - "effectiveDate": "2022-08-26" - }, - "activeStatus": "A", - "address": { - "mailing": {}, - "physical": { - "zip": "02601-3766", - "city": "Hyannis", - "state": "MA", - "address1": "233 Stevens Street", - "address2": null, - "address3": null - } - }, - "classification": "Multi-Specialty CBOC", - "detailedServices": null, - "facilityType": "va_health_facility", - "feedback": { - "health": { - "primaryCareUrgent": 0.8799999952316284, - "primaryCareRoutine": 0.949999988079071 - }, - "effectiveDate": "2022-06-28" - }, - "hours": { - "friday": "700AM-400PM", - "monday": "700AM-400PM", - "sunday": "Closed", - "tuesday": "700AM-400PM", - "saturday": "Closed", - "thursday": "700AM-400PM", - "wednesday": "700AM-400PM" - }, - "id": "vha_650GB", - "lat": 41.6515538, - "long": -70.2941667, - "mobile": false, - "name": "Hyannis VA Clinic", - "operatingStatus": { - "code": "NORMAL" - }, - "operationalHoursSpecialInstructions": null, - "phone": { - "fax": "508-771-0940", - "main": "508-771-3190", - "pharmacy": "866-400-1241", - "afterHours": "401-273-7100 x13997", - "patientAdvocate": "401-457-3093", - "mentalHealthClinic": "401-273-7100 x 1745", - "enrollmentCoordinator": "401-273-7100 x12496", - "healthConnect": null - }, - "services": { - "other": [], - "health": [ - "Cardiology", - "Dermatology", - "Gastroenterology", - "MentalHealthCare", - "Nutrition", - "Orthopedics", - "Orthopedics", - "Podiatry", - "PrimaryCare" - ], - "lastUpdated": "2022-08-26" - }, - "uniqueId": "650GB", - "visn": "1", - "website": "https://www.va.gov/providence-health-care/locations/hyannis-va-clinic/" - } - } - ], - "meta": { - "pagination": { - "current_page": 1, - "prev_page": null, - "next_page": null, - "total_pages": 1 - } - }, - "links": { - "self": "https://staging-api.va.gov/v1/facilities/va?page=1&per_page=100&state=MA&type=health", - "first": "https://staging-api.va.gov/v1/facilities/va?per_page=100&state=MA&type=health", - "prev": null, - "next": null, - "last": "https://staging-api.va.gov/v1/facilities/va?page=1&per_page=100&state=MA&type=health" - } -} diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockPrefill.json b/src/applications/hca/tests/e2e/fixtures/mocks/mockPrefill.json deleted file mode 100644 index 19b1150e9df2..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockPrefill.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "formData": { - "veteranFullName": { "first": "Julio", "middle": "E", "last": "Hunter" }, - "gender": "M", - "veteranDateOfBirth": "1951-11-18", - "veteranSocialSecurityNumber": "796378321", - "homePhone": "6575107441", - "email": "vets.gov.user+71@gmail.com", - "lastServiceBranch": "air force", - "lastEntryDate": "1977-06-03", - "lastDischargeDate": "1998-06-30", - "dischargeType": "honorable" - }, - "metadata": { - "version": 0, - "prefill": true, - "returnUrl": "/check-your-personal-information" - } -} diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockUser.js b/src/applications/hca/tests/e2e/fixtures/mocks/mockUser.js deleted file mode 100644 index 6aa3e10e69f0..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockUser.js +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable camelcase */ -const mockUser = { - data: { - id: '', - type: 'users_scaffolds', - attributes: { - profile: { - sign_in: { - service_name: 'idme', - }, - email: 'vets.gov.user+71@gmail.com', - loa: { current: 3 }, - first_name: 'Julio', - middle_name: 'E', - last_name: 'Hunter', - gender: 'M', - birth_date: '1951-11-18', - verified: true, - }, - veteran_status: { - status: 'OK', - is_veteran: true, - }, - inProgressForms: [], - prefillsAvailable: ['1010ez'], - services: [ - 'facilities', - 'hca', - 'edu-benefits', - 'evss-claims', - 'user-profile', - 'rx', - 'messaging', - ], - va_profile: { - status: 'OK', - birth_date: '19511118', - family_name: 'Hunter', - gender: 'M', - given_names: ['Julio', 'E'], - }, - vet360ContactInformation: { - email: null, - residentialAddress: null, - mailingAddress: { - addressLine1: '123 elm st', - addressLine2: null, - addressLine3: null, - addressPou: 'CORRESPONDENCE', - addressType: 'DOMESTIC', - city: 'Northampton', - countryName: 'United States', - countryCodeIso2: 'US', - countryCodeIso3: 'USA', - countryCodeFips: null, - countyCode: '36005', - countyName: 'Bronx County', - createdAt: '2020-12-10T20:02:29.000+00:00', - effectiveEndDate: null, - effectiveStartDate: '2021-01-20T02:29:58.000+00:00', - geocodeDate: '2021-01-20T02:31:05.000+00:00', - geocodePrecision: 31.0, - id: 208622, - internationalPostalCode: null, - latitude: 40.8293, - longitude: -73.9284, - province: null, - sourceDate: '2021-01-20T02:29:58.000+00:00', - sourceSystemUser: null, - stateCode: 'MA', - transactionId: '0edf4400-9de7-442c-ae65-abf192702cef', - updatedAt: '2021-01-20T02:31:06.000+00:00', - validationKey: null, - vet360Id: '1273500', - zipCode: '01060', - zipCodeSuffix: '2100', - }, - mobilePhone: null, - homePhone: null, - workPhone: { - areaCode: '270', - countryCode: '1', - createdAt: '2018-04-20T17:22:56.000+00:00', - extension: null, - effectiveEndDate: null, - effectiveStartDate: '2012-10-25T09:03:30.000+00:00', - id: 2270938, - isInternational: false, - isTextable: false, - isTextPermitted: false, - isTty: false, - isVoicemailable: false, - phoneNumber: '2323232', - phoneType: 'WORK', - sourceDate: '2012-10-25T09:03:30.000+00:00', - sourceSystemUser: null, - transactionId: 'DATA SEEDING', - updatedAt: '2018-04-20T17:22:56.000+00:00', - vet360Id: '1273500', - }, - temporaryPhone: null, - faxNumber: null, - textPermission: null, - }, - }, - }, - meta: { - errors: null, - }, -}; -/* eslint-enable camelcase */ - -export default mockUser; diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockUserInvalidDob.js b/src/applications/hca/tests/e2e/fixtures/mocks/mockUserInvalidDob.js deleted file mode 100644 index 9dca4842d1b4..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockUserInvalidDob.js +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable camelcase */ -const mockUser = { - data: { - id: '', - type: 'users_scaffolds', - attributes: { - profile: { - sign_in: { - service_name: 'idme', - }, - email: 'vets.gov.user+71@gmail.com', - loa: { current: 3 }, - first_name: 'Julio', - middle_name: 'E', - last_name: 'Hunter', - gender: 'M', - birth_date: '1851-11-18', - verified: true, - }, - veteran_status: { - status: 'OK', - is_veteran: true, - }, - inProgressForms: [], - prefillsAvailable: ['1010ez'], - services: [ - 'facilities', - 'hca', - 'edu-benefits', - 'evss-claims', - 'user-profile', - 'rx', - 'messaging', - ], - va_profile: { - status: 'OK', - birth_date: '19511118', - family_name: 'Hunter', - gender: 'M', - given_names: ['Julio', 'E'], - }, - vet360ContactInformation: { - email: null, - residentialAddress: null, - mailingAddress: { - addressLine1: '123 elm st', - addressLine2: null, - addressLine3: null, - addressPou: 'CORRESPONDENCE', - addressType: 'DOMESTIC', - city: 'Northampton', - countryName: 'United States', - countryCodeIso2: 'US', - countryCodeIso3: 'USA', - countryCodeFips: null, - countyCode: '36005', - countyName: 'Bronx County', - createdAt: '2020-12-10T20:02:29.000+00:00', - effectiveEndDate: null, - effectiveStartDate: '2021-01-20T02:29:58.000+00:00', - geocodeDate: '2021-01-20T02:31:05.000+00:00', - geocodePrecision: 31.0, - id: 208622, - internationalPostalCode: null, - latitude: 40.8293, - longitude: -73.9284, - province: null, - sourceDate: '2021-01-20T02:29:58.000+00:00', - sourceSystemUser: null, - stateCode: 'MA', - transactionId: '0edf4400-9de7-442c-ae65-abf192702cef', - updatedAt: '2021-01-20T02:31:06.000+00:00', - validationKey: null, - vet360Id: '1273500', - zipCode: '01060', - zipCodeSuffix: '2100', - }, - mobilePhone: null, - homePhone: null, - workPhone: { - areaCode: '270', - countryCode: '1', - createdAt: '2018-04-20T17:22:56.000+00:00', - extension: null, - effectiveEndDate: null, - effectiveStartDate: '2012-10-25T09:03:30.000+00:00', - id: 2270938, - isInternational: false, - isTextable: false, - isTextPermitted: false, - isTty: false, - isVoicemailable: false, - phoneNumber: '2323232', - phoneType: 'WORK', - sourceDate: '2012-10-25T09:03:30.000+00:00', - sourceSystemUser: null, - transactionId: 'DATA SEEDING', - updatedAt: '2018-04-20T17:22:56.000+00:00', - vet360Id: '1273500', - }, - temporaryPhone: null, - faxNumber: null, - textPermission: null, - }, - }, - }, - meta: { - errors: null, - }, -}; -/* eslint-enable camelcase */ - -export default mockUser; diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/mockUserNoDob.js b/src/applications/hca/tests/e2e/fixtures/mocks/mockUserNoDob.js deleted file mode 100644 index 5e184db8ab45..000000000000 --- a/src/applications/hca/tests/e2e/fixtures/mocks/mockUserNoDob.js +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable camelcase */ -const mockUser = { - data: { - id: '', - type: 'users_scaffolds', - attributes: { - profile: { - sign_in: { - service_name: 'idme', - }, - email: 'vets.gov.user+71@gmail.com', - loa: { current: 3 }, - first_name: 'Julio', - middle_name: 'E', - last_name: 'Hunter', - gender: 'M', - verified: true, - }, - veteran_status: { - status: 'OK', - is_veteran: true, - }, - inProgressForms: [], - prefillsAvailable: ['1010ez'], - services: [ - 'facilities', - 'hca', - 'edu-benefits', - 'evss-claims', - 'user-profile', - 'rx', - 'messaging', - ], - va_profile: { - status: 'OK', - family_name: 'Hunter', - gender: 'M', - given_names: ['Julio', 'E'], - }, - vet360ContactInformation: { - email: null, - residentialAddress: null, - mailingAddress: { - addressLine1: '123 elm st', - addressLine2: null, - addressLine3: null, - addressPou: 'CORRESPONDENCE', - addressType: 'DOMESTIC', - city: 'Northampton', - countryName: 'United States', - countryCodeIso2: 'US', - countryCodeIso3: 'USA', - countryCodeFips: null, - countyCode: '36005', - countyName: 'Bronx County', - createdAt: '2020-12-10T20:02:29.000+00:00', - effectiveEndDate: null, - effectiveStartDate: '2021-01-20T02:29:58.000+00:00', - geocodeDate: '2021-01-20T02:31:05.000+00:00', - geocodePrecision: 31.0, - id: 208622, - internationalPostalCode: null, - latitude: 40.8293, - longitude: -73.9284, - province: null, - sourceDate: '2021-01-20T02:29:58.000+00:00', - sourceSystemUser: null, - stateCode: 'MA', - transactionId: '0edf4400-9de7-442c-ae65-abf192702cef', - updatedAt: '2021-01-20T02:31:06.000+00:00', - validationKey: null, - vet360Id: '1273500', - zipCode: '01060', - zipCodeSuffix: '2100', - }, - mobilePhone: null, - homePhone: null, - workPhone: { - areaCode: '270', - countryCode: '1', - createdAt: '2018-04-20T17:22:56.000+00:00', - extension: null, - effectiveEndDate: null, - effectiveStartDate: '2012-10-25T09:03:30.000+00:00', - id: 2270938, - isInternational: false, - isTextable: false, - isTextPermitted: false, - isTty: false, - isVoicemailable: false, - phoneNumber: '2323232', - phoneType: 'WORK', - sourceDate: '2012-10-25T09:03:30.000+00:00', - sourceSystemUser: null, - transactionId: 'DATA SEEDING', - updatedAt: '2018-04-20T17:22:56.000+00:00', - vet360Id: '1273500', - }, - temporaryPhone: null, - faxNumber: null, - textPermission: null, - 'view:totalDisabilityRating': 90, - }, - }, - }, - meta: { - errors: null, - }, -}; -/* eslint-enable camelcase */ - -export default mockUser; diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/prefill.json b/src/applications/hca/tests/e2e/fixtures/mocks/prefill.json new file mode 100644 index 000000000000..86d5b1473918 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/prefill.json @@ -0,0 +1,22 @@ +{ + "statusCode": 200, + "body": { + "formData": { + "veteranFullName": { "first": "Julio", "middle": "E", "last": "Hunter" }, + "gender": "M", + "veteranDateOfBirth": "1951-11-18", + "veteranSocialSecurityNumber": "796378321", + "homePhone": "6575107441", + "email": "vets.gov.user+71@gmail.com", + "lastServiceBranch": "air force", + "lastEntryDate": "1977-06-03", + "lastDischargeDate": "1998-06-30", + "dischargeType": "honorable" + }, + "metadata": { + "version": 0, + "prefill": true, + "returnUrl": "/check-your-personal-information" + } + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/save-in-progress.json b/src/applications/hca/tests/e2e/fixtures/mocks/save-in-progress.json new file mode 100644 index 000000000000..3d98b9357c03 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/save-in-progress.json @@ -0,0 +1,30 @@ +{ + "statusCode": 200, + "body": { + "data": { + "id": "", + "type": "in_progress_forms", + "attributes": { + "formId": "1010ez", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z", + "metadata": { + "version": 0, + "returnUrl": "/check-your-personal-information", + "savedAt": 1735707600000, + "submission": { + "status": false, + "errorMessage": false, + "id": false, + "timestamp": false, + "hasAttemptedSubmit": false + }, + "createdAt": 1735707600000, + "expiresAt": 1740805200000, + "lastUpdated": 1735707600000, + "inProgressFormId": 12345 + } + } + } + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/submission.json b/src/applications/hca/tests/e2e/fixtures/mocks/submission.json new file mode 100644 index 000000000000..2721f251dee1 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/submission.json @@ -0,0 +1,7 @@ +{ + "statusCode": 200, + "body": { + "formSubmissionId": "123fake-submission-id-567", + "timestamp": 1736357117521 + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/user.invalidDob.json b/src/applications/hca/tests/e2e/fixtures/mocks/user.invalidDob.json new file mode 100644 index 000000000000..fd78e498b283 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/user.invalidDob.json @@ -0,0 +1,116 @@ +{ + "data": { + "id": "", + "type": "users_scaffolds", + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "vets.gov.user+71@gmail.com", + "loa": { + "current": 3 + }, + "first_name": "Julio", + "middle_name": "E", + "last_name": "Hunter", + "gender": "M", + "birth_date": "1851-11-18", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true + }, + "inProgressForms": [], + "prefillsAvailable": [ + "1010ez" + ], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "user-profile", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "birth_date": "19511118", + "family_name": "Hunter", + "gender": "M", + "given_names": [ + "Julio", + "E" + ] + }, + "vet360ContactInformation": { + "email": null, + "residentialAddress": null, + "mailingAddress": { + "addressLine1": "123 elm st", + "addressLine2": null, + "addressLine3": null, + "addressPou": "CORRESPONDENCE", + "addressType": "DOMESTIC", + "city": "Northampton", + "countryName": "United States", + "countryCodeIso2": "US", + "countryCodeIso3": "USA", + "countryCodeFips": null, + "countyCode": "36005", + "countyName": "Bronx County", + "createdAt": "2020-12-10T20:02:29.000+00:00", + "effectiveEndDate": null, + "effectiveStartDate": "2021-01-20T02:29:58.000+00:00", + "geocodeDate": "2021-01-20T02:31:05.000+00:00", + "geocodePrecision": 31, + "id": 208622, + "internationalPostalCode": null, + "latitude": 40.8293, + "longitude": -73.9284, + "province": null, + "sourceDate": "2021-01-20T02:29:58.000+00:00", + "sourceSystemUser": null, + "stateCode": "MA", + "transactionId": "0edf4400-9de7-442c-ae65-abf192702cef", + "updatedAt": "2021-01-20T02:31:06.000+00:00", + "validationKey": null, + "vet360Id": "1273500", + "zipCode": "01060", + "zipCodeSuffix": "2100" + }, + "mobilePhone": null, + "homePhone": null, + "workPhone": { + "areaCode": "270", + "countryCode": "1", + "createdAt": "2018-04-20T17:22:56.000+00:00", + "extension": null, + "effectiveEndDate": null, + "effectiveStartDate": "2012-10-25T09:03:30.000+00:00", + "id": 2270938, + "isInternational": false, + "isTextable": false, + "isTextPermitted": false, + "isTty": false, + "isVoicemailable": false, + "phoneNumber": "2323232", + "phoneType": "WORK", + "sourceDate": "2012-10-25T09:03:30.000+00:00", + "sourceSystemUser": null, + "transactionId": "DATA SEEDING", + "updatedAt": "2018-04-20T17:22:56.000+00:00", + "vet360Id": "1273500" + }, + "temporaryPhone": null, + "faxNumber": null, + "textPermission": null + } + } + }, + "meta": { + "errors": null + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/user.json b/src/applications/hca/tests/e2e/fixtures/mocks/user.json new file mode 100644 index 000000000000..9c0e09fcd76f --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/user.json @@ -0,0 +1,111 @@ +{ + "data": { + "id": "", + "type": "users_scaffolds", + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "vets.gov.user+71@gmail.com", + "loa": { + "current": 3 + }, + "first_name": "Julio", + "middle_name": "E", + "last_name": "Hunter", + "gender": "M", + "birth_date": "1951-11-18", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true + }, + "inProgressForms": [], + "prefillsAvailable": ["1010ez"], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "user-profile", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "birth_date": "19511118", + "family_name": "Hunter", + "gender": "M", + "given_names": ["Julio", "E"] + }, + "vet360ContactInformation": { + "email": null, + "residentialAddress": null, + "mailingAddress": { + "addressLine1": "123 elm st", + "addressLine2": null, + "addressLine3": null, + "addressPou": "CORRESPONDENCE", + "addressType": "DOMESTIC", + "city": "Northampton", + "countryName": "United States", + "countryCodeIso2": "US", + "countryCodeIso3": "USA", + "countryCodeFips": null, + "countyCode": "36005", + "countyName": "Bronx County", + "createdAt": "2020-12-10T20:02:29.000+00:00", + "effectiveEndDate": null, + "effectiveStartDate": "2021-01-20T02:29:58.000+00:00", + "geocodeDate": "2021-01-20T02:31:05.000+00:00", + "geocodePrecision": 31, + "id": 208622, + "internationalPostalCode": null, + "latitude": 40.8293, + "longitude": -73.9284, + "province": null, + "sourceDate": "2021-01-20T02:29:58.000+00:00", + "sourceSystemUser": null, + "stateCode": "MA", + "transactionId": "0edf4400-9de7-442c-ae65-abf192702cef", + "updatedAt": "2021-01-20T02:31:06.000+00:00", + "validationKey": null, + "vet360Id": "1273500", + "zipCode": "01060", + "zipCodeSuffix": "2100" + }, + "mobilePhone": null, + "homePhone": null, + "workPhone": { + "areaCode": "270", + "countryCode": "1", + "createdAt": "2018-04-20T17:22:56.000+00:00", + "extension": null, + "effectiveEndDate": null, + "effectiveStartDate": "2012-10-25T09:03:30.000+00:00", + "id": 2270938, + "isInternational": false, + "isTextable": false, + "isTextPermitted": false, + "isTty": false, + "isVoicemailable": false, + "phoneNumber": "2323232", + "phoneType": "WORK", + "sourceDate": "2012-10-25T09:03:30.000+00:00", + "sourceSystemUser": null, + "transactionId": "DATA SEEDING", + "updatedAt": "2018-04-20T17:22:56.000+00:00", + "vet360Id": "1273500" + }, + "temporaryPhone": null, + "faxNumber": null, + "textPermission": null + } + } + }, + "meta": { + "errors": null + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/user.noDob.json b/src/applications/hca/tests/e2e/fixtures/mocks/user.noDob.json new file mode 100644 index 000000000000..f21f51f76a44 --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/user.noDob.json @@ -0,0 +1,115 @@ +{ + "data": { + "id": "", + "type": "users_scaffolds", + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "vets.gov.user+71@gmail.com", + "loa": { + "current": 3 + }, + "first_name": "Julio", + "middle_name": "E", + "last_name": "Hunter", + "gender": "M", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true + }, + "inProgressForms": [], + "prefillsAvailable": [ + "1010ez" + ], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "user-profile", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "family_name": "Hunter", + "gender": "M", + "given_names": [ + "Julio", + "E" + ] + }, + "vet360ContactInformation": { + "email": null, + "residentialAddress": null, + "mailingAddress": { + "addressLine1": "123 elm st", + "addressLine2": null, + "addressLine3": null, + "addressPou": "CORRESPONDENCE", + "addressType": "DOMESTIC", + "city": "Northampton", + "countryName": "United States", + "countryCodeIso2": "US", + "countryCodeIso3": "USA", + "countryCodeFips": null, + "countyCode": "36005", + "countyName": "Bronx County", + "createdAt": "2020-12-10T20:02:29.000+00:00", + "effectiveEndDate": null, + "effectiveStartDate": "2021-01-20T02:29:58.000+00:00", + "geocodeDate": "2021-01-20T02:31:05.000+00:00", + "geocodePrecision": 31, + "id": 208622, + "internationalPostalCode": null, + "latitude": 40.8293, + "longitude": -73.9284, + "province": null, + "sourceDate": "2021-01-20T02:29:58.000+00:00", + "sourceSystemUser": null, + "stateCode": "MA", + "transactionId": "0edf4400-9de7-442c-ae65-abf192702cef", + "updatedAt": "2021-01-20T02:31:06.000+00:00", + "validationKey": null, + "vet360Id": "1273500", + "zipCode": "01060", + "zipCodeSuffix": "2100" + }, + "mobilePhone": null, + "homePhone": null, + "workPhone": { + "areaCode": "270", + "countryCode": "1", + "createdAt": "2018-04-20T17:22:56.000+00:00", + "extension": null, + "effectiveEndDate": null, + "effectiveStartDate": "2012-10-25T09:03:30.000+00:00", + "id": 2270938, + "isInternational": false, + "isTextable": false, + "isTextPermitted": false, + "isTty": false, + "isVoicemailable": false, + "phoneNumber": "2323232", + "phoneType": "WORK", + "sourceDate": "2012-10-25T09:03:30.000+00:00", + "sourceSystemUser": null, + "transactionId": "DATA SEEDING", + "updatedAt": "2018-04-20T17:22:56.000+00:00", + "vet360Id": "1273500" + }, + "temporaryPhone": null, + "faxNumber": null, + "textPermission": null, + "view:totalDisabilityRating": 90 + } + } + }, + "meta": { + "errors": null + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/fixtures/mocks/vamc-ehr.json b/src/applications/hca/tests/e2e/fixtures/mocks/vamc-ehr.json new file mode 100644 index 000000000000..2155a7f10eae --- /dev/null +++ b/src/applications/hca/tests/e2e/fixtures/mocks/vamc-ehr.json @@ -0,0 +1,8 @@ +{ + "data": { + "nodeQuery": { + "count": 0, + "entities": [] + } + } +} \ No newline at end of file diff --git a/src/applications/hca/tests/e2e/hca-authenticated-dob.cypress.spec.js b/src/applications/hca/tests/e2e/hca-authenticated-dob.cypress.spec.js index b3ecb762a418..9d26bea12d77 100644 --- a/src/applications/hca/tests/e2e/hca-authenticated-dob.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-authenticated-dob.cypress.spec.js @@ -1,11 +1,11 @@ import { getTime } from 'date-fns'; import manifest from '../../manifest.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockUserNoDob from './fixtures/mocks/mockUserNoDob'; -import mockUserInvalidDob from './fixtures/mocks/mockUserInvalidDob'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUserInvalidDob from './fixtures/mocks/user.invalidDob.json'; +import mockUser from './fixtures/mocks/user.json'; +import mockUserNoDob from './fixtures/mocks/user.noDob.json'; import { goToNextPage } from './utils'; describe('HCA-User-Authenticated-Without-DOB', () => { @@ -14,10 +14,11 @@ describe('HCA-User-Authenticated-Without-DOB', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -28,10 +29,7 @@ describe('HCA-User-Authenticated-Without-DOB', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { @@ -73,10 +71,11 @@ describe('HCA-User-Authenticated-With-Invalid-DOB', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -87,10 +86,7 @@ describe('HCA-User-Authenticated-With-Invalid-DOB', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { @@ -132,10 +128,11 @@ describe('HCA-User-Authenticated-With-DOB', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -146,10 +143,7 @@ describe('HCA-User-Authenticated-With-DOB', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { diff --git a/src/applications/hca/tests/e2e/hca-enrollment-status.cypress.spec.js b/src/applications/hca/tests/e2e/hca-enrollment-status.cypress.spec.js index 8d1b9a4cb439..cc086a991b91 100644 --- a/src/applications/hca/tests/e2e/hca-enrollment-status.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-enrollment-status.cypress.spec.js @@ -1,12 +1,12 @@ import { getTime } from 'date-fns'; import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; import { - HCA_ENROLLMENT_STATUSES, HCA_APPLY_ALLOWED_STATUSES, + HCA_ENROLLMENT_STATUSES, } from '../../utils/constants'; +import featureToggles from './fixtures/mocks/feature-toggles.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUser from './fixtures/mocks/user.json'; Object.values(HCA_ENROLLMENT_STATUSES).forEach(status => { describe(`HCA-Enrollment-Status: ${status}`, () => { @@ -34,10 +34,7 @@ Object.values(HCA_ENROLLMENT_STATUSES).forEach(status => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { @@ -93,10 +90,7 @@ describe('HCA-Enrollment-Status: Server Error', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { diff --git a/src/applications/hca/tests/e2e/hca-household.cypress.spec.js b/src/applications/hca/tests/e2e/hca-household.cypress.spec.js index 1f3b0a39419c..5b86b3570aff 100644 --- a/src/applications/hca/tests/e2e/hca-household.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-household.cypress.spec.js @@ -1,19 +1,19 @@ import { getTime } from 'date-fns'; import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; import maxTestData from './fixtures/data/maximal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import featureToggles from './fixtures/mocks/feature-toggles.json'; +import mockFacilities from './fixtures/mocks/facilities.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUser from './fixtures/mocks/user.json'; import { acceptPrivacyAgreement, - advanceToHousehold, advanceFromHouseholdToReview, - goToNextPage, + advanceToHousehold, fillDependentBasicInformation, fillSpousalBasicInformation, + goToNextPage, } from './utils'; -import mockFacilities from './fixtures/mocks/mockFacilitiesV1.json'; const { data: testData } = maxTestData; @@ -23,14 +23,12 @@ describe('HCA-Household-Non-Disclosure', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -173,14 +171,12 @@ describe('HCA-Household-Spousal-Disclosure', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -396,14 +392,12 @@ describe('HCA-Household-Dependent-Disclosure', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -1075,14 +1069,12 @@ describe('HCA-Household-Full-Disclosure', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { diff --git a/src/applications/hca/tests/e2e/hca-insurance-v2.cypress.spec.js b/src/applications/hca/tests/e2e/hca-insurance-v2.cypress.spec.js index e3d04de3fea4..b31b4d8d57b4 100644 --- a/src/applications/hca/tests/e2e/hca-insurance-v2.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-insurance-v2.cypress.spec.js @@ -1,10 +1,10 @@ import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles-insurance.json'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; import maxTestData from './fixtures/data/maximal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import featureToggles from './fixtures/mocks/feature-toggles.insurance.json'; import { - goToNextPage, fillTextWebComponent, + goToNextPage, selectYesNoWebComponent, } from './utils'; @@ -17,10 +17,9 @@ const APIs = { describe('HCA-Health-Insurance-Information', () => { const setupGuestUser = () => { cy.intercept('GET', APIs.features, featureToggles).as('mockFeatures'); - cy.intercept('GET', APIs.enrollment, { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept('GET', APIs.enrollment, mockEnrollmentStatus).as( + 'mockEnrollmentStatus', + ); cy.visit(manifest.rootUrl); cy.wait(['@mockFeatures']); }; diff --git a/src/applications/hca/tests/e2e/hca-keyboard-only.cypress.spec.js b/src/applications/hca/tests/e2e/hca-keyboard-only.cypress.spec.js index a8de4ff56c84..eb5fe0b8efe1 100644 --- a/src/applications/hca/tests/e2e/hca-keyboard-only.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-keyboard-only.cypress.spec.js @@ -1,16 +1,16 @@ import { format, subYears } from 'date-fns'; import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; import maxTestData from './fixtures/data/maximal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import featureToggles from './fixtures/mocks/feature-toggles.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUser from './fixtures/mocks/user.json'; import { fillAddressWithKeyboard, fillDateWithKeyboard, fillNameWithKeyboard, - selectRadioWithKeyboard, selectDropdownWithKeyboard, + selectRadioWithKeyboard, } from './utils'; describe('HCA-Keyboard-Only', () => { @@ -24,10 +24,11 @@ describe('HCA-Keyboard-Only', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -38,10 +39,9 @@ describe('HCA-Keyboard-Only', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('GET', '/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('GET', '/v0/in_progress_forms/1010ez', mockPrefill).as( + 'mockSip', + ); cy.intercept('PUT', '/v0/in_progress_forms/1010ez', {}); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, diff --git a/src/applications/hca/tests/e2e/hca-registration-only.cypress.spec.js b/src/applications/hca/tests/e2e/hca-registration-only.cypress.spec.js index e6386b6d022a..479e36b52e25 100644 --- a/src/applications/hca/tests/e2e/hca-registration-only.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-registration-only.cypress.spec.js @@ -1,9 +1,9 @@ import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles-reg-only.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; import minTestData from './fixtures/data/minimal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import featureToggles from './fixtures/mocks/feature-toggles.reg-only.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUser from './fixtures/mocks/user.json'; import { goToNextPage } from './utils'; const { data: testData } = minTestData; @@ -17,19 +17,16 @@ const APIs = { describe('HCA-Registration-Only-Authenticated-User', () => { const setupAuthUser = ({ userPercentOfDisability = 0 }) => { cy.intercept('GET', APIs.features, featureToggles).as('mockFeatures'); - cy.intercept('GET', APIs.enrollment, { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept('GET', APIs.enrollment, mockEnrollmentStatus).as( + 'mockEnrollmentStatus', + ); cy.intercept(APIs.disability, { statusCode: 200, body: { data: { id: '', type: 'hash', attributes: { userPercentOfDisability } }, }, }).as('mockDisabilityRating'); - cy.intercept(APIs.prefill, { statusCode: 200, body: mockPrefill }).as( - 'mockSip', - ); + cy.intercept(APIs.prefill, mockPrefill).as('mockSip'); cy.login(mockUser); cy.visit(manifest.rootUrl); cy.wait([ @@ -102,10 +99,9 @@ describe('HCA-Registration-Only-Authenticated-User', () => { describe('HCA-Registration-Only-Guest-User', () => { const setupGuestUser = () => { cy.intercept('GET', APIs.features, featureToggles).as('mockFeatures'); - cy.intercept('GET', APIs.enrollment, { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept('GET', APIs.enrollment, mockEnrollmentStatus).as( + 'mockEnrollmentStatus', + ); cy.visit(manifest.rootUrl); cy.wait(['@mockFeatures']); }; diff --git a/src/applications/hca/tests/e2e/hca-shortform.cypress.spec.js b/src/applications/hca/tests/e2e/hca-shortform.cypress.spec.js index 47e917bc6e8c..a50d95f0c043 100644 --- a/src/applications/hca/tests/e2e/hca-shortform.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-shortform.cypress.spec.js @@ -1,11 +1,11 @@ import { getTime } from 'date-fns'; -import mockFacilities from 'applications/hca/tests/e2e/fixtures/mocks/mockFacilitiesV1.json'; import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockUser from './fixtures/mocks/mockUser'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; -import mockPrefill from './fixtures/mocks/mockPrefill.json'; import minTestData from './fixtures/data/minimal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import mockFacilities from './fixtures/mocks/facilities.json'; +import featureToggles from './fixtures/mocks/feature-toggles.json'; +import mockPrefill from './fixtures/mocks/prefill.json'; +import mockUser from './fixtures/mocks/user.json'; import { acceptPrivacyAgreement, goToNextPage, @@ -22,14 +22,12 @@ describe('HCA-Shortform-Authenticated-High-Disability', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -145,10 +143,11 @@ describe('HCA-Shortform-Authenticated-Low-Disability', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('/v0/health_care_applications/rating_info', { statusCode: 200, body: { @@ -159,10 +158,7 @@ describe('HCA-Shortform-Authenticated-Low-Disability', () => { }, }, }).as('mockDisabilityRating'); - cy.intercept('/v0/in_progress_forms/1010ez', { - statusCode: 200, - body: mockPrefill, - }).as('mockSip'); + cy.intercept('/v0/in_progress_forms/1010ez', mockPrefill).as('mockSip'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { @@ -226,10 +222,11 @@ describe('HCA-Shortform-UnAuthenticated', () => { cy.intercept('GET', '/v0/feature_toggles*', featureToggles).as( 'mockFeatures', ); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ).as('mockEnrollmentStatus'); cy.intercept('POST', '/v0/health_care_applications', { statusCode: 200, body: { diff --git a/src/applications/hca/tests/e2e/hca-tera-branching.cypress.spec.js b/src/applications/hca/tests/e2e/hca-tera-branching.cypress.spec.js index 42b35e2800ea..0ef3fb934a5c 100644 --- a/src/applications/hca/tests/e2e/hca-tera-branching.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca-tera-branching.cypress.spec.js @@ -1,7 +1,7 @@ import manifest from '../../manifest.json'; -import featureToggles from './fixtures/mocks/feature-toggles-tera.json'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; import maxTestData from './fixtures/data/maximal-test.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import featureToggles from './fixtures/mocks/feature-toggles.tera.json'; import { goToNextPage } from './utils'; const { data: testData } = maxTestData; @@ -13,10 +13,9 @@ const APIs = { describe('HCA-TERA-Branching', () => { const setupGuestUser = () => { cy.intercept('GET', APIs.features, featureToggles).as('mockFeatures'); - cy.intercept('GET', APIs.enrollment, { - statusCode: 404, - body: mockEnrollmentStatus, - }).as('mockEnrollmentStatus'); + cy.intercept('GET', APIs.enrollment, mockEnrollmentStatus).as( + 'mockEnrollmentStatus', + ); cy.visit(manifest.rootUrl); cy.wait(['@mockFeatures']); }; diff --git a/src/applications/hca/tests/e2e/hca.cypress.spec.js b/src/applications/hca/tests/e2e/hca.cypress.spec.js index b223cbb7cb82..b828f9666e1c 100644 --- a/src/applications/hca/tests/e2e/hca.cypress.spec.js +++ b/src/applications/hca/tests/e2e/hca.cypress.spec.js @@ -1,12 +1,11 @@ import path from 'path'; import testForm from 'platform/testing/e2e/cypress/support/form-tester'; import { createTestConfig } from 'platform/testing/e2e/cypress/support/form-tester/utilities'; - import formConfig from '../../config/form'; import manifest from '../../manifest.json'; +import mockEnrollmentStatus from './fixtures/mocks/enrollment-status.json'; +import mockFacilities from './fixtures/mocks/facilities.json'; import featureToggles from './fixtures/mocks/feature-toggles.json'; -import mockFacilities from './fixtures/mocks/mockFacilitiesV1.json'; -import mockEnrollmentStatus from './fixtures/mocks/mockEnrollmentStatus.json'; import { acceptPrivacyAgreement, goToNextPage, @@ -139,10 +138,11 @@ const testConfig = createTestConfig( formSubmissionId: '123fake-submission-id-567', timestamp: '2016-05-16', }); - cy.intercept('GET', '/v0/health_care_applications/enrollment_status*', { - statusCode: 404, - body: mockEnrollmentStatus, - }); + cy.intercept( + 'GET', + '/v0/health_care_applications/enrollment_status*', + mockEnrollmentStatus, + ); cy.intercept( 'GET', '/v0/health_care_applications/facilities?*', diff --git a/src/applications/ivc-champva/10-7959a/containers/ConfirmationPage.jsx b/src/applications/ivc-champva/10-7959a/containers/ConfirmationPage.jsx index 2c5236eaa5d6..9c2893e694d0 100644 --- a/src/applications/ivc-champva/10-7959a/containers/ConfirmationPage.jsx +++ b/src/applications/ivc-champva/10-7959a/containers/ConfirmationPage.jsx @@ -95,9 +95,9 @@ export class ConfirmationPage extends React.Component {
    ATTN: APPEALS
    - PO Box 460948 + PO Box 600
    - Denver, CO 80246-0948 + Spring City, PA 19475

    How to contact us about CHAMPVA claims

    @@ -115,9 +115,9 @@ export class ConfirmationPage extends React.Component {
    ATTN: CHAMPVA Claims
    - PO Box 460948 + PO Box 500
    - Denver, CO 80246-0948 + Spring City, PA 19475

    You can also contact us online through Ask VA.

    diff --git a/src/applications/login/containers/MhvAccess.jsx b/src/applications/login/containers/MhvAccess.jsx index ef7c7b8db2d9..3155b5377177 100644 --- a/src/applications/login/containers/MhvAccess.jsx +++ b/src/applications/login/containers/MhvAccess.jsx @@ -6,18 +6,18 @@ export default function MhvAccess() {

    - Get temporary access to My HealtheVet + Access the My HealtheVet sign-in option

    - Some groups are approved to access the My HealtheVet sign-in option - until they create a new modern account. This sign-in process may - change in the future. + Get temporary access to the My HealtheVet sign-in option. This sign-in + process may change in the future.

    +

    Sign in

    login({ policy: 'mhv' })} - text="Sign in with My HealtheVet" + text="My HealtheVet" data-testid="accessMhvBtn" />
    diff --git a/src/applications/login/tests/containers/MhvAccess.unit.spec.js b/src/applications/login/tests/containers/MhvAccess.unit.spec.js index ba0dbaa1d43f..07aa069706fe 100644 --- a/src/applications/login/tests/containers/MhvAccess.unit.spec.js +++ b/src/applications/login/tests/containers/MhvAccess.unit.spec.js @@ -7,22 +7,24 @@ import MhvAccess from '../../containers/MhvAccess'; describe('MhvAccess', () => { it('renders main title', () => { const screen = renderInReduxProvider(); - const mainTitle = screen.getByRole('heading', { - name: /get temporary access to my healthevet/i, - }); + const mainTitle = screen.getByText( + /access the my healthevet sign-in option/i, + ); expect(mainTitle).to.exist; }); it('renders information paragraph', () => { const screen = renderInReduxProvider(); const description = screen.getByText( - /Some groups are approved to access the My HealtheVet sign-in option/i, + /get temporary access to the my healthevet sign-in option/i, ); expect(description).to.exist; }); it('renders button', () => { const screen = renderInReduxProvider(); + const signInHeading = screen.getByText(/sign in/i); + expect(signInHeading).to.exist; const accessButton = screen.getByTestId('accessMhvBtn'); expect(accessButton).to.exist; fireEvent.click(accessButton); diff --git a/src/applications/login/tests/mhv-access-page.cypress.spec.js b/src/applications/login/tests/mhv-access-page.cypress.spec.js new file mode 100644 index 000000000000..ad50826821fb --- /dev/null +++ b/src/applications/login/tests/mhv-access-page.cypress.spec.js @@ -0,0 +1,33 @@ +describe('My HealtheVet Access Page', () => { + beforeEach(() => { + cy.visit('/sign-in/mhv'); + }); + + it('displays the page title and description', () => { + cy.injectAxeThenAxeCheck(); + cy.get('#signin-signup-modal-title') + .should('exist') + .and('contain', 'Access the My HealtheVet sign-in option'); + cy.get('p.vads-u-measure--5') + .should('exist') + .and( + 'contain', + 'Get temporary access to the My HealtheVet sign-in option. This sign-in process may change in the future.', + ); + }); + + it('displays a sign-in button and respond to clicks', () => { + cy.injectAxeThenAxeCheck(); + cy.get('va-button[text="My HealtheVet"]').should('exist'); + cy.get('[data-testid="accessMhvBtn"]').click(); + }); + + it('should display the "Having trouble signing in?" section', () => { + cy.injectAxeThenAxeCheck(); + cy.contains('h2', 'Having trouble signing in?').should('exist'); + cy.contains( + 'p', + 'Contact the administrator who gave you access to this page.', + ).should('exist'); + }); +}); diff --git a/src/applications/mhv-landing-page/components/alerts/AlertAccountApiAlert.jsx b/src/applications/mhv-landing-page/components/alerts/AlertAccountApiAlert.jsx index a2d85198441f..fd0c2c01558b 100644 --- a/src/applications/mhv-landing-page/components/alerts/AlertAccountApiAlert.jsx +++ b/src/applications/mhv-landing-page/components/alerts/AlertAccountApiAlert.jsx @@ -66,7 +66,7 @@ const AlertAccountApiAlert = ({ page. Or check back later.

    - {errorCode.length > 0 ? ( + {errorCode > 0 ? (

    If the problem persists, call the My HealtheVet helpdesk at 877-327-0022 (TTY: 711). We’re here Monday through Friday, 8:00 @@ -96,13 +96,13 @@ const AlertAccountApiAlert = ({ AlertAccountApiAlert.defaultProps = { title: 'Error code 000: Contact the My HealtheVet help desk', - errorCode: '', + errorCode: 0, recordEvent: recordEventFn, testId: 'mhv-alert--mhv-registration', }; AlertAccountApiAlert.propTypes = { - errorCode: PropTypes.string, + errorCode: PropTypes.number, title: PropTypes.string, headline: PropTypes.string, recordEvent: PropTypes.func, diff --git a/src/applications/mhv-landing-page/mocks/api/user/mhvAccountStatus.js b/src/applications/mhv-landing-page/mocks/api/user/mhvAccountStatus.js index 96f12cbf3df3..87d00072dee2 100644 --- a/src/applications/mhv-landing-page/mocks/api/user/mhvAccountStatus.js +++ b/src/applications/mhv-landing-page/mocks/api/user/mhvAccountStatus.js @@ -21,7 +21,7 @@ const eightZeroOne = (req, res) => { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '801', + code: 801, }, ], }); @@ -33,7 +33,7 @@ const eightZeroFive = (req, res) => { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '805', + code: 805, }, ], }); @@ -45,7 +45,7 @@ const eightZeroSix = (req, res) => { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '802', + code: 802, }, ], }); @@ -57,7 +57,7 @@ const fiveZeroZero = (req, res) => { { title: 'The server responded with status 500', detail: 'things fall apart', - code: '500', + code: 500, }, ], }); @@ -69,22 +69,22 @@ const multiError = (req, res) => { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '802', + code: 802, }, { title: 'The server responded with status 500', detail: 'things fall apart', - code: '500', + code: 500, }, { title: 'The server responded with status 422', detail: 'things fall apart', - code: '805', + code: 805, }, { title: 'The server responded with status 422', detail: 'things fall apart', - code: '801', + code: 801, }, ], }); @@ -110,7 +110,7 @@ const accountStatusEightZeroOne = { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '801', + code: 801, }, ], }; @@ -120,7 +120,7 @@ const accountStatusFiveZeroZero = { { title: 'The server responded with status 500', detail: 'things fall apart', - code: '500', + code: 500, }, ], }; @@ -139,22 +139,22 @@ const accountStatusMultiError = { { title: 'The server responded with status 422', detail: 'things fall apart', - code: '802', + code: 802, }, { title: 'The server responded with status 500', detail: 'things fall apart', - code: '500', + code: 500, }, { title: 'The server responded with status 422', detail: 'things fall apart', - code: '805', + code: 805, }, { title: 'The server responded with status 422', detail: 'things fall apart', - code: '801', + code: 801, }, ], }; diff --git a/src/applications/mhv-landing-page/selectors/mhvAccountStatus.js b/src/applications/mhv-landing-page/selectors/mhvAccountStatus.js index af581754760c..8ff290ff8139 100644 --- a/src/applications/mhv-landing-page/selectors/mhvAccountStatus.js +++ b/src/applications/mhv-landing-page/selectors/mhvAccountStatus.js @@ -1,4 +1,4 @@ -const userActionErrorCodes = ['801', '805', '806', '807']; +const userActionErrorCodes = [801, 805, 806, 807]; export const mhvAccountStatusLoading = state => { return state?.myHealth?.accountStatus?.loading; diff --git a/src/applications/mhv-landing-page/tests/e2e/alerts/no-mhv-account.cypress.spec.js b/src/applications/mhv-landing-page/tests/e2e/alerts/no-mhv-account.cypress.spec.js index 6abbb12e21d9..77a87213d291 100644 --- a/src/applications/mhv-landing-page/tests/e2e/alerts/no-mhv-account.cypress.spec.js +++ b/src/applications/mhv-landing-page/tests/e2e/alerts/no-mhv-account.cypress.spec.js @@ -64,8 +64,6 @@ describe(`${appName} - MHV Registration Alert - `, () => { }); it('should reference a specific error', () => { - 'Tell the representative that you received'; - cy.injectAxeThenAxeCheck(); cy.findByText('Tell the representative that you received', { exact: false, diff --git a/src/applications/mhv-landing-page/tests/selectors/mhvAccountStatus.unit.spec.js b/src/applications/mhv-landing-page/tests/selectors/mhvAccountStatus.unit.spec.js new file mode 100644 index 000000000000..e1effba8d50d --- /dev/null +++ b/src/applications/mhv-landing-page/tests/selectors/mhvAccountStatus.unit.spec.js @@ -0,0 +1,81 @@ +import { expect } from 'chai'; +import { + mhvAccountStatusUsersuccess, + mhvAccountStatusErrorsSorted, + mhvAccountStatusUserError, +} from '../../selectors'; +import { accountStatusMultiError } from '../../mocks/api/user/mhvAccountStatus'; + +describe('mhvAccountStatusUsersuccess', () => { + it('returns false when loading', () => { + const loadingState = { + myHealth: { + accountStatus: { + loading: true, + }, + }, + }; + const result = mhvAccountStatusUsersuccess(loadingState); + expect(result).to.be.false; + }); + + it('returns false when errors', () => { + const errorState = { + myHealth: { + accountStatus: { + data: { + errors: [ + { + title: 'The server responded with status 422', + detail: 'things fall apart', + code: 801, + }, + ], + }, + }, + }, + }; + const result = mhvAccountStatusUsersuccess(errorState); + expect(result).to.be.false; + }); + + it('returns true not loading and no errors', () => { + const state = { + myHealth: { + accountStatus: { + data: {}, + }, + }, + }; + const result = mhvAccountStatusUsersuccess(state); + expect(result).to.be.true; + }); +}); + +describe('mhvAccountStatusErrorsSorted', () => { + it('prioritizes user errors', () => { + const state = { + myHealth: { + accountStatus: { + data: accountStatusMultiError, + }, + }, + }; + const result = mhvAccountStatusErrorsSorted(state); + expect(result[0].code).to.eq(805); + }); +}); + +describe('mhvAccountStatusUserError', () => { + it('returns user error code', () => { + const state = { + myHealth: { + accountStatus: { + data: accountStatusMultiError, + }, + }, + }; + const result = mhvAccountStatusUserError(state); + expect(result[0].code).to.eq(805); + }); +}); diff --git a/src/applications/mhv-medical-records/containers/App.jsx b/src/applications/mhv-medical-records/containers/App.jsx index b5ed2f83e654..7c375733db5f 100644 --- a/src/applications/mhv-medical-records/containers/App.jsx +++ b/src/applications/mhv-medical-records/containers/App.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom/cjs/react-router-dom.min'; import PropTypes from 'prop-types'; @@ -9,6 +9,7 @@ import { renderMHVDowntime, useDatadogRum, MhvSecondaryNav, + useBackToTop, } from '@department-of-veterans-affairs/mhv/exports'; import { DowntimeNotification, @@ -38,11 +39,8 @@ const App = ({ children }) => { ); const dispatch = useDispatch(); - - const [isHidden, setIsHidden] = useState(true); - const [height, setHeight] = useState(0); const location = useLocation(); - const measuredRef = useRef(); + const { measuredRef, isHidden } = useBackToTop(location); const atLandingPage = location.pathname === '/'; const scheduledDowntimes = useSelector( @@ -117,52 +115,6 @@ const App = ({ children }) => { }; useDatadogRum(datadogRumConfig); - useEffect( - () => { - if (height) { - // small screen (mobile) - if (window.innerWidth <= 481 && height > window.innerHeight * 4) { - setIsHidden(false); - } - // medium screen (desktop/tablet) - else if (window.innerWidth > 481 && height > window.innerHeight * 2) { - setIsHidden(false); - } - // default to hidden - else { - setIsHidden(true); - } - } - }, - [height, location], - ); - - const { current } = measuredRef; - - useEffect( - () => { - if (!current) return () => {}; - let isMounted = true; // Flag to prevent React state update on an unmounted component - - const resizeObserver = new ResizeObserver(() => { - requestAnimationFrame(() => { - if (isMounted && height !== current.offsetHeight) { - setHeight(current.offsetHeight); - } - }); - }); - resizeObserver.observe(current); - return () => { - isMounted = false; - if (current) { - resizeObserver.unobserve(current); - } - resizeObserver.disconnect(); - }; - }, - [current, height], - ); - useEffect( () => { // If the user is not whitelisted or feature flag is disabled, redirect them. @@ -242,6 +194,7 @@ const App = ({ children }) => { )}

    { + datadogRum.addAction( + dataDogActionNames.medicationsListPage.PAGINATION, + ); + }} max-page-list-length={MAX_PAGE_LIST_LENGTH} id="pagination" className="pagination vads-u-justify-content--center no-print" diff --git a/src/applications/mhv-medications/components/MedicationsList/MedicationsListCard.jsx b/src/applications/mhv-medications/components/MedicationsList/MedicationsListCard.jsx index d6d7d705487b..b86e41e360d5 100644 --- a/src/applications/mhv-medications/components/MedicationsList/MedicationsListCard.jsx +++ b/src/applications/mhv-medications/components/MedicationsList/MedicationsListCard.jsx @@ -53,7 +53,7 @@ const MedicationsListCard = ({ rx }) => { {rx.dispStatus !== 'Unknown' && rx.dispStatus !== 'Active: Non-VA' && ( -

    +

    Prescription number: {rx.prescriptionNumber}

    )} diff --git a/src/applications/mhv-medications/components/MedicationsList/MedicationsListFilter.jsx b/src/applications/mhv-medications/components/MedicationsList/MedicationsListFilter.jsx index 47f0cebe2d88..b6b49ccb4bbb 100644 --- a/src/applications/mhv-medications/components/MedicationsList/MedicationsListFilter.jsx +++ b/src/applications/mhv-medications/components/MedicationsList/MedicationsListFilter.jsx @@ -93,6 +93,9 @@ const MedicationsListFilter = props => { data-testid="rx-filter" ref={ref} level={3} + data-dd-action-name={ + dataDogActionNames.medicationsListPage.FILTER_LIST_ACCORDION + } uswds > diff --git a/src/applications/mhv-medications/components/MedicationsList/MedicationsListSort.jsx b/src/applications/mhv-medications/components/MedicationsList/MedicationsListSort.jsx index 96b1a1b54f2b..6b0067a82abe 100644 --- a/src/applications/mhv-medications/components/MedicationsList/MedicationsListSort.jsx +++ b/src/applications/mhv-medications/components/MedicationsList/MedicationsListSort.jsx @@ -55,7 +55,7 @@ const MedicationsListSort = props => { .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); datadogRum.addAction( - `click on ${capitalizedOption} Option - ${pageType.LIST}`, + `${capitalizedOption} Option - ${pageType.LIST}`, ); }} uswds diff --git a/src/applications/mhv-medications/components/PrescriptionDetails/GroupedMedications.jsx b/src/applications/mhv-medications/components/PrescriptionDetails/GroupedMedications.jsx index e9fb427bd8af..0de7921a490d 100644 --- a/src/applications/mhv-medications/components/PrescriptionDetails/GroupedMedications.jsx +++ b/src/applications/mhv-medications/components/PrescriptionDetails/GroupedMedications.jsx @@ -69,7 +69,7 @@ const GroupedMedications = props => { className="vads-u-border-top--1px vads-u-border-color--gray-lighter vads-u-margin-top--3" key={rx.prescriptionId} > -
    +

    Prescription number: {rx.prescriptionNumber}

    diff --git a/src/applications/mhv-medications/components/PrescriptionDetails/VaPrescription.jsx b/src/applications/mhv-medications/components/PrescriptionDetails/VaPrescription.jsx index 2f036618188b..a8c517eaa048 100644 --- a/src/applications/mhv-medications/components/PrescriptionDetails/VaPrescription.jsx +++ b/src/applications/mhv-medications/components/PrescriptionDetails/VaPrescription.jsx @@ -77,8 +77,15 @@ const VaPrescription = prescription => { }vads-u-display--block vads-c-action-link--green vads-u-margin-bottom--3`} to="/refill" data-testid="refill-nav-link" + data-dd-action-name={ + dataDogActionNames.detailsPage.FILL_THIS_PRESCRIPTION + } > - {hasBeenDispensed ? 'Refill' : 'Fill'} this prescription + {/* TODO: clean after grouping flag is gone */} + {!showGroupingContent && + `${hasBeenDispensed ? 'Refill' : 'Fill'} this prescription`} + {showGroupingContent && + `Request a ${hasBeenDispensed ? 'refill' : 'fill'}`} ) : ( @@ -102,7 +109,7 @@ const VaPrescription = prescription => {

    Prescription number

    -

    +

    {prescription.prescriptionNumber}

    @@ -129,7 +136,7 @@ const VaPrescription = prescription => {

    Prescription number

    -

    +

    {prescription.prescriptionNumber}

    @@ -437,11 +444,12 @@ const VaPrescription = prescription => { refillHistory[0].dispensedDate !== undefined) && ( <>

    - Showing {refillHistory.length} refills, from newest to - oldest + {`Showing ${refillHistory.length} refill${ + refillHistory.length > 1 ? 's, from newest to oldest' : '' + }`}

    { const refillPosition = refillHistory.length - i - 1; const refillLabelId = `rx-refill-${refillPosition}`; return ( - -

    +

    { > {i + 1 === refillHistory.length ? 'Original fill' - : `Refill ${refillPosition}`} -

    -

    - Filled by pharmacy on + : `Refill`}

    -

    - {dateFormat(entry.dispensedDate)} -

    -

    - Shipped on -

    -

    - {dateFormat(latestTrackingStatus?.completeDateTime)} -

    + {i === 0 && ( + <> +

    + Shipped on +

    +

    + {dateFormat( + latestTrackingStatus?.completeDateTime, + )} +

    + + )}

    - ); }; diff --git a/src/applications/mhv-medications/containers/PrescriptionDetails.jsx b/src/applications/mhv-medications/containers/PrescriptionDetails.jsx index a419844189ee..af5e0a790cb5 100644 --- a/src/applications/mhv-medications/containers/PrescriptionDetails.jsx +++ b/src/applications/mhv-medications/containers/PrescriptionDetails.jsx @@ -326,10 +326,7 @@ const PrescriptionDetails = () => { const content = () => { if (prescription || prescriptionsApiError) { return ( - // TODO: clean after grouping flag is gone -
    +

    { introContent() )} {/* NOTE: The HTML content comes from a reliable source (MHV API/Krames API) */} -
    +
    diff --git a/src/applications/mhv-medications/containers/Prescriptions.jsx b/src/applications/mhv-medications/containers/Prescriptions.jsx index e5bbb73d500d..5b290a47909e 100644 --- a/src/applications/mhv-medications/containers/Prescriptions.jsx +++ b/src/applications/mhv-medications/containers/Prescriptions.jsx @@ -62,7 +62,7 @@ import PrescriptionsPrintOnly from './PrescriptionsPrintOnly'; import { getPrescriptionSortedList } from '../api/rxApi'; import ApiErrorNotification from '../components/shared/ApiErrorNotification'; import CernerFacilityAlert from '../components/shared/CernerFacilityAlert'; -import { pageType } from '../util/dataDogConstants'; +import { dataDogActionNames, pageType } from '../util/dataDogConstants'; import MedicationsListFilter from '../components/MedicationsList/MedicationsListFilter'; const Prescriptions = () => { @@ -645,7 +645,7 @@ const Prescriptions = () => { {prescriptionsApiError ? ( <> - + ) : ( <> @@ -680,6 +680,10 @@ const Prescriptions = () => { className="vads-c-action-link--green vads-u-margin--0" to={medicationsUrls.subdirectories.REFILL} data-testid="prescriptions-nav-link-to-refill" + data-dd-action-name={ + dataDogActionNames.medicationsListPage + .START_REFILL_REQUEST + } > Start a refill request diff --git a/src/applications/mhv-medications/containers/RefillPrescriptions.jsx b/src/applications/mhv-medications/containers/RefillPrescriptions.jsx index 4861dd66a9b2..e2223cb6aa63 100644 --- a/src/applications/mhv-medications/containers/RefillPrescriptions.jsx +++ b/src/applications/mhv-medications/containers/RefillPrescriptions.jsx @@ -189,7 +189,7 @@ const RefillPrescriptions = ({ isLoadingList = true }) => { {prescriptionsApiError ? ( <> - + ) : ( <> @@ -244,6 +244,7 @@ const RefillPrescriptions = ({ isLoadingList = true }) => { dataDogActionNames.refillPage .SELECT_SINGLE_MEDICATION_CHECKBOX } + data-dd-privacy="mask" checked={ selectedRefillList.find( item => diff --git a/src/applications/mhv-medications/routes.jsx b/src/applications/mhv-medications/routes.jsx index 0215656ac97a..b6521e5814fb 100644 --- a/src/applications/mhv-medications/routes.jsx +++ b/src/applications/mhv-medications/routes.jsx @@ -43,17 +43,11 @@ const routes = (
    -
    - -
    -
    - -
    { Data.REFILL_HISTORY_INFO, ); detailsPage.clickRefillHistoryAccordionOnDetailsPage(); - detailsPage.verifyFilledDateFieldInAccordionCardInfoOnDetailPage( - Data.FILL_DATE_FIELD, - ); + detailsPage.verifyImageFieldInAccordionCardInfoOnDetailsPage( Data.IMAGE_FIELD, ); diff --git a/src/applications/mhv-medications/tests/e2e/medications-original-fill-request-link-text-details-page.cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-original-fill-request-link-text-details-page.cypress.spec.js new file mode 100644 index 000000000000..61c4b6c4f462 --- /dev/null +++ b/src/applications/mhv-medications/tests/e2e/medications-original-fill-request-link-text-details-page.cypress.spec.js @@ -0,0 +1,21 @@ +import MedicationsSite from './med_site/MedicationsSite'; +import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import medicationsList from './fixtures/grouped-prescriptions-list.json'; +import MedicationsDetailsPage from './pages/MedicationsDetailsPage'; +import olderRxDetails from './fixtures/older-prescription-details.json'; +import { Data } from './utils/constants'; + +describe('Medications Details Page Request Refill Link', () => { + it('visits Medications Details Page Original Fill Link Text', () => { + const site = new MedicationsSite(); + const landingPage = new MedicationsLandingPage(); + const detailsPage = new MedicationsDetailsPage(); + site.login(); + landingPage.visitLandingPageURL(); + landingPage.visitMedicationsListPage(medicationsList); + cy.injectAxe(); + cy.axeCheck('main'); + detailsPage.clickMedicationDetailsLink(olderRxDetails, 2); + detailsPage.verifyRefillLinkTextOnDetailsPage(Data.ORIGINAL_FILL_LINK_TEXT); + }); +}); diff --git a/src/applications/mhv-medications/tests/e2e/medications-refill-history-details-page..cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-refill-history-details-page..cypress.spec.js index 6c227db95fe0..52ae5f270a11 100644 --- a/src/applications/mhv-medications/tests/e2e/medications-refill-history-details-page..cypress.spec.js +++ b/src/applications/mhv-medications/tests/e2e/medications-refill-history-details-page..cypress.spec.js @@ -19,14 +19,8 @@ describe('Medications Refill History on Details Page', () => { detailsPage.clickMedicationDetailsLink(refillHistoryDetails, cardNumber); detailsPage.verifyRefillHistoryHeaderOnDetailsPage(); detailsPage.verifyFirstRefillHeaderTextOnDetailsPage(); - detailsPage.verifyFillDateFieldOnDetailsPage(); + detailsPage.verifyShippedOnDateFieldOnDetailsPage(); detailsPage.verifyNoImageFieldMessageOnDetailsPage(); - detailsPage.verifyRxFilledByPharmacyDateOnDetailsPage( - refillHistoryDetails.data.attributes.refillDate, - ); - detailsPage.verifyRxShippedOnDateOnDetailsPage( - refillHistoryDetails.data.attributes.dispensedDate, - ); }); }); diff --git a/src/applications/mhv-medications/tests/e2e/medications-refill-request-link-text-validation-details.cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-refill-request-link-text-validation-details.cypress.spec.js new file mode 100644 index 000000000000..24493bb0a8e9 --- /dev/null +++ b/src/applications/mhv-medications/tests/e2e/medications-refill-request-link-text-validation-details.cypress.spec.js @@ -0,0 +1,22 @@ +import MedicationsSite from './med_site/MedicationsSite'; +import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import medicationsList from './fixtures/grouped-prescriptions-list.json'; +import MedicationsDetailsPage from './pages/MedicationsDetailsPage'; +import olderRxDetails from './fixtures/older-prescription-details.json'; +import { Data } from './utils/constants'; + +describe('Medications Details Page Refill Prescription Link', () => { + it('visits Medications Details Page Grouping Refill Link Text', () => { + const site = new MedicationsSite(); + const landingPage = new MedicationsLandingPage(); + const detailsPage = new MedicationsDetailsPage(); + site.login(); + landingPage.visitLandingPageURL(); + landingPage.visitMedicationsListPage(medicationsList); + cy.injectAxe(); + cy.axeCheck('main'); + detailsPage.clickMedicationDetailsLink(olderRxDetails, 1); + detailsPage.verifyLastFilledDateOnDetailsPage(Data.LAST_FILLED_DATE); + detailsPage.verifyRefillLinkTextOnDetailsPage(Data.REFILL_LINK_TEXT); + }); +}); diff --git a/src/applications/mhv-medications/tests/e2e/medications-single-previous-prescription-description-details-page.cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-single-previous-prescription-description-details-page.cypress.spec.js new file mode 100644 index 000000000000..2f9bfa38ed50 --- /dev/null +++ b/src/applications/mhv-medications/tests/e2e/medications-single-previous-prescription-description-details-page.cypress.spec.js @@ -0,0 +1,23 @@ +import MedicationsSite from './med_site/MedicationsSite'; +import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import medicationsList from './fixtures/grouped-prescriptions-list.json'; +import MedicationsDetailsPage from './pages/MedicationsDetailsPage'; +import olderRxDetails from './fixtures/older-prescription-details.json'; +import { Data } from './utils/constants'; + +describe('Medications Details Page Previous Prescription', () => { + it('visits Medications Details Page single previous refill description', () => { + const site = new MedicationsSite(); + const landingPage = new MedicationsLandingPage(); + const detailsPage = new MedicationsDetailsPage(); + site.login(); + landingPage.visitLandingPageURL(); + landingPage.visitMedicationsListPage(medicationsList); + cy.injectAxe(); + cy.axeCheck('main'); + detailsPage.clickMedicationDetailsLink(olderRxDetails, 5); + detailsPage.verifyPreviousPrescriptionsPaginationTextOnDetailsPage( + Data.SINGLE_PREVIOUS_RX_INFO, + ); + }); +}); diff --git a/src/applications/mhv-medications/tests/e2e/medications-single-refill-history-description-details-page.cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-single-refill-history-description-details-page.cypress.spec.js new file mode 100644 index 000000000000..e350181dd3e0 --- /dev/null +++ b/src/applications/mhv-medications/tests/e2e/medications-single-refill-history-description-details-page.cypress.spec.js @@ -0,0 +1,23 @@ +import MedicationsSite from './med_site/MedicationsSite'; +import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import medicationsList from './fixtures/grouped-prescriptions-list.json'; +import MedicationsDetailsPage from './pages/MedicationsDetailsPage'; +import olderRxDetails from './fixtures/older-prescription-details.json'; +import { Data } from './utils/constants'; + +describe('Medications Details Page refill history', () => { + it('visits Medications Details Page single refill description', () => { + const site = new MedicationsSite(); + const landingPage = new MedicationsLandingPage(); + const detailsPage = new MedicationsDetailsPage(); + site.login(); + landingPage.visitLandingPageURL(); + landingPage.visitMedicationsListPage(medicationsList); + cy.injectAxe(); + cy.axeCheck('main'); + detailsPage.clickMedicationDetailsLink(olderRxDetails, 7); + detailsPage.verifyRefillHistoryInformationTextOnDetailsPage( + Data.SINGLE_REFILL_HISTORY_INFO, + ); + }); +}); diff --git a/src/applications/mhv-medications/tests/e2e/medications-validate-back-to-top-button-on-list-page.cypress.spec.js b/src/applications/mhv-medications/tests/e2e/medications-validate-back-to-top-button-on-list-page.cypress.spec.js new file mode 100644 index 000000000000..c11704f8139d --- /dev/null +++ b/src/applications/mhv-medications/tests/e2e/medications-validate-back-to-top-button-on-list-page.cypress.spec.js @@ -0,0 +1,19 @@ +import MedicationsSite from './med_site/MedicationsSite'; +import MedicationsLandingPage from './pages/MedicationsLandingPage'; +import MedicationsListPage from './pages/MedicationsListPage'; + +describe('Medications List Page BackToTop', () => { + it('visits Medications List Page BackToTop Button', () => { + const site = new MedicationsSite(); + const listPage = new MedicationsListPage(); + const landingPage = new MedicationsLandingPage(); + site.login(); + landingPage.visitLandingPageURL(); + cy.injectAxe(); + cy.axeCheck('main'); + listPage.clickGotoMedicationsLink(); + cy.scrollTo(0, 1500); + listPage.clickBackToTopButtonOnListPage(); + listPage.verifyMedicationsListPageTitleIsFocused(); + }); +}); diff --git a/src/applications/mhv-medications/tests/e2e/pages/MedicationsDetailsPage.js b/src/applications/mhv-medications/tests/e2e/pages/MedicationsDetailsPage.js index 80faa57baa05..a22f919c6069 100644 --- a/src/applications/mhv-medications/tests/e2e/pages/MedicationsDetailsPage.js +++ b/src/applications/mhv-medications/tests/e2e/pages/MedicationsDetailsPage.js @@ -324,7 +324,7 @@ class MedicationsDetailsPage { verifyFirstRefillHeaderTextOnDetailsPage = () => { cy.get('[data-testid="rx-refill"]') .first() - .should('contain', 'Refill 1'); + .should('contain', 'Refill'); }; verifyFillDateFieldOnDetailsPage = () => { @@ -554,6 +554,18 @@ class MedicationsDetailsPage { verifyNoMedicationsErrorAlertWhenUserNavsToDetailsPage = text => { cy.get('[data-testid="no-medications-list"]').should('have.text', text); }; + + verifyLastFilledDateOnDetailsPage = text => { + cy.get('[data-testid="rx-last-filled-date"]').should('have.text', text); + }; + + verifyRefillLinkTextOnDetailsPage = text => { + cy.get('[data-testid="refill-nav-link"]').should('have.text', text); + }; + + verifyRefillHistoryDescriptionText = text => { + cy.get('[data-testid="refill-history-info"]').should('have.text', text); + }; } export default MedicationsDetailsPage; diff --git a/src/applications/mhv-medications/tests/e2e/pages/MedicationsListPage.js b/src/applications/mhv-medications/tests/e2e/pages/MedicationsListPage.js index f0bbdf284b13..5e5a828ea7fd 100644 --- a/src/applications/mhv-medications/tests/e2e/pages/MedicationsListPage.js +++ b/src/applications/mhv-medications/tests/e2e/pages/MedicationsListPage.js @@ -784,14 +784,8 @@ class MedicationsListPage { }; verifyFilterCollapsedOnListPage = () => { - cy.get('[data-testid="filter-button"]') - .shadow() - .find('[type="button"]') - .should('not.exist'); - cy.get('[data-testid="filter-option"]') - .shadow() - .find('[class="usa-legend"]', { force: true }) - .should('not.exist'); + cy.get('[data-testid="filter-button"]').should('not.be.visible'); + cy.get('[data-testid="filter-option"]').should('not.be.visible'); }; visitMedicationsListPageURL = medication => { @@ -800,8 +794,8 @@ class MedicationsListPage { '/my_health/v1/medical_records/allergies', allergies, ).as('allergies'); - cy.visit(medicationsUrls.MEDICATIONS_URL); cy.intercept('GET', `${Paths.MED_LIST}`, medication).as('noMedications'); + cy.visit(medicationsUrls.MEDICATIONS_URL); }; verifyEmptyMedicationsListAlertOnListPage = text => { @@ -820,6 +814,21 @@ class MedicationsListPage { waitForAnimations: true, }); }; + + clickBackToTopButtonOnListPage = () => { + cy.get('[data-testid="rx-back-to-top"]') + .should('exist') + .and('be.visible'); + cy.get('[data-testid="rx-back-to-top"]', { includeShadowDom: true }) + .find('[class ="text"]') + .click({ force: true }); + }; + + verifyMedicationsListPageTitleIsFocused = () => { + cy.get('[data-testid="list-page-title"]') + .should('be.visible') + .and('be.focused'); + }; } export default MedicationsListPage; diff --git a/src/applications/mhv-medications/tests/e2e/utils/constants.js b/src/applications/mhv-medications/tests/e2e/utils/constants.js index 31477bb9b5e4..5c832e37762c 100644 --- a/src/applications/mhv-medications/tests/e2e/utils/constants.js +++ b/src/applications/mhv-medications/tests/e2e/utils/constants.js @@ -22,6 +22,11 @@ export const Data = { FILL_DATE_FIELD: 'Filled by pharmacy on', IMAGE_FIELD: 'Image', MED_DESCRIPTION: 'Medication description', + LAST_FILLED_DATE: 'Last filled on March 18, 2024', + REFILL_LINK_TEXT: 'Request a refill', + ORIGINAL_FILL_LINK_TEXT: 'Request a fill', + SINGLE_REFILL_HISTORY_INFO: 'Showing 1 refill', + SINGLE_PREVIOUS_RX_INFO: 'Showing 1 prescription', }; export const Paths = { LANDING_LIST: diff --git a/src/applications/mhv-medications/util/dataDogConstants.js b/src/applications/mhv-medications/util/dataDogConstants.js index 540a7bc43e7f..ec79c3cbf0ad 100644 --- a/src/applications/mhv-medications/util/dataDogConstants.js +++ b/src/applications/mhv-medications/util/dataDogConstants.js @@ -34,6 +34,9 @@ export const dataDogActionNames = { [NON_ACTIVE_FILTER_KEY]: 'Non-active filter', APPLY_FILTER_BUTTON: 'Apply filter button', RESET_FILTER_BUTTON: 'Reset filter button', + START_REFILL_REQUEST: `Start a refill request - ${pageType.LIST}`, + FILTER_LIST_ACCORDION: 'Filter accordion', + PAGINATION: 'Pagination', }, landingPage: { COMPOSE_A_MESSAGE_LINK: `Compose A Message Link - ${pageType.ABOUT}`, @@ -66,6 +69,7 @@ export const dataDogActionNames = { pageType.DETAILS }`, RX_DOCUMENTATION_LINK: `Rx Documentation Link - ${pageType.DETAILS}`, + FILL_THIS_PRESCRIPTION: `Fill this prescription - ${pageType.DETAILS}`, }, refillPage: { GO_TO_YOUR_MEDICATIONS_LIST_ACTION_LINK: `Go To Your Medications List Action Link - ${ diff --git a/src/applications/pensions/config/chapters/06-additional-information/uploadDocuments.js b/src/applications/pensions/config/chapters/06-additional-information/uploadDocuments.js index 3c10caae15b8..c98d2e783962 100644 --- a/src/applications/pensions/config/chapters/06-additional-information/uploadDocuments.js +++ b/src/applications/pensions/config/chapters/06-additional-information/uploadDocuments.js @@ -46,8 +46,8 @@ export default { fileUploadUrl: `${environment.API_URL}/v0/claim_attachments`, maxSize: MAX_FILE_SIZE_BYTES, fileTypes: ['pdf', 'jpg', 'jpeg', 'png'], - fileTooBigErrorMessage: - 'Your file can’t have a width and height larger than 78 inches and 100 inches. Follow the instructions for your device on how to resize the file and try again.', + fileUploadNetworkErrorMessage: + 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', fileUploadNetworkErrorAlert: { header: 'We couldn’t upload your file', body: [ diff --git a/src/applications/personalization/review-information/components/WelcomeVAContactAdditionalInfo.js b/src/applications/personalization/review-information/components/WelcomeVAContactAdditionalInfo.js index bcd595b576de..7aeb02f4d318 100644 --- a/src/applications/personalization/review-information/components/WelcomeVAContactAdditionalInfo.js +++ b/src/applications/personalization/review-information/components/WelcomeVAContactAdditionalInfo.js @@ -1,7 +1,7 @@ import React from 'react'; const WelcomeVAContactAdditionalInfo = ( - <> +
    - +

    ); export default WelcomeVAContactAdditionalInfo; diff --git a/src/applications/personalization/review-information/config/form.js b/src/applications/personalization/review-information/config/form.js index d39f2fae43df..1826351acd95 100644 --- a/src/applications/personalization/review-information/config/form.js +++ b/src/applications/personalization/review-information/config/form.js @@ -70,6 +70,7 @@ const formConfig = { collapsibleNavLinks: true, }, formId: VA_FORM_IDS.FORM_WELCOME_VA_SETUP_REVIEW_INFORMATION, + disableSave: true, saveInProgress: { // messages: { // inProgress: 'Your welcome va setup review information form application (00-0000) is in progress.', diff --git a/src/applications/personalization/review-information/containers/ConfirmationPage.jsx b/src/applications/personalization/review-information/containers/ConfirmationPage.jsx index 2e6f24cf5654..787f6c5f8c01 100644 --- a/src/applications/personalization/review-information/containers/ConfirmationPage.jsx +++ b/src/applications/personalization/review-information/containers/ConfirmationPage.jsx @@ -25,7 +25,7 @@ export const ConfirmationPage = () => { profile.

    @@ -33,14 +33,14 @@ export const ConfirmationPage = () => {
    diff --git a/src/applications/personalization/review-information/tests/unit/components/WelcomeVAContactAdditionalInfo.unit.spec.js b/src/applications/personalization/review-information/tests/unit/components/WelcomeVAContactAdditionalInfo.unit.spec.js new file mode 100644 index 000000000000..9d9618f2b6f8 --- /dev/null +++ b/src/applications/personalization/review-information/tests/unit/components/WelcomeVAContactAdditionalInfo.unit.spec.js @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import WelcomeVAContactAdditionalInfo from '../../../components/WelcomeVAContactAdditionalInfo'; + +describe('', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(WelcomeVAContactAdditionalInfo); + }); + + it('renders as expected', () => { + expect(wrapper.exists()).to.be.true; + }); + + it('renders a component', () => { + const additionalInfo = wrapper.find('va-additional-info'); + expect(additionalInfo.exists()).to.be.true; + expect(additionalInfo.prop('trigger')).to.equal( + 'Which benefits and services does VA use this contact information for?', + ); + }); + + it('renders the correct paragraph texts', () => { + const paragraphs = wrapper.find('p'); + expect(paragraphs).to.have.lengthOf(3); + expect(paragraphs.at(0).text()).to.include( + 'We use this information to contact you about these VA benefits and services:', + ); + expect(paragraphs.at(1).text()).to.include( + 'If you’re enrolled in VA health care, we also use this information to send you these:', + ); + expect(paragraphs.at(2).text()).to.include( + 'Find out how to change your contact information for other VA benefits', + ); + }); + + it('renders the correct unordered lists and list items', () => { + const lists = wrapper.find('ul'); + expect(lists).to.have.lengthOf(2); + + const firstListItems = lists.at(0).find('li'); + expect(firstListItems).to.have.lengthOf(4); + expect(firstListItems.at(0).text()).to.equal('Disability compensation'); + expect(firstListItems.at(1).text()).to.equal('Pension benefits'); + expect(firstListItems.at(2).text()).to.equal('Claims and appeals'); + expect(firstListItems.at(3).text()).to.equal( + 'Veteran Readiness and Employment (VR&E)', + ); + + const secondListItems = lists.at(1).find('li'); + expect(secondListItems).to.have.lengthOf(4); + expect(secondListItems.at(0).text()).to.equal('Appointment reminders'); + expect(secondListItems.at(1).text()).to.equal( + 'Communications from your VA medical center', + ); + expect(secondListItems.at(2).text()).to.equal('Lab and test results'); + expect(secondListItems.at(3).text()).to.include( + 'Prescription medicines (we send your medicines to your mailing address)', + ); + }); + + it('renders a link with the correct href and text', () => { + const link = wrapper.find('a'); + expect(link.exists()).to.be.true; + expect(link.prop('href')).to.equal( + '/resources/change-your-address-on-file-with-va/#change-your-address-by-contact', + ); + expect(link.text()).to.include( + 'Find out how to change your contact information for other VA benefits', + ); + }); +}); diff --git a/src/applications/personalization/review-information/tests/unit/config/form.unit.spec.js b/src/applications/personalization/review-information/tests/unit/config/form.unit.spec.js new file mode 100644 index 000000000000..54e5695a6a77 --- /dev/null +++ b/src/applications/personalization/review-information/tests/unit/config/form.unit.spec.js @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import formConfig from '../../../config/form'; + +describe('formConfig', () => { + it('is an object', () => { + expect(formConfig).to.be.an('object'); + }); + + it('has a rootUrl property', () => { + expect(formConfig).to.have.property('rootUrl'); + expect(formConfig.rootUrl).to.be.a('string'); + }); + + it('has a formId property', () => { + expect(formConfig).to.have.property('formId'); + expect(formConfig.formId).to.be.a('string'); + }); + + it('has a chapters property', () => { + expect(formConfig).to.have.property('chapters'); + expect(formConfig.chapters).to.be.an('object'); + }); + + describe('chapters', () => { + it('contains the infoPage chapter', () => { + expect(formConfig.chapters).to.have.property('infoPage'); + expect(formConfig.chapters.infoPage).to.be.an('object'); + }); + + it('contains pages in the infoPage chapter', () => { + const { infoPage } = formConfig.chapters; + expect(infoPage).to.have.property('pages'); + expect(infoPage.pages).to.be.an('object'); + }); + + it('includes a contact information page', () => { + const contactInfoPage = + formConfig.chapters.infoPage.pages.confirmContactInfo; + expect(contactInfoPage).to.exist; + expect(contactInfoPage).to.be.an('object'); + expect(contactInfoPage).to.have.property('CustomPage'); + }); + }); + + describe('contact information page', () => { + const contactInfoPage = + formConfig.chapters.infoPage.pages.confirmContactInfo; + + it('has an onNavForward method', () => { + expect(contactInfoPage).to.have.property('onNavForward'); + expect(contactInfoPage.onNavForward).to.be.a('function'); + }); + + it('onNavForward navigates to the confirmation path', () => { + const mockGoPath = path => expect(path).to.equal('confirmation'); + contactInfoPage.onNavForward({ goPath: mockGoPath }); + }); + + it('has an onNavBack method', () => { + expect(contactInfoPage).to.have.property('onNavBack'); + expect(contactInfoPage.onNavBack).to.be.a('function'); + }); + + it('onNavBack redirects to the My VA page', () => { + const originalLocation = window.location; + + contactInfoPage.onNavBack(); + expect(window.location).to.equal('https://dev.va.gov/my-va/'); + + global.window.location = originalLocation; + }); + }); + + describe('submit function', () => { + it('exists and returns a promise with a confirmation number', async () => { + const result = await formConfig.submit(); + expect(result).to.be.an('object'); + expect(result.attributes).to.have.property('confirmationNumber'); + expect(result.attributes.confirmationNumber).to.be.a('string'); + }); + }); + + describe('footerContent', () => { + it('exists as a property', () => { + expect(formConfig).to.have.property('footerContent'); + expect(formConfig.footerContent).to.be.a('function'); + }); + }); + + describe('savedFormMessages', () => { + it('includes a notFound message', () => { + expect(formConfig.savedFormMessages).to.have.property('notFound'); + expect(formConfig.savedFormMessages.notFound).to.be.a('string'); + }); + + it('includes a noAuth message', () => { + expect(formConfig.savedFormMessages).to.have.property('noAuth'); + expect(formConfig.savedFormMessages.noAuth).to.be.a('string'); + }); + }); + + describe('formOptions', () => { + it('includes a noTopNav property', () => { + expect(formConfig.formOptions).to.have.property('noTopNav'); + expect(formConfig.formOptions.noTopNav).to.be.true; + }); + }); +}); diff --git a/src/applications/personalization/review-information/tests/unit/containers/ConfirmationPage.unit.spec.jsx b/src/applications/personalization/review-information/tests/unit/containers/ConfirmationPage.unit.spec.jsx new file mode 100644 index 000000000000..345f8e00fd58 --- /dev/null +++ b/src/applications/personalization/review-information/tests/unit/containers/ConfirmationPage.unit.spec.jsx @@ -0,0 +1,67 @@ +/* eslint-disable @department-of-veterans-affairs/enzyme-unmount */ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { cleanup } from '@testing-library/react'; +import ConfirmationPage from '../../../containers/ConfirmationPage'; + +describe('ConfirmationPage', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + cleanup(); + }); + + it('renders as expected', () => { + expect(wrapper.exists()).to.be.true; + }); + + it('contains the correct alert content', () => { + const alert = wrapper.find('va-alert'); + expect(alert.exists()).to.be.true; + expect(alert.prop('status')).to.equal('success'); + expect(alert.find('h2').text()).to.equal( + 'Contact information added to your profile', + ); + }); + + it('contains the correct informational messages', () => { + const alert = wrapper.find('va-alert'); + const paragraphs = alert.find('p'); + expect(paragraphs).to.have.lengthOf(2); + expect(paragraphs.at(0).text()).to.include('If you apply for VA benefits'); + expect(paragraphs.at(1).text()).to.include( + 'You can change your email and text notification settings', + ); + }); + + it('renders "Go to your notification settings" link correctly', () => { + const notificationLink = wrapper.find('va-link-action').at(0); + expect(notificationLink.exists()).to.be.true; + expect(notificationLink.prop('href')).to.equal('/profile/notifications/'); + expect(notificationLink.prop('text')).to.equal( + 'Go to your notification settings in your VA.gov profile', + ); + }); + + it('renders "Go back to My VA" link correctly', () => { + const myVaLink = wrapper.find('va-link-action').at(1); + expect(myVaLink.exists()).to.be.true; + expect(myVaLink.prop('href')).to.equal('/my-va/'); + expect(myVaLink.prop('text')).to.equal('Go back to My VA'); + }); + + it('renders "Go back to your profile" link correctly', () => { + const profileLink = wrapper.find('va-link-action').at(2); + expect(profileLink.exists()).to.be.true; + expect(profileLink.prop('href')).to.equal('/profile/'); + expect(profileLink.prop('text')).to.equal('Go back to your profile'); + }); +}); diff --git a/src/applications/pre-need-integration/components/PreparerHelpers.jsx b/src/applications/pre-need-integration/components/PreparerHelpers.jsx index 7f0a82234d58..eefcdf811c1d 100644 --- a/src/applications/pre-need-integration/components/PreparerHelpers.jsx +++ b/src/applications/pre-need-integration/components/PreparerHelpers.jsx @@ -12,10 +12,10 @@ export function ContactDetailsTitle() { ); } -export function ValidateAddressTitle() { +export function ValidateAddressTitle({ title }) { return (
    -

    Validate Address

    +

    {title}

    ); } diff --git a/src/applications/pre-need-integration/components/SuggestedAddressRadio.jsx b/src/applications/pre-need-integration/components/SuggestedAddressRadio.jsx new file mode 100644 index 000000000000..46f316b0e5ec --- /dev/null +++ b/src/applications/pre-need-integration/components/SuggestedAddressRadio.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { VaRadio } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { FIELD_NAMES } from '@@vap-svc/constants'; +import { formatSuggestedAddress } from '../utils/helpers'; +import { ValidateAddressTitle } from './PreparerHelpers'; + +export default function SuggestedAddressRadio({ + title, + userAddress, + selectedAddress, + addressValidation, + onChangeSelectedAddress, +}) { + return ( +
    + +

    We found a similar address to the one you entered.

    + + {userAddress && ( + + )} + {addressValidation?.confirmedSuggestions?.[0] && ( + + )} + +
    + ); +} diff --git a/src/applications/pre-need-integration/config/form.jsx b/src/applications/pre-need-integration/config/form.jsx index 1d6e3cd7216b..bd3bc8adc200 100644 --- a/src/applications/pre-need-integration/config/form.jsx +++ b/src/applications/pre-need-integration/config/form.jsx @@ -106,7 +106,9 @@ import { ContactDetailsTitle, PreparerDetailsTitle, } from '../components/PreparerHelpers'; -import preparerContactDetailsCustom from './pages/preparerContactDetailsCustom'; +import ApplicantSuggestedAddress from './pages/applicantSuggestedAddress'; +import SponsorSuggestedAddress from './pages/sponsorSuggestedAddress'; +import preparerSuggestedAddress from './pages/preparerSuggestedAddress'; const { preneedAttachments, @@ -205,16 +207,16 @@ const formConfig = { uiSchema: preparerContactDetails.uiSchema, schema: preparerContactDetails.schema, }, - validatePreparerContactDetails: { + preparerSuggestedAddress: { title: 'Validate Address', - path: 'validate-preparer-contact-details', + path: 'preparer-suggested-address', depends: formData => isAuthorizedAgent(formData), uiSchema: { application: { applicant: { - 'view:validateAddress': { + 'view:preparerSuggestedAddress': { 'ui:title': 'Validate Address', - 'ui:field': preparerContactDetailsCustom, + 'ui:field': preparerSuggestedAddress, }, }, }, @@ -228,7 +230,7 @@ const formConfig = { applicant: { type: 'object', properties: { - 'view:validateAddress': { + 'view:preparerSuggestedAddress': { type: 'object', properties: {}, }, @@ -335,6 +337,40 @@ const formConfig = { ), schema: applicantContactInformation.schema, }, + applicantSuggestedAddress: { + title: 'Validate Address', + path: 'applicant-suggested-address', + depends: formData => !isAuthorizedAgent(formData), + uiSchema: { + application: { + applicant: { + 'view:applicantSuggestedAddress': { + 'ui:title': 'Validate Address', + 'ui:field': ApplicantSuggestedAddress, + }, + }, + }, + }, + schema: { + type: 'object', + properties: { + application: { + type: 'object', + properties: { + applicant: { + type: 'object', + properties: { + 'view:applicantSuggestedAddress': { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, applicantContactInformationPreparer: { title: applicantContactInfoPreparerAddressTitle, path: 'applicant-contact-information-preparer', @@ -346,6 +382,40 @@ const formConfig = { ), schema: applicantContactInformation.schema, }, + applicantSuggestedAddressPreparer: { + title: 'Validate Address', + path: 'preparer-suggested-address-preparer', + depends: formData => isAuthorizedAgent(formData), + uiSchema: { + application: { + applicant: { + 'view:applicantSuggestedAddressPreparer': { + 'ui:title': 'Validate Address', + 'ui:field': ApplicantSuggestedAddress, + }, + }, + }, + }, + schema: { + type: 'object', + properties: { + application: { + type: 'object', + properties: { + applicant: { + type: 'object', + properties: { + 'view:applicantSuggestedAddressPreparer': { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, applicantDemographics: { title: 'Your demographics', path: 'applicant-demographics', @@ -430,6 +500,45 @@ const formConfig = { uiSchema: sponsorContactInformation.uiSchema, schema: sponsorContactInformation.schema, }, + sponsorSuggestedAddress: { + title: 'Validate Address', + path: 'sponsor-suggested-address', + depends: formData => + !isVeteran(formData) && + ((!isApplicantTheSponsor(formData) && + !isSponsorDeceased(formData)) || + isApplicantTheSponsor(formData)) && + formData?.application?.veteran?.address.street !== undefined, + uiSchema: { + application: { + applicant: { + 'view:sponsorSuggestedAddress': { + 'ui:title': 'Validate Address', + 'ui:field': SponsorSuggestedAddress, + }, + }, + }, + }, + schema: { + type: 'object', + properties: { + application: { + type: 'object', + properties: { + applicant: { + type: 'object', + properties: { + 'view:sponsorSuggestedAddress': { + type: 'object', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, sponsorDemographics: { title: 'Sponsor demographics', path: 'sponsor-demographics', diff --git a/src/applications/pre-need-integration/config/pages/addressConfirmation.jsx b/src/applications/pre-need-integration/config/pages/addressConfirmation.jsx new file mode 100644 index 000000000000..71220deb78a4 --- /dev/null +++ b/src/applications/pre-need-integration/config/pages/addressConfirmation.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +function AddressConfirmation(/* { formData } */) { + const formDataUserAddress = { + street: '123 Test Street', + city: 'City', + state: 'MC', + postalCode: '28226', + country: 'USA', + }; + + // Helper function to conditionally return a line with a break + const renderLine = content => { + return content ? ( + <> + {content} +
    + + ) : null; + }; + + // For city/state/postalCode line, we build it conditionally: + const cityStatePostal = [ + formDataUserAddress?.city, + formDataUserAddress?.city && + (formDataUserAddress?.state || formDataUserAddress?.postalCode) + ? ',' + : '', + formDataUserAddress?.state, + formDataUserAddress?.state && formDataUserAddress?.postalCode ? ' ' : '', + formDataUserAddress?.postalCode, + ] + .join('') + .trim(); + + return ( + <> + +

    Check the address you entered

    + +

    + We can’t confirm the address you entered with the U.S. Postal + Service. Check the address before continuing. +

    +
    +
    +

    + Check your mailing address +

    +

    You entered:

    +
    +

    + {renderLine(formDataUserAddress?.street)} + {renderLine(formDataUserAddress?.street2)} + {cityStatePostal && renderLine(cityStatePostal)} + {renderLine(formDataUserAddress?.country)} +

    +
    +

    + If the address is correct, you can continue. If you need to edit the + address, you can go back. +

    + +

    + The address you entered may not be in the U.S. Postal Service’s + system. Or, you may have entered an error or other incorrect + information. +

    +
    + + ); +} + +const mapStateToProps = state => { + return { + state, + formData: state?.form?.data, + addressValidation: state?.vapService?.addressValidation, + }; +}; + +export default connect(mapStateToProps)(AddressConfirmation); diff --git a/src/applications/pre-need-integration/config/pages/applicantSuggestedAddress.jsx b/src/applications/pre-need-integration/config/pages/applicantSuggestedAddress.jsx new file mode 100644 index 000000000000..278a0d62d7ea --- /dev/null +++ b/src/applications/pre-need-integration/config/pages/applicantSuggestedAddress.jsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import { setData } from 'platform/forms-system/src/js/actions'; +import { validateAddress } from 'platform/user/profile/vap-svc/actions'; +import set from 'platform/utilities/data/set'; +import AddressConfirmation from './addressConfirmation'; +import { isAuthorizedAgent } from '../../utils/helpers'; + +import SuggestedAddressRadio from '../../components/SuggestedAddressRadio'; + +function ApplicantSuggestedAddress({ formData, addressValidation }) { + const dispatch = useDispatch(); + const [isLoading, setIsLoading] = useState(true); + const [userAddress, setUserAddress] = useState(null); + const [selectedAddress, setSelectedAddress] = useState(null); + const [suggestedAddress, setSuggestedAddress] = useState(null); + + const extractUserAddress = () => { + return formData?.application?.claimant?.address || {}; + }; + // Prepare address for API Request + const prepareAddressForAPI = address => ({ + addressLine1: address.street, + addressLine2: address.street2, + addressPou: 'CORRESPONDENCE', + addressType: 'DOMESTIC', + city: address.city, + countryCodeIso3: address.country, + stateCode: address.state, + zipCode: address.postalCode, + }); + + const shouldShowSuggestedAddress = () => { + if (!suggestedAddress?.addressLine1 || !userAddress?.street) return false; + return !( + userAddress.street === suggestedAddress.addressLine1 && + userAddress.city === suggestedAddress.city && + userAddress.state === suggestedAddress.stateCode && + userAddress.postalCode === suggestedAddress.zipCode && + userAddress.country === suggestedAddress.countryCodeIso3 + ); + }; + + // Handle Address Validation + useEffect(() => { + async function fetchSuggestedAddresses() { + try { + const formDataUserAddress = extractUserAddress(); + setUserAddress(formDataUserAddress); + setSelectedAddress(formDataUserAddress); + + await dispatch( + validateAddress( + '/profile/addresses', + 'POST', + 'mailingAddress', + prepareAddressForAPI(formDataUserAddress), + 'mailing-address', + ), + ); + } catch (error) { + setIsLoading(true); // This is temporary, send it to address confirmation screen instead + } + } + fetchSuggestedAddresses(); + }, []); + + useEffect( + () => { + if (addressValidation?.addressFromUser?.addressLine1) setIsLoading(false); + }, + [addressValidation], + ); + + useEffect( + () => { + setSuggestedAddress(addressValidation.confirmedSuggestions?.[0]); + }, + [addressValidation], + ); + + // Handle Address Selection Change + const onChangeSelectedAddress = event => { + const selected = JSON.parse(event.detail.value); + setSelectedAddress(selected); + let newAddress; + if ('addressLine1' in selected) { + newAddress = { + street: selected.addressLine1, + street2: selected.addressLine2, + city: selected.city, + country: selected.countryCodeIso3, + state: selected.stateCode, + postalCode: selected.zipCode, + }; + } else { + newAddress = selected; + } + const updatedFormData = set( + 'application.claimant.address', + newAddress, + formData, + ); + dispatch(setData(updatedFormData)); + }; + + if (isLoading) { + return ( + + ); + } + + return shouldShowSuggestedAddress() ? ( + + ) : ( + + ); +} + +// Map state to props +const mapStateToProps = state => ({ + formData: state?.form?.data, + addressValidation: state?.vapService?.addressValidation, +}); + +export default connect(mapStateToProps)(ApplicantSuggestedAddress); diff --git a/src/applications/pre-need-integration/config/pages/preparerContactDetails.jsx b/src/applications/pre-need-integration/config/pages/preparerContactDetails.jsx index c2e8e384f8d5..f6d57d4f51e5 100644 --- a/src/applications/pre-need-integration/config/pages/preparerContactDetails.jsx +++ b/src/applications/pre-need-integration/config/pages/preparerContactDetails.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { merge } from 'lodash'; import fullSchemaPreNeed from 'vets-json-schema/dist/40-10007-INTEGRATION-schema.json'; +import get from 'platform/utilities/data/get'; import * as address from '../../definitions/address'; @@ -27,23 +28,74 @@ export const uiSchema = { applicant: { 'view:applicantInfo': { mailingAddress: merge({}, address.uiSchema('Your mailing address'), { - country: { 'ui:required': isAuthorizedAgent }, + country: { + 'ui:required': isAuthorizedAgent, + 'ui:errorMessages': { + required: 'Select Country', + }, + }, street: { 'ui:title': 'Street address', 'ui:required': isAuthorizedAgent, + 'ui:errorMessages': { + required: 'Enter a street address', + }, }, street2: { 'ui:title': 'Street address line 2', }, - city: { 'ui:required': isAuthorizedAgent }, + city: { + 'ui:required': isAuthorizedAgent, + 'ui:errorMessages': { + required: 'Enter a city', + }, + }, state: { 'ui:title': preparerMailingAddressStateTitleWrapper, 'ui:required': isAuthorizedAgent, 'ui:options': { hideIf: formData => !preparerAddressHasState(formData), }, + 'ui:errorMessages': { + enum: 'Select a state or territory', + }, + }, + postalCode: { + 'ui:required': isAuthorizedAgent, + 'ui:errorMessages': { + required: 'Enter a postal code', + }, + 'ui:options': { + replaceSchema: (formData, _schema, _uiSchema, index, path) => { + const addressPath = path.slice(0, -1); + const data = get(addressPath, formData) ?? {}; + const { country } = data; + const addressSchema = _schema; + const addressUiSchema = _uiSchema; + + // country-specific error messages + if (country === 'USA') { + addressUiSchema['ui:errorMessages'] = { + required: 'Please provide a valid postal code', + }; + } else if (['CAN', 'MEX'].includes(country) || !country) { + addressUiSchema['ui:errorMessages'] = { + required: 'Enter a postal code', + }; + } else { + // no pattern validation for other countries + addressUiSchema['ui:errorMessages'] = { + required: + 'Enter a postal code that meets your country’s requirements. If your country doesn’t require a postal code, enter N/A.', + }; + } + + return { + ...addressSchema, + }; + }, + }, }, - postalCode: { 'ui:required': isAuthorizedAgent }, }), }, 'view:contactInfo': { diff --git a/src/applications/pre-need-integration/config/pages/preparerContactDetailsCustom.jsx b/src/applications/pre-need-integration/config/pages/preparerContactDetailsCustom.jsx deleted file mode 100644 index 8a2a61a5faae..000000000000 --- a/src/applications/pre-need-integration/config/pages/preparerContactDetailsCustom.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { connect, useDispatch } from 'react-redux'; -import { setData } from 'platform/forms-system/src/js/actions'; -import { FIELD_NAMES } from '@@vap-svc/constants'; -import { validateAddress } from 'platform/user/profile/vap-svc/actions'; -import { formatAddress } from 'platform/forms/address/helpers'; -import set from 'platform/utilities/data/set'; -import { ValidateAddressTitle } from '../../components/PreparerHelpers'; - -function PreparerContanctDetailsCustom({ formData, addressValidation }) { - const dispatch = useDispatch(); - const [userAddress, setUserAddress] = useState(); - const [selectedAddress, setSelectedAddress] = useState(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const formDataUserAddress = - formData?.application?.applicant['view:applicantInfo']?.mailingAddress; - - const addressForAPIRequest = { - addressLine1: formDataUserAddress.street, - addressLine2: formDataUserAddress.street2, - addressPou: 'CORRESPONDENCE', - addressType: 'DOMESTIC', - city: formDataUserAddress.city, - countryCodeIso3: formDataUserAddress.country, - stateCode: formDataUserAddress.state, - zipCode: formDataUserAddress.postalCode, - }; - async function getSuggestedAddresses() { - try { - setUserAddress(formDataUserAddress); - setSelectedAddress(formDataUserAddress); - // Dispatch suggestedAddresses to vapService redux store - await dispatch( - validateAddress( - '/profile/addresses', - 'POST', - 'mailingAddress', - addressForAPIRequest, - 'mailing-address', - ), - ); - setIsLoading(false); - } catch (error) { - setIsLoading(true); - } - } - getSuggestedAddresses(); - }, []); - - const onChangeSelectedAddress = (address, id) => { - setSelectedAddress(address); - let newAddress; - if (id !== 'userEntered') { - newAddress = { - street: address.addressLine1, - street2: address.addressLine2, - city: address.city, - country: address.countryCodeIso3, - state: address.stateCode, - postalCode: address.zipCode, - }; - } else { - newAddress = address; - } - - const updatedFormData = set( - 'application.applicant[view:applicantInfo].mailingAddress', - newAddress, - { ...formData }, // make a copy of the original formData - ); - dispatch(setData(updatedFormData)); - }; - - function formatUserAddress(address) { - const { street } = address; - const { country } = address; - const cityStateZip = String( - `${address.city}, ${address.state} ${address.postalCode}`, - ); - return { street, cityStateZip, country }; - } - - function renderAddressOption(address, id = 'userEntered') { - if (address !== undefined) { - const { street, cityStateZip, country } = - id !== 'userEntered' - ? formatAddress(address) - : formatUserAddress(address); - - return ( -
    - { - onChangeSelectedAddress(address, id); - }} - checked={selectedAddress === address} - /> - - -
    - ); - } - return null; - } - - return isLoading ? ( - - ) : ( -
    - - You entered: - {renderAddressOption(userAddress)} - Suggested Addresses: - {addressValidation?.confirmedSuggestions?.length !== 0 && - addressValidation?.confirmedSuggestions?.map((address, index) => - renderAddressOption(address, String(index)), - )} -
    - ); -} - -const mapStateToProps = state => { - return { - formData: state?.form?.data, - addressValidation: state?.vapService?.addressValidation, - }; -}; - -export default connect(mapStateToProps)(PreparerContanctDetailsCustom); diff --git a/src/applications/pre-need-integration/config/pages/preparerSuggestedAddress.jsx b/src/applications/pre-need-integration/config/pages/preparerSuggestedAddress.jsx new file mode 100644 index 000000000000..23d1ed34a19a --- /dev/null +++ b/src/applications/pre-need-integration/config/pages/preparerSuggestedAddress.jsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import { setData } from 'platform/forms-system/src/js/actions'; +import { validateAddress } from 'platform/user/profile/vap-svc/actions'; +import set from 'platform/utilities/data/set'; +import AddressConfirmation from './addressConfirmation'; +import SuggestedAddressRadio from '../../components/SuggestedAddressRadio'; + +function PreparerSuggestedAddress({ formData, addressValidation }) { + const dispatch = useDispatch(); + const [userAddress, setUserAddress] = useState(null); + const [selectedAddress, setSelectedAddress] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [suggestedAddress, setSuggestedAddress] = useState(null); + + // Extract address details from formData + const extractUserAddress = () => { + return ( + formData?.application?.applicant['view:applicantInfo']?.mailingAddress || + {} + ); + }; + + // Prepare address for API Request + const prepareAddressForAPI = address => ({ + addressLine1: address.street, + addressLine2: address.street2, + addressPou: 'CORRESPONDENCE', + addressType: 'DOMESTIC', + city: address.city, + countryCodeIso3: address.country, + stateCode: address.state, + zipCode: address.postalCode, + }); + + const shouldShowSuggestedAddress = () => { + if (!suggestedAddress?.addressLine1 || !userAddress?.street) return false; + return !( + userAddress.street === suggestedAddress.addressLine1 && + userAddress.city === suggestedAddress.city && + userAddress.state === suggestedAddress.stateCode && + userAddress.postalCode === suggestedAddress.zipCode && + userAddress.country === suggestedAddress.countryCodeIso3 + ); + }; + + // Fetch suggested addresses when component mounts + useEffect(() => { + async function fetchSuggestedAddresses() { + try { + const formDataUserAddress = extractUserAddress(); + setUserAddress(formDataUserAddress); + setSelectedAddress(formDataUserAddress); + + await dispatch( + validateAddress( + '/profile/addresses', + 'POST', + 'mailingAddress', + prepareAddressForAPI(formDataUserAddress), + 'mailing-address', + ), + ); + } catch (error) { + setIsLoading(true); // This is temporary, send it to address confirmation screen instead + } + } + fetchSuggestedAddresses(); + }, []); + + // Update isLoading state when addressValidation changes + useEffect( + () => { + if (addressValidation?.addressFromUser?.addressLine1) setIsLoading(false); + }, + [addressValidation], + ); + + // Update suggested address when addressValidation changes + useEffect( + () => { + setSuggestedAddress(addressValidation?.confirmedSuggestions[0]); + }, + [addressValidation], + ); + + // Handle address selection changes + const onChangeSelectedAddress = event => { + const selected = JSON.parse(event.detail.value); + setSelectedAddress(selected); + let newAddress; + if ('addressLine1' in selected) { + newAddress = { + street: selected.addressLine1, + street2: selected.addressLine2, + city: selected.city, + country: selected.countryCodeIso3, + state: selected.stateCode, + postalCode: selected.zipCode, + }; + } else { + newAddress = selected; + } + const updatedFormData = set( + 'application.applicant[view:applicantInfo].mailingAddress', + newAddress, + formData, + ); + dispatch(setData(updatedFormData)); + }; + + // Render loading indicator or content + if (isLoading) { + return ( + + ); + } + + return shouldShowSuggestedAddress() ? ( + + ) : ( + + ); +} + +// Map state to props +const mapStateToProps = state => ({ + formData: state?.form?.data, + addressValidation: state?.vapService?.addressValidation, +}); + +export default connect(mapStateToProps)(PreparerSuggestedAddress); diff --git a/src/applications/pre-need-integration/config/pages/servicePeriodsPages.jsx b/src/applications/pre-need-integration/config/pages/servicePeriodsPages.jsx index 8607067b48b8..a4c60e2b21c9 100644 --- a/src/applications/pre-need-integration/config/pages/servicePeriodsPages.jsx +++ b/src/applications/pre-need-integration/config/pages/servicePeriodsPages.jsx @@ -19,6 +19,112 @@ import { import rankEnums from '../../utils/rankEnums'; import { rankLabels } from '../../utils/rankLabels'; +export function handleGetItemName(item) { + return item?.serviceBranch ? serviceLabels[item.serviceBranch] : null; +} + +export function handleAlertMaxItems() { + return 'You have added the maximum number of allowed service periods for this application. You may edit or delete a service period or choose to continue the application.'; +} + +export function handleCardDescription(item) { + const dateRangeFrom = item?.dateRange?.from; + const dateRangeTo = item?.dateRange?.to; + + let range = ''; + + if (dateRangeFrom) { + range += formatReviewDate(dateRangeFrom); + } + + if (dateRangeFrom && dateRangeTo) { + range += ' - '; + } + + if (dateRangeTo) { + range += formatReviewDate(dateRangeTo); + } + + return range; +} + +export function handleCancelAddTitle(props) { + const servicePeriodName = props.getItemName(props.itemData); + + if (servicePeriodName === null) { + return `Cancel adding this service period`; + } + return `Cancel adding ${servicePeriodName} service period`; +} + +export function handleCancelAddNo() { + return 'No, keep this'; +} + +export function handleDeleteTitle(props) { + const servicePeriodName = props.getItemName(props.itemData); + + return `Are you sure you want to remove this ${servicePeriodName} service period?`; +} + +export function handleDeleteDescription(props) { + const servicePeriodName = props.getItemName(props.itemData); + + return `This will remove ${servicePeriodName} and all the information from the service period records.`; +} + +export function handleDeleteNeedAtLeastOneDescription() { + return 'If you remove this service period, we’ll take you to a screen where you can add another service period. You’ll need to list at least one service period for us to process this form.'; +} + +export function handleDeleteYes() { + return 'Yes, remove this'; +} + +export function handleDeleteNo() { + return 'No, keep this'; +} + +export function handleCancelEditTitle(props) { + const servicePeriodName = props.getItemName(props.itemData); + + return `Cancel editing ${servicePeriodName} service period?`; +} + +export function handleCancelEditDescription() { + return 'If you cancel, you’ll lose any changes you made on this screen and you will be returned to the service periods review page.'; +} + +export function handleCancelEditYes() { + return 'Yes, cancel'; +} + +export function handleCancelEditNo() { + return 'No, keep this'; +} + +export function handleSummaryTitle(formData) { + return hasServiceRecord(formData) + ? 'Veteran’s service period(s)' + : 'Review service period records'; +} + +export function handleVeteranDepends(formData) { + return isVeteran(formData) && !isAuthorizedAgent(formData); +} + +export function handlePreparerVeteranDepends(formData) { + return isVeteran(formData) && isAuthorizedAgent(formData); +} + +export function handleNonVeteranDepends(formData) { + return !isVeteran(formData) && !isAuthorizedAgent(formData); +} + +export function handlePreparerNonVeteranDepends(formData) { + return !isVeteran(formData) && isAuthorizedAgent(formData); +} + /** @type {ArrayBuilderOptions} */ const options = { arrayPath: 'serviceRecords', @@ -28,67 +134,21 @@ const options = { isItemIncomplete: item => !item?.serviceBranch, // include all required fields here maxItems: 3, text: { - getItemName: item => { - return item?.serviceBranch ? serviceLabels[item.serviceBranch] : null; - }, - alertMaxItems: () => - 'You have added the maximum number of allowed service periods for this application. You may edit or delete a service period or choose to continue the application.', - cardDescription: item => { - const dateRangeFrom = item?.dateRange?.from; - const dateRangeTo = item?.dateRange?.to; - - let range = ''; - - if (dateRangeFrom) { - range += formatReviewDate(dateRangeFrom); - } - - if (dateRangeFrom && dateRangeTo) { - range += ' - '; - } - - if (dateRangeTo) { - range += formatReviewDate(dateRangeTo); - } - - return range; - }, - cancelAddTitle: props => { - const servicePeriodName = props.getItemName(props.itemData); - - if (servicePeriodName === null) { - return `Cancel adding this service period`; - } - return `Cancel adding ${servicePeriodName} service period`; - }, - cancelAddNo: () => 'No, keep this', - deleteTitle: props => { - const servicePeriodName = props.getItemName(props.itemData); - - return `Are you sure you want to remove this ${servicePeriodName} service period?`; - }, - deleteDescription: props => { - const servicePeriodName = props.getItemName(props.itemData); - - return `This will remove ${servicePeriodName} and all the information from the service period records.`; - }, - deleteNeedAtLeastOneDescription: () => - 'If you remove this service period, we’ll take you to a screen where you can add another service period. You’ll need to list at least one service period for us to process this form.', - deleteYes: () => 'Yes, remove this', - deleteNo: () => 'No, keep this', - cancelEditTitle: props => { - const servicePeriodName = props.getItemName(props.itemData); - - return `Cancel editing ${servicePeriodName} service period?`; - }, - cancelEditDescription: () => - 'If you cancel, you’ll lose any changes you made on this screen and you will be returned to the service periods review page.', - cancelEditYes: () => 'Yes, cancel', - cancelEditNo: () => 'No, keep this', - summaryTitle: formData => - hasServiceRecord(formData) - ? 'Veteran’s service period(s)' - : 'Review service period records', + getItemName: handleGetItemName, + alertMaxItems: handleAlertMaxItems, + cardDescription: handleCardDescription, + cancelAddTitle: handleCancelAddTitle, + cancelAddNo: handleCancelAddNo, + deleteTitle: handleDeleteTitle, + deleteDescription: handleDeleteDescription, + deleteNeedAtLeastOneDescription: handleDeleteNeedAtLeastOneDescription, + deleteYes: handleDeleteYes, + deleteNo: handleDeleteNo, + cancelEditTitle: handleCancelEditTitle, + cancelEditDescription: handleCancelEditDescription, + cancelEditYes: handleCancelEditYes, + cancelEditNo: handleCancelEditNo, + summaryTitle: handleSummaryTitle, }, }; @@ -136,7 +196,7 @@ function handleTitle(isVet, isPrep, vetTitle, sponsorTitle, prepTitle) { } /** @returns {PageSchema} */ -function servicePeriodInformationPage(isVet, isPrep) { +export function servicePeriodInformationPage(isVet, isPrep) { return { uiSchema: { ...arrayBuilderItemFirstPageTitleUI({ @@ -469,21 +529,21 @@ export const servicePeriodsPagesVeteran = arrayBuilderPages( path: 'service-periods-veteran', uiSchema: introPage.uiSchema, schema: introPage.schema, - depends: formData => isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleVeteranDepends(formData), }), servicePeriodsSummaryVeteran: pageBuilder.summaryPage({ title: 'Your service period(s)', path: 'service-periods-summary-veteran', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, - depends: formData => isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleVeteranDepends(formData), }), servicePeriodInformationPageVeteran: pageBuilder.itemPage({ title: 'Service period', path: 'service-periods-veteran/:index/service-period', uiSchema: servicePeriodInformationPageVeteran.uiSchema, schema: servicePeriodInformationPageVeteran.schema, - depends: formData => isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleVeteranDepends(formData), }), }), ); @@ -496,21 +556,21 @@ export const servicePeriodsPagesPreparerVeteran = arrayBuilderPages( path: 'service-periods-preparer-veteran', uiSchema: introPage.uiSchema, schema: introPage.schema, - depends: formData => isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerVeteranDepends(formData), }), servicePeriodsSummaryPreparerVeteran: pageBuilder.summaryPage({ title: 'Applicant’s service period(s)', path: 'service-periods-summary-preparer-veteran', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, - depends: formData => isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerVeteranDepends(formData), }), servicePeriodInformationPagePreparerVeteran: pageBuilder.itemPage({ title: 'Service period', path: 'service-periods-preparer-veteran/:index/service-period', uiSchema: servicePeriodInformationPagePreparerVeteran.uiSchema, schema: servicePeriodInformationPagePreparerVeteran.schema, - depends: formData => isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerVeteranDepends(formData), }), }), ); @@ -523,21 +583,21 @@ export const servicePeriodsPagesNonVeteran = arrayBuilderPages( path: 'service-periods-nonveteran', uiSchema: introPage.uiSchema, schema: introPage.schema, - depends: formData => !isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleNonVeteranDepends(formData), }), servicePeriodsSummaryNonVeteran: pageBuilder.summaryPage({ title: 'Sponsor’s service period(s)', path: 'service-periods-summary-nonveteran', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, - depends: formData => !isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleNonVeteranDepends(formData), }), servicePeriodInformationPageNonVeteran: pageBuilder.itemPage({ title: 'Service period', path: 'service-periods-nonveteran/:index/service-period', uiSchema: servicePeriodInformationPageNonVeteran.uiSchema, schema: servicePeriodInformationPageNonVeteran.schema, - depends: formData => !isVeteran(formData) && !isAuthorizedAgent(formData), + depends: formData => handleNonVeteranDepends(formData), }), }), ); @@ -550,21 +610,21 @@ export const servicePeriodsPagesPreparerNonVeteran = arrayBuilderPages( path: 'service-periods-preparer-nonveteran', uiSchema: introPage.uiSchema, schema: introPage.schema, - depends: formData => !isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerNonVeteranDepends(formData), }), servicePeriodsSummaryPreparerNonVeteran: pageBuilder.summaryPage({ title: 'Applicant’s service period(s)', path: 'service-periods-summary-preparer-nonveteran', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, - depends: formData => !isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerNonVeteranDepends(formData), }), servicePeriodInformationPagePreparerNonVeteran: pageBuilder.itemPage({ title: 'Service period', path: 'service-periods-preparer-nonveteran/:index/service-period', uiSchema: servicePeriodInformationPagePreparerNonVeteran.uiSchema, schema: servicePeriodInformationPagePreparerNonVeteran.schema, - depends: formData => !isVeteran(formData) && isAuthorizedAgent(formData), + depends: formData => handlePreparerNonVeteranDepends(formData), }), }), ); diff --git a/src/applications/pre-need-integration/config/pages/sponsorContactInformation.jsx b/src/applications/pre-need-integration/config/pages/sponsorContactInformation.jsx index f20fd586aabc..642af11c9e68 100644 --- a/src/applications/pre-need-integration/config/pages/sponsorContactInformation.jsx +++ b/src/applications/pre-need-integration/config/pages/sponsorContactInformation.jsx @@ -12,29 +12,48 @@ import { bottomPadding, } from '../../utils/helpers'; +// Import veteran properties from schema const { veteran } = fullSchemaPreNeed.properties.application.properties; +// Component for state title export const sponsorMailingAddressStateTitleWrapper = ( ); +// Validation function +const isRequired = formData => { + return formData?.application?.veteran?.address.street !== undefined; +}; + +// UI Schema export const uiSchema = { application: { veteran: { - address: merge({}, address.uiSchema('Sponsor’s mailing address'), { - street: { - 'ui:title': 'Street address', - }, - street2: { - 'ui:title': 'Street address line 2', - }, - state: { - 'ui:title': sponsorMailingAddressStateTitleWrapper, - 'ui:options': { - hideIf: formData => !sponsorMailingAddressHasState(formData), + address: merge( + {}, + address.uiSchema( + 'Sponsor mailing address', + 'You can choose to enter your sponsor’s mailing address. This is optional. We’ll confirm this address with the U.S. Postal Service.', + false, + isRequired, + false, + ['city', 'postalCode'], + ), + { + street: { + 'ui:title': 'Street address', + }, + street2: { + 'ui:title': 'Street address line 2', + }, + state: { + 'ui:title': sponsorMailingAddressStateTitleWrapper, + 'ui:options': { + hideIf: formData => !sponsorMailingAddressHasState(formData), + }, }, }, - }), + ), 'view:contactInfoSubheader': { 'ui:description': sponsorContactInfoSubheader, 'ui:options': { @@ -59,6 +78,7 @@ export const uiSchema = { }, }; +// Schema export const schema = { type: 'object', properties: { @@ -68,7 +88,7 @@ export const schema = { veteran: { type: 'object', properties: { - address: address.schema(fullSchemaPreNeed), + address: address.schema(fullSchemaPreNeed, isRequired), 'view:contactInfoSubheader': { type: 'object', properties: {}, diff --git a/src/applications/pre-need-integration/config/pages/sponsorSuggestedAddress.jsx b/src/applications/pre-need-integration/config/pages/sponsorSuggestedAddress.jsx new file mode 100644 index 000000000000..c26149b45f5c --- /dev/null +++ b/src/applications/pre-need-integration/config/pages/sponsorSuggestedAddress.jsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import { setData } from 'platform/forms-system/src/js/actions'; +import { validateAddress } from 'platform/user/profile/vap-svc/actions'; +import set from 'platform/utilities/data/set'; +import AddressConfirmation from './addressConfirmation'; +import SuggestedAddressRadio from '../../components/SuggestedAddressRadio'; + +function SponsorSuggestedAddress({ formData, addressValidation }) { + const dispatch = useDispatch(); + const [userAddress, setUserAddress] = useState(null); + const [selectedAddress, setSelectedAddress] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [suggestedAddress, setSuggestedAddress] = useState(null); + + const extractUserAddress = () => { + return formData?.application?.veteran?.address || {}; + }; + + // Prepare address for API Request + const prepareAddressForAPI = address => ({ + addressLine1: address.street, + addressLine2: address.street2, + addressPou: 'CORRESPONDENCE', + addressType: 'DOMESTIC', + city: address.city, + countryCodeIso3: address.country, + stateCode: address.state, + zipCode: address.postalCode, + }); + + const shouldShowSuggestedAddress = () => { + if (!suggestedAddress?.addressLine1 || !userAddress?.street) return false; + return !( + userAddress.street === suggestedAddress.addressLine1 && + userAddress.city === suggestedAddress.city && + userAddress.state === suggestedAddress.stateCode && + userAddress.postalCode === suggestedAddress.zipCode && + userAddress.country === suggestedAddress.countryCodeIso3 + ); + }; + + useEffect(() => { + async function fetchSuggestedAddresses() { + try { + const formDataUserAddress = extractUserAddress(); + setUserAddress(formDataUserAddress); + setSelectedAddress(formDataUserAddress); + + await dispatch( + validateAddress( + '/profile/addresses', + 'POST', + 'mailingAddress', + prepareAddressForAPI(formDataUserAddress), + 'mailing-address', + ), + ); + } catch (error) { + setIsLoading(true); // This is temporary, send it to address confirmation screen instead + } + } + fetchSuggestedAddresses(); + }, []); + + useEffect( + () => { + if (addressValidation?.addressFromUser?.addressLine1) setIsLoading(false); + }, + [addressValidation], + ); + + // Update suggested address when addressValidation changes + useEffect( + () => { + setSuggestedAddress(addressValidation?.confirmedSuggestions[0]); + }, + [addressValidation], + ); + + // Handle Address Selection Change + const onChangeSelectedAddress = event => { + const selected = JSON.parse(event.detail.value); + setSelectedAddress(selected); + let newAddress; + if ('addressLine1' in selected) { + newAddress = { + street: selected.addressLine1, + street2: selected.addressLine2, + city: selected.city, + country: selected.countryCodeIso3, + state: selected.stateCode, + postalCode: selected.zipCode, + }; + } else { + newAddress = selected; + } + const updatedFormData = set( + 'application.veteran.address', + newAddress, + formData, + ); + dispatch(setData(updatedFormData)); + }; + + if (isLoading) { + return ( + + ); + } + + return shouldShowSuggestedAddress() ? ( + + ) : ( + + ); +} + +// Map state to props +const mapStateToProps = state => ({ + formData: state?.form?.data, + addressValidation: state?.vapService?.addressValidation, +}); + +export default connect(mapStateToProps)(SponsorSuggestedAddress); diff --git a/src/applications/pre-need-integration/definitions/address.js b/src/applications/pre-need-integration/definitions/address.js index b3c66772f825..ba88da6da019 100644 --- a/src/applications/pre-need-integration/definitions/address.js +++ b/src/applications/pre-need-integration/definitions/address.js @@ -148,12 +148,15 @@ export function schema(currentSchema, isRequired = false) { * Receives formData and an index (if in an array item) * @param {boolean} ignoreRequired - Ignore the required fields array, to avoid overwriting form specific * customizations + * @param {[string]} fields - Custom list of required fields. Defaults to address fields */ export function uiSchema( label = 'Address', + description = null, useStreet3 = false, isRequired = null, ignoreRequired = false, + fields = requiredFields, ) { let fieldOrder = [ 'country', @@ -280,6 +283,7 @@ export function uiSchema( return { 'ui:title': label, + 'ui:description': description, 'ui:validations': [validateAddress], 'ui:options': { updateSchema: (formData, addressSchema, addressUiSchema, index, path) => { @@ -334,7 +338,7 @@ export function uiSchema( if (isRequired) { const required = isRequired(formData, index); if (required && currentSchema.required.length === 0) { - currentSchema = set('required', requiredFields, currentSchema); + currentSchema = set('required', fields, currentSchema); } else if (!required && currentSchema.required.length > 0) { currentSchema = set('required', [], currentSchema); } @@ -355,6 +359,9 @@ export function uiSchema( street: { 'ui:title': 'Street', 'ui:autocomplete': 'address-line1', + 'ui:errorMessages': { + required: 'Enter a street address', + }, }, street2: { 'ui:title': 'Line 2', @@ -367,12 +374,49 @@ export function uiSchema( city: { 'ui:title': 'City', 'ui:autocomplete': 'address-level2', + 'ui:errorMessages': { + required: 'Enter a city', + }, + }, + state: { + 'ui:errorMessages': { + enum: 'Select a state or territory', + required: 'Select a state or territory', + }, }, postalCode: { 'ui:title': 'Postal code', 'ui:autocomplete': 'postal-code', 'ui:options': { widgetClassNames: 'usa-input-medium', + replaceSchema: (formData, _schema, _uiSchema, index, path) => { + const addressPath = path.slice(0, -1); + const data = get(addressPath, formData) ?? {}; + const { country } = data; + const addressSchema = _schema; + const addressUiSchema = _uiSchema; + + // country-specific error messages + if (country === 'USA') { + addressUiSchema['ui:errorMessages'] = { + required: 'Please provide a valid postal code', + }; + } else if (['CAN', 'MEX'].includes(country) || !country) { + addressUiSchema['ui:errorMessages'] = { + required: 'Enter a postal code', + }; + } else { + // no pattern validation for other countries + addressUiSchema['ui:errorMessages'] = { + required: + 'Enter a postal code that meets your country’s requirements. If your country doesn’t require a postal code, enter N/A.', + }; + } + + return { + ...addressSchema, + }; + }, }, }, }; diff --git a/src/applications/pre-need-integration/tests/components/SuggestedAddressRadio.unit.spec.jsx b/src/applications/pre-need-integration/tests/components/SuggestedAddressRadio.unit.spec.jsx new file mode 100644 index 000000000000..75ff01537dad --- /dev/null +++ b/src/applications/pre-need-integration/tests/components/SuggestedAddressRadio.unit.spec.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import SuggestedAddressRadio from '../../components/SuggestedAddressRadio'; + +describe('Suggested Address Radio Component', () => { + const mockSuggestedAddress = { + confirmedSuggestions: [ + { + addressLine1: '123 Mock St', + city: 'Mock City', + stateCode: 'MC', + zipCode: '12345', + countryCodeIso3: 'USA', + }, + ], + }; + + const mockUserAddress = { + address: { + street: '1234 Mock St', + city: 'Mock City', + state: 'MC', + zipCode: '12345', + country: 'USA', + }, + }; + const mockOnChangeSelectedAddress = () => {}; + const props = { + title: 'Confirm your mailing address', + userAddress: mockUserAddress, // not mockUserAddress: ... + selectedAddress: mockUserAddress, // or null/empty if you want + addressValidation: mockSuggestedAddress, + onChangeSelectedAddress: mockOnChangeSelectedAddress, + }; + + it('should render', () => { + const wrapper = mount(); + expect(wrapper.find('va-radio-option').length).to.equal(2); + wrapper.unmount(); + }); + + // it('should invoke onChange when a radio button is clicked', () => { + // const onChange = sinon.spy(); + // const wrapper = mount( + // , + // ); + + // const vaRadio = wrapper.find('VaRadio'); + // expect(vaRadio.exists()).to.be.true; + + // vaRadio + // .props() + // .onVaValueChange({ detail: { value: 'Pinhead', checked: true } }); + // expect(onChange.calledWith('Pinhead')).to.be.true; + // wrapper.unmount(); + // }); +}); diff --git a/src/applications/pre-need-integration/tests/config/applicantMilitaryName.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/applicantMilitaryName.unit.spec.jsx index 6dc54ed7668b..82960e8aebed 100644 --- a/src/applications/pre-need-integration/tests/config/applicantMilitaryName.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/applicantMilitaryName.unit.spec.jsx @@ -1,67 +1,67 @@ -// import React from 'react'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; -// import { mount } from 'enzyme'; +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; -// import { -// DefinitionTester, -// selectRadio, -// } from 'platform/testing/unit/schemaform-utils.jsx'; -// import formConfig from '../../config/form'; +import { + DefinitionTester, + selectRadio, +} from 'platform/testing/unit/schemaform-utils.jsx'; +import formConfig from '../../config/form'; -// describe('Pre-need applicant military name', () => { -// const { -// schema, -// uiSchema, -// } = formConfig.chapters.militaryHistory.pages.applicantMilitaryNameSelf; +describe('Pre-need applicant military name', () => { + const { + schema, + uiSchema, + } = formConfig.chapters.militaryName.pages.applicantMilitaryNameSelf; -// it('should render', () => { -// const form = mount( -// , -// ); + it('should render', () => { + const form = mount( + , + ); -// expect(form.find('input').length).to.equal(2); -// form.unmount(); -// }); + expect(form.find('input').length).to.equal(2); + form.unmount(); + }); -// it('should not submit empty form', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should not submit empty form', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// form.find('form').simulate('submit'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(1); -// expect(onSubmit.called).to.be.false; -// form.unmount(); -// }); + expect(form.find('.usa-input-error').length).to.equal(1); + expect(onSubmit.called).to.be.false; + form.unmount(); + }); -// it('should submit with required fields filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with required fields filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// selectRadio(form, 'root_application_veteran_view:hasServiceName', 'N'); -// form.find('form').simulate('submit'); + selectRadio(form, 'root_application_veteran_view:hasServiceName', 'N'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformation.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformation.unit.spec.jsx index c270be2bf63d..b143100e413c 100644 --- a/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformation.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformation.unit.spec.jsx @@ -1,74 +1,74 @@ -// import React from 'react'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; -// import { mount } from 'enzyme'; +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; -// import { -// DefinitionTester, -// fillData, -// } from 'platform/testing/unit/schemaform-utils.jsx'; -// import formConfig from '../../config/form'; +import { + DefinitionTester, + fillData, +} from 'platform/testing/unit/schemaform-utils.jsx'; +import formConfig from '../../config/form'; -// describe('Pre-need applicant military name information', () => { -// const { -// schema, -// uiSchema, -// } = formConfig.chapters.militaryHistory.pages.applicantMilitaryNameInformation; +describe('Pre-need applicant military name information', () => { + const { + schema, + uiSchema, + } = formConfig.chapters.militaryName.pages.applicantMilitaryNameInformation; -// it('should render', () => { -// const form = mount( -// , -// ); + it('should render', () => { + const form = mount( + , + ); -// expect(form.find('input').length).to.equal(3); -// expect(form.find('select').length).to.equal(1); -// form.unmount(); -// }); + expect(form.find('input').length).to.equal(3); + expect(form.find('select').length).to.equal(1); + form.unmount(); + }); -// it('should submit with required fields filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with required fields filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// form.find('form').simulate('submit'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); -// it('should submit with all info filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with all info filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); -// fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); + fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); -// form.find('form').simulate('submit'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformationPreparer.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformationPreparer.unit.spec.jsx index e3651f2dbc67..36112f7c615b 100644 --- a/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformationPreparer.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/applicantMilitaryNameInformationPreparer.unit.spec.jsx @@ -1,74 +1,74 @@ -// import React from 'react'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; -// import { mount } from 'enzyme'; +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; -// import { -// DefinitionTester, -// fillData, -// } from 'platform/testing/unit/schemaform-utils.jsx'; -// import formConfig from '../../config/form'; +import { + DefinitionTester, + fillData, +} from 'platform/testing/unit/schemaform-utils.jsx'; +import formConfig from '../../config/form'; -// describe('Pre-need Integration Preparer military name information', () => { -// const { -// schema, -// uiSchema, -// } = formConfig.chapters.militaryHistory.pages.applicantMilitaryNameInformationPreparer; +describe('Pre-need Integration Preparer military name information', () => { + const { + schema, + uiSchema, + } = formConfig.chapters.militaryName.pages.applicantMilitaryNameInformationPreparer; -// it('should render', () => { -// const form = mount( -// , -// ); + it('should render', () => { + const form = mount( + , + ); -// expect(form.find('input').length).to.equal(3); -// expect(form.find('select').length).to.equal(1); -// form.unmount(); -// }); + expect(form.find('input').length).to.equal(3); + expect(form.find('select').length).to.equal(1); + form.unmount(); + }); -// it('should submit with required fields filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with required fields filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// form.find('form').simulate('submit'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); -// it('should submit with all info filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with all info filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); -// fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); + fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); -// form.find('form').simulate('submit'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/applicantSuggestedAddress.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/applicantSuggestedAddress.unit.spec.jsx new file mode 100644 index 000000000000..8b837e3bf39b --- /dev/null +++ b/src/applications/pre-need-integration/tests/config/applicantSuggestedAddress.unit.spec.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { expect } from 'chai'; +import { act } from 'react-dom/test-utils'; +import { mount } from 'enzyme'; +import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; +import thunk from 'redux-thunk'; +import formConfig from '../../config/form'; + +const mockStore = configureMockStore([thunk]); + +const payload = { + claimant: { + address: { + street: '123 Test Street', + city: 'City', + state: 'MC', + postalCode: '28226', + country: 'USA', + }, + }, +}; + +const createStore = confirmedSuggestions => + mockStore({ + form: { + data: { + application: payload, + }, + }, + vapService: { + addressValidation: { + confirmedSuggestions, + addressFromUser: { + addressLine1: '123 Test', + }, + }, + }, + }); + +// Helper to flush promises in the event loop +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + +// Reusable test setup function +const renderComponent = async store => { + let wrapper; + + await act(async () => { + wrapper = mount( + + + , + ); + }); + + // Wait for all async effects to complete + await act(async () => { + await flushPromises(); + }); + + // Trigger a re-render + wrapper.update(); + + return wrapper; +}; + +describe('Applicant Suggested Address', () => { + it('should render suggested address radio if given a suggested address', async () => { + const store = createStore([ + { + addressLine1: '123 Mock St', + city: 'Mock City', + stateCode: 'MC', + zipCode: '12345', + countryCodeIso3: 'USA', + }, + ]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(1); + expect(wrapper.find('AddressConfirmation').length).to.equal(0); + + wrapper.unmount(); + }); + + it('should render confirm address if NOT given a suggested address', async () => { + const store = createStore([]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(0); + expect(wrapper.find('AddressConfirmation').length).to.equal(1); + + wrapper.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/preparerSuggestedAddress.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/preparerSuggestedAddress.unit.spec.jsx new file mode 100644 index 000000000000..626963e818e1 --- /dev/null +++ b/src/applications/pre-need-integration/tests/config/preparerSuggestedAddress.unit.spec.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { expect } from 'chai'; +import { act } from 'react-dom/test-utils'; +import { mount } from 'enzyme'; +import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; +import thunk from 'redux-thunk'; +import formConfig from '../../config/form'; + +const mockStore = configureMockStore([thunk]); + +const payload = { + applicant: { + 'view:applicantInfo': { + mailingAddress: { + street: '123 Test Street', + city: 'City', + state: 'MC', + postalCode: '28226', + country: 'USA', + }, + }, + }, +}; + +const createStore = confirmedSuggestions => + mockStore({ + form: { + data: { + application: payload, + }, + }, + vapService: { + addressValidation: { + confirmedSuggestions, + addressFromUser: { + addressLine1: '123 Test', + }, + }, + }, + }); + +// Helper to flush promises in the event loop +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + +// Reusable test setup function +const renderComponent = async store => { + let wrapper; + + await act(async () => { + wrapper = mount( + + + , + ); + }); + + // Wait for all async effects to complete + await act(async () => { + await flushPromises(); + }); + + // Trigger a re-render + wrapper.update(); + + return wrapper; +}; + +describe('Preparer Suggested Address', () => { + it('should render suggested address radio if given a suggested address', async () => { + const store = createStore([ + { + addressLine1: '123 Mock St', + city: 'Mock City', + stateCode: 'MC', + zipCode: '12345', + countryCodeIso3: 'USA', + }, + ]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(1); + expect(wrapper.find('AddressConfirmation').length).to.equal(0); + + wrapper.unmount(); + }); + + it('should render confirm address if NOT given a suggested address', async () => { + const store = createStore([]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(0); + expect(wrapper.find('AddressConfirmation').length).to.equal(1); + + wrapper.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/servicePeriods.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/servicePeriods.unit.spec.jsx index 7a3460950ffe..d06e48b1b789 100644 --- a/src/applications/pre-need-integration/tests/config/servicePeriods.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/servicePeriods.unit.spec.jsx @@ -1,153 +1,182 @@ -// import React from 'react'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; -// import { mount } from 'enzyme'; -// import { Provider } from 'react-redux'; -// import configureMockStore from 'redux-mock-store'; - -// import { -// DefinitionTester, -// fillDate, -// } from 'platform/testing/unit/schemaform-utils.jsx'; -// import formConfig from '../../config/form'; - -// const mockStore = configureMockStore(); - -// const payload = { -// application: { -// claimant: { -// dateOfBirth: '2000-1-1', // This DOB should be before the service dates being tested -// }, -// veteran: { -// serviceRecords: [ -// { -// serviceBranch: 'AL', -// dateRange: { -// from: '2002-1-1', -// to: '2003-1-1', -// }, -// }, -// ], -// }, -// }, -// }; - -// const store = mockStore({ -// form: { -// data: payload, -// }, -// }); - -// describe('Pre-need service periods', () => { -// function servicePeriodsTests({ schema, uiSchema }, inputCount = 4) { -// it('should render', () => { -// const form = mount( -// -// -// , -// ); - -// expect(form.find('input').length).to.equal(inputCount); -// expect(form.find('select').length).to.equal(5); -// form.unmount(); -// }); - -// it('should not submit empty form', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// -// -// , -// ); - -// form.find('form').simulate('submit'); - -// expect(form.find('.usa-input-error').length).to.equal(2); -// expect(onSubmit.called).to.be.false; -// form.unmount(); -// }); - -// it.skip('should add another service period', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// -// -// , -// ); - -// expect(form.find('input').length).to.equal(inputCount); -// expect(form.find('select').length).to.equal(5); - -// form.find('.va-growable-add-btn').simulate('click'); - -// expect( -// form -// .find('.va-growable-background') -// .first() -// .text(), -// ).to.contain('Allied Forces'); -// form.unmount(); -// }); - -// it('should submit with valid data', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// -// -// , -// ); - -// fillDate( -// form, -// 'root_application_veteran_serviceRecords_0_dateRange_from', -// '2002-1-1', -// ); -// fillDate( -// form, -// 'root_application_veteran_serviceRecords_0_dateRange_to', -// '2003-1-1', -// ); - -// form.find('form').simulate('submit'); - -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); -// } - -// const { sponsorMilitaryHistory } = formConfig.chapters.militaryHistory.pages; -// const { -// applicantMilitaryHistorySelf, -// } = formConfig.chapters.militaryHistory.pages; - -// describe('sponsor', () => { -// servicePeriodsTests(sponsorMilitaryHistory); -// }); - -// describe('applicant', () => { -// servicePeriodsTests(applicantMilitaryHistorySelf); -// }); -// }); +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { DefinitionTester } from '@department-of-veterans-affairs/platform-testing/schemaform-utils'; +import sinon from 'sinon'; + +import formConfig from '../../config/form'; +import { + servicePeriodInformationPage, + handleGetItemName, + handleAlertMaxItems, + handleCardDescription, + handleCancelAddTitle, + handleCancelAddNo, + handleDeleteTitle, + handleDeleteDescription, + handleDeleteNeedAtLeastOneDescription, + handleDeleteYes, + handleDeleteNo, + handleCancelEditTitle, + handleCancelEditDescription, + handleCancelEditYes, + handleCancelEditNo, + handleSummaryTitle, + handleVeteranDepends, + handlePreparerVeteranDepends, + handleNonVeteranDepends, + handlePreparerNonVeteranDepends, +} from '../../config/pages/servicePeriodsPages'; + +describe('pension add federal medical centers page', () => { + it('should render', () => { + const form = mount( + , + ); + + expect(form.find('select').length).to.equal(5); + expect(form.find('input').length).to.equal(4); + form.unmount(); + }); + + it('should not submit empty form', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); + + form.find('form').simulate('submit'); + + expect(form.find('.usa-input-error').length).to.equal(1); + expect(onSubmit.called).to.be.false; + form.unmount(); + }); + + it('should handle page text', () => { + expect(handleGetItemName({ serviceBranch: 'AC' })).to.equal( + 'U.S. Army Air Corps', + ); + + expect(handleAlertMaxItems()).to.equal( + 'You have added the maximum number of allowed service periods for this application. You may edit or delete a service period or choose to continue the application.', + ); + + expect( + handleCardDescription({ + dateRange: { from: '19500315', to: '20000523' }, + }), + ).to.equal('03/15/1950 - 05/23/2000'); + + expect( + handleCancelAddTitle({ + getItemName: handleGetItemName, + itemData: { serviceBranch: 'AC' }, + }), + ).to.equal('Cancel adding U.S. Army Air Corps service period'); + + expect( + handleCancelAddTitle({ + getItemName: handleGetItemName, + itemData: { serviceBranch: null }, + }), + ).to.equal('Cancel adding this service period'); + + expect(handleCancelAddNo()).to.equal('No, keep this'); + + expect( + handleDeleteTitle({ + getItemName: handleGetItemName, + itemData: { serviceBranch: 'AC' }, + }), + ).to.equal( + 'Are you sure you want to remove this U.S. Army Air Corps service period?', + ); + + expect( + handleDeleteDescription({ + getItemName: handleGetItemName, + itemData: { serviceBranch: 'AC' }, + }), + ).to.equal( + 'This will remove U.S. Army Air Corps and all the information from the service period records.', + ); + + expect(handleDeleteNeedAtLeastOneDescription()).to.equal( + 'If you remove this service period, we’ll take you to a screen where you can add another service period. You’ll need to list at least one service period for us to process this form.', + ); + + expect(handleDeleteYes()).to.equal('Yes, remove this'); + + expect(handleDeleteNo()).to.equal('No, keep this'); + + expect( + handleCancelEditTitle({ + getItemName: handleGetItemName, + itemData: { serviceBranch: 'AC' }, + }), + ).to.equal('Cancel editing U.S. Army Air Corps service period?'); + + expect(handleCancelEditDescription()).to.equal( + 'If you cancel, you’ll lose any changes you made on this screen and you will be returned to the service periods review page.', + ); + + expect(handleCancelEditYes()).to.equal('Yes, cancel'); + + expect(handleCancelEditNo()).to.equal('No, keep this'); + + expect(handleSummaryTitle({})).to.equal('Review service period records'); + + expect( + handleVeteranDepends({ + application: { + claimant: { relationshipToVet: 'veteran' }, + applicant: { + applicantRelationshipToClaimant: 'Authorized Agent/Rep', + }, + }, + }), + ).to.equal(false); + + expect( + handlePreparerVeteranDepends({ + application: { + claimant: { relationshipToVet: 'veteran' }, + applicant: { + applicantRelationshipToClaimant: 'Authorized Agent/Rep', + }, + }, + }), + ).to.equal(true); + + expect( + handleNonVeteranDepends({ + application: { + claimant: { relationshipToVet: 'veteran' }, + applicant: { + applicantRelationshipToClaimant: 'Authorized Agent/Rep', + }, + }, + }), + ).to.equal(false); + + expect( + handlePreparerNonVeteranDepends({ + application: { + claimant: { relationshipToVet: 'veteran' }, + applicant: { + applicantRelationshipToClaimant: 'Authorized Agent/Rep', + }, + }, + }), + ).to.equal(false); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/sponsorMilitaryNameInformation.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/sponsorMilitaryNameInformation.unit.spec.jsx index 2add81c7a1f8..45c79e28634f 100644 --- a/src/applications/pre-need-integration/tests/config/sponsorMilitaryNameInformation.unit.spec.jsx +++ b/src/applications/pre-need-integration/tests/config/sponsorMilitaryNameInformation.unit.spec.jsx @@ -1,74 +1,74 @@ -// import React from 'react'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; -// import { mount } from 'enzyme'; +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; -// import { -// DefinitionTester, -// fillData, -// } from 'platform/testing/unit/schemaform-utils.jsx'; -// import formConfig from '../../config/form'; +import { + DefinitionTester, + fillData, +} from 'platform/testing/unit/schemaform-utils.jsx'; +import formConfig from '../../config/form'; -// describe('Pre-need sponsor military name information', () => { -// const { -// schema, -// uiSchema, -// } = formConfig.chapters.militaryHistory.pages.sponsorMilitaryNameInformation; +describe('Pre-need sponsor military name information', () => { + const { + schema, + uiSchema, + } = formConfig.chapters.militaryName.pages.sponsorMilitaryNameInformation; -// it('should render', () => { -// const form = mount( -// , -// ); + it('should render', () => { + const form = mount( + , + ); -// expect(form.find('input').length).to.equal(3); -// expect(form.find('select').length).to.equal(1); -// form.unmount(); -// }); + expect(form.find('input').length).to.equal(3); + expect(form.find('select').length).to.equal(1); + form.unmount(); + }); -// it('should submit with required fields filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with required fields filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// form.find('form').simulate('submit'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); -// it('should submit with all info filled in', () => { -// const onSubmit = sinon.spy(); -// const form = mount( -// , -// ); + it('should submit with all info filled in', () => { + const onSubmit = sinon.spy(); + const form = mount( + , + ); -// fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); -// fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); -// fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); -// fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); + fillData(form, 'input#root_application_veteran_serviceName_last', 'Smith'); + fillData(form, 'input#root_application_veteran_serviceName_first', 'Jane'); + fillData(form, 'input#root_application_veteran_serviceName_middle', 'M'); + fillData(form, 'select#root_application_veteran_serviceName_suffix', 'Jr.'); -// form.find('form').simulate('submit'); + form.find('form').simulate('submit'); -// expect(form.find('.usa-input-error').length).to.equal(0); -// expect(onSubmit.called).to.be.true; -// form.unmount(); -// }); -// }); + expect(form.find('.usa-input-error').length).to.equal(0); + expect(onSubmit.called).to.be.true; + form.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/tests/config/sponsorSuggestedAddress.unit.spec.jsx b/src/applications/pre-need-integration/tests/config/sponsorSuggestedAddress.unit.spec.jsx new file mode 100644 index 000000000000..2fbdca584129 --- /dev/null +++ b/src/applications/pre-need-integration/tests/config/sponsorSuggestedAddress.unit.spec.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { expect } from 'chai'; +import { act } from 'react-dom/test-utils'; +import { mount } from 'enzyme'; +import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; +import thunk from 'redux-thunk'; +import formConfig from '../../config/form'; + +const mockStore = configureMockStore([thunk]); + +const payload = { + veteran: { + address: { + street: '123 Test Street', + city: 'City', + state: 'MC', + postalCode: '28226', + country: 'USA', + }, + }, +}; + +const createStore = confirmedSuggestions => + mockStore({ + form: { + data: { + application: payload, + }, + }, + vapService: { + addressValidation: { + confirmedSuggestions, + addressFromUser: { + addressLine1: '123 Test', + }, + }, + }, + }); + +// Helper to flush promises in the event loop +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + +// Reusable test setup function +const renderComponent = async store => { + let wrapper; + + await act(async () => { + wrapper = mount( + + + , + ); + }); + + // Wait for all async effects to complete + await act(async () => { + await flushPromises(); + }); + + // Trigger a re-render + wrapper.update(); + + return wrapper; +}; + +describe('Sponsort Suggested Address', () => { + it('should render suggested address radio if given a suggested address', async () => { + const store = createStore([ + { + addressLine1: '123 Mock St', + city: 'Mock City', + stateCode: 'MC', + zipCode: '12345', + countryCodeIso3: 'USA', + }, + ]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(1); + expect(wrapper.find('AddressConfirmation').length).to.equal(0); + + wrapper.unmount(); + }); + + it('should render confirm address if NOT given a suggested address', async () => { + const store = createStore([]); + + const wrapper = await renderComponent(store); + + expect(wrapper.find('va-loading-indicator').length).to.equal(0); + expect(wrapper.find('SuggestedAddressRadio').length).to.equal(0); + expect(wrapper.find('AddressConfirmation').length).to.equal(1); + + wrapper.unmount(); + }); +}); diff --git a/src/applications/pre-need-integration/utils/helpers.js b/src/applications/pre-need-integration/utils/helpers.js index ba9841cf8850..6bf0e649e322 100644 --- a/src/applications/pre-need-integration/utils/helpers.js +++ b/src/applications/pre-need-integration/utils/helpers.js @@ -14,6 +14,7 @@ import VaCheckboxGroupField from 'platform/forms-system/src/js/web-component-fie import VaTextInputField from 'platform/forms-system/src/js/web-component-fields/VaTextInputField'; import VaSelectField from 'platform/forms-system/src/js/web-component-fields/VaSelectField'; import currentOrPastDateUI from 'platform/forms-system/src/js/definitions/currentOrPastDate'; +import { countries } from 'platform/forms/address'; import { stringifyFormReplacer, @@ -288,7 +289,7 @@ export const nonVeteranApplicantDetailsDescriptionPreparer = export const applicantContactInfoAddressTitle = 'Your mailing address'; export const applicantContactInfoPreparerAddressTitle = - 'Applicant’s mailing address'; + 'Applicant mailing address'; export const applicantContactInfoSubheader = (
    @@ -1364,3 +1365,27 @@ export function MailingAddressStateTitle(props) { } return 'State or territory'; } + +export const formatSuggestedAddress = address => { + if (address) { + let displayAddress = ''; + const street = address.street || address.addressLine1; + const street2 = address.street2 || address.addressLine2; + const { city } = address; + const state = address.state || address.stateCode; + const zip = address.postalCode || address.zipCode; + const country = address.country || address.countryCodeIso3; + + if (street) displayAddress += street; + if (street2) displayAddress += `, ${street2}`; + if (city) displayAddress += `, ${city}`; + if (state) displayAddress += `, ${state}`; + if (zip) displayAddress += ` ${zip}`; + if (country && country !== 'USA') + displayAddress += `, ${countries.find(c => c.value === country).label || + country}`; + + return displayAddress.trim(); + } + return ''; +}; diff --git a/src/applications/registry.scaffold.json b/src/applications/registry.scaffold.json index 30867fdf5c2b..0a21e17e1329 100644 --- a/src/applications/registry.scaffold.json +++ b/src/applications/registry.scaffold.json @@ -14,7 +14,7 @@ "rootUrl": "/find-forms", "widgetType": "find-va-forms", "productDirectory": { - "filePath": "src/applications/find-forms", + "filePath": "src/applications/static-pages/find-forms", "productId": 206, "entryName": "find-va-forms" } diff --git a/src/applications/representative-appoint/config/prefillTransformer.js b/src/applications/representative-appoint/config/prefillTransformer.js index 5d18450683ba..671d21c25482 100644 --- a/src/applications/representative-appoint/config/prefillTransformer.js +++ b/src/applications/representative-appoint/config/prefillTransformer.js @@ -6,7 +6,11 @@ export default function prefillTransformer(formData) { }; if (preparerIsVeteran({ formData })) { - newFormData.veteranFullName = formData?.personalInformation?.fullName; + newFormData.veteranFullName = { + first: formData?.personalInformation?.fullName?.first, + middle: formData?.personalInformation?.fullName?.middle?.substring(0, 1), + last: formData?.personalInformation?.fullName?.last, + }; newFormData.veteranDateOfBirth = formData?.personalInformation?.dateOfBirth; newFormData.veteranSocialSecurityNumber = formData?.personalInformation?.ssn; @@ -34,7 +38,11 @@ export default function prefillTransformer(formData) { street: undefined, }; } else { - newFormData.applicantName = formData?.personalInformation?.fullName; + newFormData.applicantName = { + first: formData?.personalInformation?.fullName?.first, + middle: formData?.personalInformation?.fullName?.middle?.substring(0, 1), + last: formData?.personalInformation?.fullName?.last, + }; newFormData.applicantDOB = formData?.personalInformation?.dateOfBirth; newFormData.applicantEmail = formData?.contactInformation?.email; newFormData.applicantPhone = formData?.contactInformation?.primaryPhone; diff --git a/src/applications/representative-appoint/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/representative-appoint/tests/containers/ConfirmationPage.unit.spec.jsx index 935eb91dc106..67f845a88c1a 100644 --- a/src/applications/representative-appoint/tests/containers/ConfirmationPage.unit.spec.jsx +++ b/src/applications/representative-appoint/tests/containers/ConfirmationPage.unit.spec.jsx @@ -37,13 +37,13 @@ describe('ConfirmationPage', () => { selectedAccreditedOrganizationId: '1', applicantName: { first: 'John', - middle: 'Edmund', + middle: 'E', last: 'Doe', suffix: 'Sr.', }, veteranFullName: { first: 'John', - middle: 'Edmund', + middle: 'E', last: 'Doe', suffix: 'Sr.', }, diff --git a/src/applications/representative-appoint/tests/containers/PreSubmitInfo.unit.spec.jsx b/src/applications/representative-appoint/tests/containers/PreSubmitInfo.unit.spec.jsx index d9e343c653fc..818e0e58d5f1 100644 --- a/src/applications/representative-appoint/tests/containers/PreSubmitInfo.unit.spec.jsx +++ b/src/applications/representative-appoint/tests/containers/PreSubmitInfo.unit.spec.jsx @@ -17,7 +17,7 @@ describe('', () => { formData: { veteranFullName: { first: 'John', - middle: 'Edmund', + middle: 'E', last: 'Doe', suffix: 'Sr.', }, @@ -63,7 +63,7 @@ describe('', () => { const { container } = renderContainer(props, mockStore); const content = $('va-accordion-item', container); - expect(content.textContent).to.contain('John Edmund Doe Sr.'); + expect(content.textContent).to.contain('John E Doe Sr.'); }); it('should include the representative name', () => { diff --git a/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js b/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js index 6c900695764d..420d24a7644a 100644 --- a/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js +++ b/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js @@ -67,7 +67,7 @@ describe('Authenticated', () => { h.verifyUrl(ROUTES.VETERAN_PERSONAL_INFORMATION); cy.injectAxeThenAxeCheck(); cy.get('input[name="root_veteranFullName_first"]').type('John'); - cy.get('input[name="root_veteranFullName_middle"]').type('Edmund'); + cy.get('input[name="root_veteranFullName_middle"]').type('E'); cy.get('input[name="root_veteranFullName_last"]').type('Doe'); cy.get('va-select.usa-form-group--month-select') diff --git a/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js b/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js index 2b5a5596f4d2..1c6df30fbb0c 100644 --- a/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js +++ b/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js @@ -69,7 +69,7 @@ describe('Unauthenticated', () => { cy.injectAxeThenAxeCheck(); cy.get('input[name="root_applicantName_first"]').type('Adam'); - cy.get('input[name="root_applicantName_middle"]').type('James'); + cy.get('input[name="root_applicantName_middle"]').type('J'); cy.get('input[name="root_applicantName_last"]').type('Friedman'); cy.get('va-select.usa-form-group--month-select') @@ -114,7 +114,7 @@ describe('Unauthenticated', () => { // VETERAN_PERSONAL_INFORMATION; h.verifyUrl(ROUTES.VETERAN_PERSONAL_INFORMATION); cy.get('input[name="root_veteranFullName_first"]').type('John'); - cy.get('input[name="root_veteranFullName_middle"]').type('Edmund'); + cy.get('input[name="root_veteranFullName_middle"]').type('E'); cy.get('input[name="root_veteranFullName_last"]').type('Doe'); cy.get('va-select.usa-form-group--month-select') diff --git a/src/applications/representative-appoint/tests/fixtures/data/21-22a/form-data.json b/src/applications/representative-appoint/tests/fixtures/data/21-22a/form-data.json index 534e138763b6..045331ecd796 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/21-22a/form-data.json +++ b/src/applications/representative-appoint/tests/fixtures/data/21-22a/form-data.json @@ -27,7 +27,7 @@ "claimantRelationship": "CHILD", "veteranFullName": { "first": "John", - "middle": "Edmund", + "middle": "E", "last": "Doe", "suffix": "Sr." }, diff --git a/src/applications/representative-appoint/tests/fixtures/data/form-data.json b/src/applications/representative-appoint/tests/fixtures/data/form-data.json index 55e5c1517b84..1bec6e23ace7 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/form-data.json +++ b/src/applications/representative-appoint/tests/fixtures/data/form-data.json @@ -26,7 +26,7 @@ "claimantRelationship": "CHILD", "veteranFullName": { "first": "John", - "middle": "Edmund", + "middle": "E", "last": "Doe", "suffix": "Sr." }, diff --git a/src/applications/representative-appoint/tests/fixtures/data/initial-form-data.json b/src/applications/representative-appoint/tests/fixtures/data/initial-form-data.json index 98e0521ac714..9770ad38d188 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/initial-form-data.json +++ b/src/applications/representative-appoint/tests/fixtures/data/initial-form-data.json @@ -4,7 +4,7 @@ "applicantName": {}, "veteranFullName": { "first": "John", - "middle": "Edmund", + "middle": "E", "last": "Doe", "suffix": "Sr." }, diff --git a/src/applications/representative-appoint/tests/fixtures/data/pdf-transformed-form-data.json b/src/applications/representative-appoint/tests/fixtures/data/pdf-transformed-form-data.json index f008c33d2667..4725593f838d 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/pdf-transformed-form-data.json +++ b/src/applications/representative-appoint/tests/fixtures/data/pdf-transformed-form-data.json @@ -2,7 +2,7 @@ "veteran": { "name": { "first": "John", - "middle": "Edmund", + "middle": "E", "last": "Doe" }, "ssn": "333224444", diff --git a/src/applications/representative-appoint/tests/fixtures/data/prefill.json b/src/applications/representative-appoint/tests/fixtures/data/prefill.json index 1d562bd81583..ab6f46f3707f 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/prefill.json +++ b/src/applications/representative-appoint/tests/fixtures/data/prefill.json @@ -3,7 +3,7 @@ "fullName": { "first": "Greg", "last": "Anderson", - "middle": "A" + "middle": "Alex" }, "dateOfBirth": "1933-04-05", "ssn": "796121200" diff --git a/src/applications/representative-appoint/tests/fixtures/data/test-data.json b/src/applications/representative-appoint/tests/fixtures/data/test-data.json index a4560d1abb25..1ce8f1155517 100644 --- a/src/applications/representative-appoint/tests/fixtures/data/test-data.json +++ b/src/applications/representative-appoint/tests/fixtures/data/test-data.json @@ -1,7 +1,7 @@ { "veteranFullName": { "first": "John", - "middle": "Edmund", + "middle": "E", "last": "Doe", "suffix": "Sr." }, diff --git a/src/applications/search/components/SearchMaintenance.jsx b/src/applications/search/components/SearchMaintenance.jsx index 8a96c02fee2e..3ad41350b916 100644 --- a/src/applications/search/components/SearchMaintenance.jsx +++ b/src/applications/search/components/SearchMaintenance.jsx @@ -1,6 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { getDay, getHours, setHours, setMinutes, setSeconds } from 'date-fns'; +import { + add, + getDay, + getHours, + setHours, + setMinutes, + setSeconds, +} from 'date-fns'; import { utcToZonedTime, format as tzFormat } from 'date-fns-tz'; const maintenanceDays = [2, 4]; // Days: 2 for Tuesday, 4 for Thursday @@ -29,11 +36,7 @@ const calculateCurrentMaintenanceWindow = () => { start = setSeconds(start, 0); // Calculate end time by adding the duration to the start time - let end = new Date( - start.getTime() + maintenanceDurationHours * 60 * 60 * 1000, - ); - - end = utcToZonedTime(end, maintenanceTimezone); // Ensure the end time is also adjusted to the specified timezone + const end = add(start, { hours: maintenanceDurationHours }); // Format start and end dates to include timezone offset correctly const startFormatted = tzFormat(start, "EEE MMM d yyyy HH:mm:ss 'GMT'XXXX", { diff --git a/src/applications/search/tests/e2e/helpers.js b/src/applications/search/tests/e2e/helpers.js index 67f15ffcbcc0..cfd1fe826a21 100644 --- a/src/applications/search/tests/e2e/helpers.js +++ b/src/applications/search/tests/e2e/helpers.js @@ -1,7 +1,7 @@ export const SELECTORS = { APP: '[data-e2e-id="search-app"]', SEARCH_INPUT: '#search-field', - SEARCH_BUTTON: '#search-field + button[type="submit"]', + SEARCH_BUTTON: '#search-field ~ button[type="submit"]', SEARCH_RESULTS: '[data-e2e-id="search-results"]', SEARCH_RESULTS_EMPTY: '[data-e2e-id="search-results-empty"]', SEARCH_RESULTS_TITLE: '[data-e2e-id="result-title"]', diff --git a/src/applications/simple-forms/21-0845/config/form.js b/src/applications/simple-forms/21-0845/config/form.js index 8a21f6be3ed9..236fcebc5b8a 100644 --- a/src/applications/simple-forms/21-0845/config/form.js +++ b/src/applications/simple-forms/21-0845/config/form.js @@ -7,7 +7,7 @@ import manifest from '../manifest.json'; import transformForSubmit from './submit-transformer'; import IntroductionPage from '../containers/IntroductionPage'; import ConfirmationPage from '../containers/ConfirmationPage'; -import { pageFocusScroll } from './helpers'; +import { pageFocusScroll, pageFocusScrollNoProgressBar } from './helpers'; import getHelp from '../../shared/components/GetFormHelp'; import { AUTHORIZER_TYPES, INFORMATION_SCOPES } from '../definitions/constants'; // pages @@ -104,7 +104,7 @@ const formConfig = { uiSchema: authorizerTypePg.uiSchema, schema: authorizerTypePg.schema, // needs form-level useCustomScrollAndFocus: true to work. - scrollAndFocusTarget: pageFocusScroll(), + scrollAndFocusTarget: pageFocusScrollNoProgressBar(), pageClass: 'authorizer-type', }, }, diff --git a/src/applications/simple-forms/21-0845/config/helpers.js b/src/applications/simple-forms/21-0845/config/helpers.js index 6c82bad930bb..ee781a678a49 100644 --- a/src/applications/simple-forms/21-0845/config/helpers.js +++ b/src/applications/simple-forms/21-0845/config/helpers.js @@ -1,5 +1,5 @@ import scrollTo from 'platform/utilities/ui/scrollTo'; -import { focusByOrder } from 'platform/utilities/ui'; +import { focusByOrder, waitForRenderThenFocus } from 'platform/utilities/ui'; export const pageFocusScroll = () => { return () => { @@ -9,3 +9,13 @@ export const pageFocusScroll = () => { }, 100); }; }; + +export const pageFocusScrollNoProgressBar = () => { + return () => { + scrollTo('topScrollElement'); + setTimeout(() => { + const radio = document.querySelector('va-radio[label-header-level]'); + waitForRenderThenFocus('h2', radio.shadowRoot); + }, 100); + }; +}; diff --git a/src/applications/simple-forms/21-0845/containers/ConfirmationPage.jsx b/src/applications/simple-forms/21-0845/containers/ConfirmationPage.jsx index 8584d0b8e93b..cf733e237217 100644 --- a/src/applications/simple-forms/21-0845/containers/ConfirmationPage.jsx +++ b/src/applications/simple-forms/21-0845/containers/ConfirmationPage.jsx @@ -3,65 +3,58 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect, useSelector } from 'react-redux'; +import { ConfirmationView } from 'platform/forms-system/src/js/components/ConfirmationView'; -import { ConfirmationPageView } from '../../shared/components/ConfirmationPageView'; +const submissionAlertContent = ( +

    + If you change your mind and want us to stop releasing your personal + information, you can contact us online through{' '} + + Ask VA + + + or call us at{' '} + + 1-800-827-1000 + + . We’re here Monday through Friday, 8:00 a.m. to 9:00 p.m. ET. +

    +); -const content = { - headlineText: 'Thank you for submitting your authorization', - nextStepsText: ( - <> -

    - If you change your mind and do not want VA to give out your personal - benefit or claim information, you may notify us in writing, or by - telephone at{' '} - - 1-800-827-1000 - {' '} - or contact VA online at{' '} - - Ask VA - - - . -

    -

    - Upon notification from you VA will no longer give out benefit or claim - information (except for the information VA has already given out based - on your permission). -

    - - ), -}; - -export const ConfirmationPage = () => { +export const ConfirmationPage = props => { const form = useSelector(state => state.form || {}); const { submission } = form; - const fullName = form.data?.veteranFullName; - const submitDate = submission.timestamp; - const confirmationNumber = submission.response?.confirmationNumber; + const submitDate = submission?.timestamp; + const confirmationNumber = submission?.response?.confirmationNumber; return ( - + pdfUrl={submission.response?.pdfUrl} + devOnly={{ + showButtons: true, + }} + > + } + /> + + + + + + + + ); }; ConfirmationPage.propTypes = { form: PropTypes.shape({ - data: PropTypes.shape({ - fullName: { - first: PropTypes.string.isRequired, - middle: PropTypes.string, - last: PropTypes.string.isRequired, - suffix: PropTypes.string, - }, - }), + data: PropTypes.object, formId: PropTypes.string, submission: PropTypes.shape({ response: PropTypes.shape({ @@ -69,10 +62,13 @@ ConfirmationPage.propTypes = { confirmationNumber: PropTypes.string.isRequired, }).isRequired, }).isRequired, - timestamp: PropTypes.string.isRequired, + timestamp: PropTypes.string, }), }), name: PropTypes.string, + route: PropTypes.shape({ + formConfig: PropTypes.object, + }), }; function mapStateToProps(state) { diff --git a/src/applications/simple-forms/21-0845/pages/authorizerType.js b/src/applications/simple-forms/21-0845/pages/authorizerType.js index b2c9d74dab2c..8f6925a388b6 100644 --- a/src/applications/simple-forms/21-0845/pages/authorizerType.js +++ b/src/applications/simple-forms/21-0845/pages/authorizerType.js @@ -1,33 +1,25 @@ import React from 'react'; +import { + radioUI, + radioSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { AUTHORIZER_TYPE_ITEMS } from '../definitions/constants'; import { getEnumsFromConstants, getLabelsFromConstants } from '../utils'; -const labelString = 'Who is submitting this authorization?'; - /** @type {PageSchema} */ export default { uiSchema: { - authorizerType: { - 'ui:title': ( - <> -

    {labelString}

    - Select the description that fits you. - - ), - 'ui:widget': 'radio', - 'ui:reviewField': ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    {labelString}
    -
    {children}
    -

    - ), - 'ui:options': { - labels: getLabelsFromConstants(AUTHORIZER_TYPE_ITEMS), + authorizerType: radioUI({ + title: 'Who is submitting this authorization?', + description: 'Select the description that fits you.', + labels: getLabelsFromConstants(AUTHORIZER_TYPE_ITEMS), + errorMessages: { + required: 'You must provide a response', }, - }, + labelHeaderLevel: '2', + labelHeaderLevelStyle: '3', + }), 'view:note': { 'ui:description': () => (

    @@ -52,10 +44,7 @@ export default { type: 'object', required: ['authorizerType'], properties: { - authorizerType: { - type: 'string', - enum: getEnumsFromConstants(AUTHORIZER_TYPE_ITEMS), - }, + authorizerType: radioSchema(getEnumsFromConstants(AUTHORIZER_TYPE_ITEMS)), 'view:note': { type: 'object', properties: {}, diff --git a/src/applications/simple-forms/21-0845/pages/infoScope.js b/src/applications/simple-forms/21-0845/pages/infoScope.js index 3f98b1c4945d..8090081b0439 100644 --- a/src/applications/simple-forms/21-0845/pages/infoScope.js +++ b/src/applications/simple-forms/21-0845/pages/infoScope.js @@ -1,5 +1,7 @@ -import React from 'react'; - +import { + radioUI, + radioSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { AUTHORIZER_TYPES, THIRD_PARTY_TYPES, @@ -10,65 +12,43 @@ import { getFullNameString } from '../utils'; /** @type {PageSchema} */ export default { uiSchema: { - informationScope: { - 'ui:widget': 'radio', - 'ui:options': { - labels: { - [INFORMATION_SCOPES.LIMITED]: 'Limited information', - [INFORMATION_SCOPES.ANY]: 'Any information', - }, - updateSchema: (formData, schema, uiSchema) => { - const { - authorizerType, - thirdPartyType, - personFullName, - organizationName, - } = formData; - const titleString = - authorizerType === AUTHORIZER_TYPES.VETERAN - ? 'How much information from your VA record do you authorize us to release to [third-party-name]?' - : 'I authorize VA to provide [third-party-name] the following information from my VA record:'; - const thirdPartyName = - thirdPartyType === THIRD_PARTY_TYPES.PERSON - ? getFullNameString(personFullName) - : organizationName; - - // eslint-disable-next-line no-param-reassign - uiSchema['ui:reviewField'] = ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    - {titleString.replace('[third-party-name]', thirdPartyName)} -
    -
    {children}
    -

    - ); - - return { - title: ( -

    - {titleString.replace('[third-party-name]', thirdPartyName)} -

    - ), - uiSchema, - }; - }, + informationScope: radioUI({ + labels: { + [INFORMATION_SCOPES.LIMITED]: 'Limited information', + [INFORMATION_SCOPES.ANY]: 'Any information', }, - 'ui:errorMessages': { + errorMessages: { required: 'Please select how much information you would like us to release', }, - }, + labelHeaderLevel: '3', + updateSchema: formData => { + const { + authorizerType, + thirdPartyType, + personFullName, + organizationName, + } = formData; + const titleString = + authorizerType === AUTHORIZER_TYPES.VETERAN + ? 'How much information from your VA record do you authorize us to release to [third-party-name]?' + : 'I authorize VA to provide [third-party-name] the following information from my VA record:'; + const thirdPartyName = + thirdPartyType === THIRD_PARTY_TYPES.PERSON + ? getFullNameString(personFullName) + : organizationName; + + return { + title: titleString.replace('[third-party-name]', thirdPartyName), + }; + }, + }), }, schema: { type: 'object', required: ['informationScope'], properties: { - informationScope: { - type: 'string', - enum: Object.values(INFORMATION_SCOPES), - }, + informationScope: radioSchema(Object.values(INFORMATION_SCOPES)), }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/organizationName.js b/src/applications/simple-forms/21-0845/pages/organizationName.js index 03d1be0e16bc..4f1ec79afb41 100644 --- a/src/applications/simple-forms/21-0845/pages/organizationName.js +++ b/src/applications/simple-forms/21-0845/pages/organizationName.js @@ -1,33 +1,25 @@ -import React from 'react'; +import { + titleUI, + textUI, + textSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; /** @type {PageSchema} */ export default { uiSchema: { - organizationName: { - 'ui:title':

    Organization’s name

    , - 'ui:reviewField': ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    Name of organization
    -
    {children}
    -

    - ), - 'ui:errorMessages': { + ...titleUI('Organization to disclose information to'), + organizationName: textUI({ + title: 'Organization’s name', + errorMessages: { required: 'Please enter the name of the organization', }, - 'ui:options': { - widgetClassNames: 'vads-u-margin-top--3', - }, - }, + }), }, schema: { type: 'object', required: ['organizationName'], properties: { - organizationName: { - type: 'string', - }, + organizationName: textSchema, }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/organizationReps.js b/src/applications/simple-forms/21-0845/pages/organizationReps.js index 0b5686e76753..f1677f43ab3b 100644 --- a/src/applications/simple-forms/21-0845/pages/organizationReps.js +++ b/src/applications/simple-forms/21-0845/pages/organizationReps.js @@ -1,8 +1,8 @@ -import React from 'react'; - -import { titleUI } from 'platform/forms-system/src/js/web-component-patterns'; - -const labelString = 'Name of representative'; +import { + titleUI, + textUI, + textSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; export default { uiSchema: { @@ -10,45 +10,23 @@ export default { 'Organization’s representatives', 'List at least one person from the organization who we can release your information to.', ), - organizationRepresentative: { - 'ui:title': ( - <> - {labelString}{' '} - - (*Required) - -
    - - At least one representative is required - - - ), - 'ui:reviewField': ({ children }) => ( - // remove custom required-span & description from - // review-field label. -
    -
    {labelString}
    -
    {children}
    -
    - ), - 'ui:errorMessages': { + organizationRepresentative: textUI({ + title: 'Name of representative', + description: 'At least one representative is required', + errorMessages: { required: 'Please enter the name of a representative', }, - }, - organizationRepresentative2: { - 'ui:title': 'Name of second representative (if any)', - }, + }), + organizationRepresentative2: textUI({ + title: 'Name of second representative (if any)', + }), }, schema: { type: 'object', required: ['organizationRepresentative'], properties: { - organizationRepresentative: { - type: 'string', - }, - organizationRepresentative2: { - type: 'string', - }, + organizationRepresentative: textSchema, + organizationRepresentative2: textSchema, }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/releaseDuration.js b/src/applications/simple-forms/21-0845/pages/releaseDuration.js index a03e6f6d948d..6af960a7282d 100644 --- a/src/applications/simple-forms/21-0845/pages/releaseDuration.js +++ b/src/applications/simple-forms/21-0845/pages/releaseDuration.js @@ -1,5 +1,7 @@ -import React from 'react'; - +import { + radioUI, + radioSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { THIRD_PARTY_TYPES, RELEASE_DURATIONS } from '../definitions/constants'; import { getEnumsFromConstants, @@ -10,55 +12,41 @@ import { /** @type {PageSchema} */ export default { uiSchema: { - // required span needs to be in page header here, per mockup - 'ui:title': ( -

    - How long do you authorize us to release your information for?{' '} - (*Required) -

    - ), - releaseDuration: { - 'ui:widget': 'radio', - 'ui:errorMessages': { + releaseDuration: radioUI({ + title: 'How long do you authorize us to release your information for?', + labels: getLabelsFromConstants(RELEASE_DURATIONS), + errorMessages: { required: 'Please select a release duration.', }, - 'ui:options': { - // dynamically update title based on form data - // it's serving more as a description here than a label - // we're hiding required span from this label via styling, per mockup - updateSchema: formData => { - const { thirdPartyType, personFullName, organizationName } = formData; - let thirdPartyName = '[third-party name]'; + labelHeaderLevel: '3', + updateSchema: formData => { + const { thirdPartyType, personFullName, organizationName } = formData; + let thirdPartyName = '[third-party name]'; - if ( - thirdPartyType === THIRD_PARTY_TYPES.PERSON && - personFullName.first && - personFullName.last - ) { - thirdPartyName = getFullNameString(personFullName); - } else if ( - thirdPartyType === THIRD_PARTY_TYPES.ORGANIZATION && - organizationName - ) { - thirdPartyName = organizationName; - } + if ( + thirdPartyType === THIRD_PARTY_TYPES.PERSON && + personFullName.first && + personFullName.last + ) { + thirdPartyName = getFullNameString(personFullName); + } else if ( + thirdPartyType === THIRD_PARTY_TYPES.ORGANIZATION && + organizationName + ) { + thirdPartyName = organizationName; + } - return { - title: `Tell us when, and how long, we should release your information to ${thirdPartyName}.`, - }; - }, - labels: getLabelsFromConstants(RELEASE_DURATIONS), + return { + title: `Tell us when, and how long, we should release your information to ${thirdPartyName}.`, + }; }, - }, + }), }, schema: { type: 'object', required: ['releaseDuration'], properties: { - releaseDuration: { - type: 'string', - enum: getEnumsFromConstants(RELEASE_DURATIONS), - }, + releaseDuration: radioSchema(getEnumsFromConstants(RELEASE_DURATIONS)), }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/releaseEndDate.js b/src/applications/simple-forms/21-0845/pages/releaseEndDate.js index e1c8860cbbe2..7a5f921f0921 100644 --- a/src/applications/simple-forms/21-0845/pages/releaseEndDate.js +++ b/src/applications/simple-forms/21-0845/pages/releaseEndDate.js @@ -1,44 +1,31 @@ -import React from 'react'; - -import definitions from 'vets-json-schema/dist/definitions.json'; - +import { + titleUI, + currentOrPastDateUI, + currentOrPastDateSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { releaseEndDateValidation } from '../validations'; -const labelString = 'When should we stop releasing your information?'; - /** @type {PageSchema} */ export default { uiSchema: { + ...titleUI('Date to stop releasing your information'), releaseEndDate: { - 'ui:title': ( -

    - {labelString}{' '} - - (*Required) - -

    - ), - 'ui:widget': 'date', - 'ui:reviewField': ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    {labelString}
    -
    {children}
    -

    - ), + ...currentOrPastDateUI({ + title: 'When should we stop releasing your information?', + description: 'Enter a valid date', + errorMessages: { + required: 'Please provide an end date.', + pattern: 'Please provide a valid end date.', + }, + }), 'ui:validations': [releaseEndDateValidation], - 'ui:errorMessages': { - required: 'Please provide an end date.', - pattern: 'Please provide a valid end date.', - }, }, }, schema: { type: 'object', required: ['releaseEndDate'], properties: { - releaseEndDate: definitions.date, + releaseEndDate: currentOrPastDateSchema, }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/securityQuestion.js b/src/applications/simple-forms/21-0845/pages/securityQuestion.js index a6bbf7244466..0c7d676204e7 100644 --- a/src/applications/simple-forms/21-0845/pages/securityQuestion.js +++ b/src/applications/simple-forms/21-0845/pages/securityQuestion.js @@ -1,5 +1,8 @@ import React from 'react'; - +import { + radioUI, + radioSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { THIRD_PARTY_TYPES, SECURITY_QUESTIONS, @@ -13,77 +16,53 @@ import { /** @type {PageSchema} */ export default { uiSchema: { - securityQuestion: { - 'ui:widget': 'radio', - 'ui:errorMessages': { + securityQuestion: radioUI({ + description: ( + <> +

    + Select a security question. We’ll ask you to enter the answer on the + next screen. You’ll then need to give the answer to your designated + third-party source. +

    +

    + We’ll ask this question each time your designated third-party source + contacts us. +

    + + ), + labels: getLabelsFromConstants(SECURITY_QUESTIONS), + errorMessages: { required: 'Please select a question.', }, - 'ui:options': { - updateSchema: (formData, schema, uiSchema) => { - const { thirdPartyType, personFullName, organizationName } = formData; - let thirdPartyName = 'the third-party'; - let labelString = ''; - - if ( - thirdPartyType === THIRD_PARTY_TYPES.PERSON && - personFullName.first && - personFullName.last - ) { - thirdPartyName = getFullNameString(personFullName); - } else if ( - thirdPartyType === THIRD_PARTY_TYPES.ORGANIZATION && - organizationName - ) { - thirdPartyName = organizationName; - } + labelHeaderLevel: '3', + updateSchema: formData => { + const { thirdPartyType, personFullName, organizationName } = formData; + let thirdPartyName = 'the third-party'; - labelString = `What security question should we ask ${thirdPartyName} to verify their identity?`; + if ( + thirdPartyType === THIRD_PARTY_TYPES.PERSON && + personFullName.first && + personFullName.last + ) { + thirdPartyName = getFullNameString(personFullName); + } else if ( + thirdPartyType === THIRD_PARTY_TYPES.ORGANIZATION && + organizationName + ) { + thirdPartyName = organizationName; + } - // eslint-disable-next-line no-param-reassign - uiSchema['ui:reviewField'] = ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    {labelString}
    -
    {children}
    -

    - ); - - return { - title: ( - <> -

    - {labelString}{' '} - - (*Required) - -

    - - Select a security question. We’ll ask you to enter the answer - on the next screen. You’ll then need to give the answer to - your designated third-party source. -
    -
    - We’ll ask this question each time your designated third-party - source contacts us. -
    - - ), - uiSchema, - }; - }, - labels: getLabelsFromConstants(SECURITY_QUESTIONS), + return { + title: `What security question should we ask ${thirdPartyName} to verify their identity?`, + }; }, - }, + }), }, schema: { type: 'object', required: ['securityQuestion'], properties: { - securityQuestion: { - type: 'string', - enum: getEnumsFromConstants(SECURITY_QUESTIONS), - }, + securityQuestion: radioSchema(getEnumsFromConstants(SECURITY_QUESTIONS)), }, }, }; diff --git a/src/applications/simple-forms/21-0845/pages/thirdPartyType.js b/src/applications/simple-forms/21-0845/pages/thirdPartyType.js index 2087bbac0d17..da4bedc7ed49 100644 --- a/src/applications/simple-forms/21-0845/pages/thirdPartyType.js +++ b/src/applications/simple-forms/21-0845/pages/thirdPartyType.js @@ -1,44 +1,31 @@ -import React from 'react'; - +import { + radioUI, + radioSchema, +} from 'platform/forms-system/src/js/web-component-patterns'; import { THIRD_PARTY_TYPES } from '../definitions/constants'; -const labelString = - 'Do you authorize us to release your information to a specific person or to an organization?'; - /** @type {PageSchema} */ export default { uiSchema: { - thirdPartyType: { - 'ui:title':

    {labelString}

    , - 'ui:widget': 'radio', - 'ui:reviewField': ({ children }) => ( - // prevent ui:title's

    from getting pulled into - // review-field's
    & causing a11y headers-hierarchy errors. -
    -
    {labelString}
    -
    {children}
    -

    - ), - 'ui:errorMessages': { + thirdPartyType: radioUI({ + title: + 'Do you authorize us to release your information to a specific person or to an organization?', + labels: { + [THIRD_PARTY_TYPES.PERSON]: 'A specific person', + [THIRD_PARTY_TYPES.ORGANIZATION]: 'An organization', + }, + errorMessages: { required: 'Please select who you would like us to release information to', }, - 'ui:options': { - labels: { - [THIRD_PARTY_TYPES.PERSON]: 'A specific person', - [THIRD_PARTY_TYPES.ORGANIZATION]: 'An organization', - }, - }, - }, + labelHeaderLevel: '3', + }), }, schema: { type: 'object', required: ['thirdPartyType'], properties: { - thirdPartyType: { - type: 'string', - enum: Object.values(THIRD_PARTY_TYPES), - }, + thirdPartyType: radioSchema(Object.values(THIRD_PARTY_TYPES)), }, }, }; diff --git a/src/applications/simple-forms/21-0845/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/containers/ConfirmationPage.unit.spec.jsx index bbb7fc8f2a4e..2db28b8325f3 100644 --- a/src/applications/simple-forms/21-0845/tests/containers/ConfirmationPage.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/containers/ConfirmationPage.unit.spec.jsx @@ -1,45 +1,101 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; + import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { createStore } from 'redux'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import { cleanup } from '@testing-library/react'; +import { format } from 'date-fns'; +import { createInitialState } from '@department-of-veterans-affairs/platform-forms-system/state/helpers'; import formConfig from '../../config/form'; import ConfirmationPage from '../../containers/ConfirmationPage'; +import testData from '../e2e/fixtures/data/authTypeVet.json'; -const storeBase = { - form: { - formId: formConfig.formId, - submission: { - response: { - confirmationNumber: '123456', +describe('ConfirmationPage', () => { + let wrapper; + let store; + const mockStore = configureMockStore(); + const initialState = { + form: { + ...createInitialState(formConfig), + data: testData, + submission: { + response: { + confirmationNumber: '1234567890', + }, + timestamp: '2022-01-01T00:00:00Z', }, - timestamp: Date.now(), }, - data: { - veteranFullName: { - first: 'Jack', - middle: 'W', - last: 'Witness', - }, - }, - }, -}; + }; + + beforeEach(() => { + store = mockStore(initialState); + wrapper = mount( + + + , + ); + }); + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + cleanup(); + }); -describe('Confirmation page', () => { - const middleware = [thunk]; - const mockStore = configureStore(middleware); + it('passes the correct props to ConfirmationPageView', () => { + const confirmationViewProps = wrapper.find('ConfirmationView').props(); - it('it should show status success and the correct name of person', () => { - const { container, getByText } = render( - - + expect(confirmationViewProps.submitDate).to.equal('2022-01-01T00:00:00Z'); + expect(confirmationViewProps.confirmationNumber).to.equal('1234567890'); + }); + + it('should select form from state when state.form is defined', () => { + const submitDate = new Date(); + const mockInitialState = { + form: { + ...createInitialState(formConfig), + data: testData, + submission: { + timestamp: submitDate, + response: { confirmationNumber: '1234' }, + }, + }, + }; + const mockDefinedState = createStore(() => mockInitialState); + + const definedWrapper = mount( + + , ); - expect(container.querySelector('va-alert')).to.have.attr( - 'status', - 'success', + + expect(definedWrapper.text()).to.include( + format(submitDate, 'MMMM d, yyyy'), ); - getByText(/Jack W Witness/); + // expect(definedWrapper.text()).to.include('1234'); + + definedWrapper.unmount(); + }); + + it('should throw error when state.form is undefined', () => { + const mockEmptyState = {}; + const mockEmptyStore = createStore(() => mockEmptyState); + + let errorWrapper; + + expect(() => { + errorWrapper = mount( + + + , + ); + }).to.throw(); + + if (errorWrapper) { + errorWrapper.unmount(); + } }); }); diff --git a/src/applications/simple-forms/21-0845/tests/pages/authorizerType.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/authorizerType.unit.spec.jsx index 93681b4cba71..3a6fd15104b2 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/authorizerType.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/authorizerType.unit.spec.jsx @@ -3,8 +3,8 @@ import { expect } from 'chai'; import { render } from '@testing-library/react'; import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; import authTypeNonVet from '../e2e/fixtures/data/authTypeNonVet.json'; @@ -17,8 +17,8 @@ const { const pageTitle = 'Who’s submitting this authorization?'; -const expectedNumberOfFields = 2; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -27,7 +27,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/tests/pages/infoScope.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/infoScope.unit.spec.jsx index 200cbe45fb8e..f33081338e57 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/infoScope.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/infoScope.unit.spec.jsx @@ -1,33 +1,18 @@ -import React from 'react'; - -import { expect } from 'chai'; -import { render } from '@testing-library/react'; -import { cloneDeep } from 'lodash'; - -import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; -import authTypeVet from '../e2e/fixtures/data/authTypeVet.json'; -import { THIRD_PARTY_TYPES } from '../../definitions/constants'; -import { getFullNameString } from '../e2e/helpers'; -const mockDataPerson3rdParty = cloneDeep(authTypeVet.data); -const mockDataPerson3rdPartyB = cloneDeep(authTypeVet.data); -mockDataPerson3rdPartyB.personFullName.middle = 'M'; -const mockDataOrganization3rdParty = cloneDeep(authTypeVet.data); const { - defaultDefinitions, schema, uiSchema, } = formConfig.chapters.infoReleaseChapter.pages.informationScopePage; const pageTitle = 'Information scope'; -const expectedNumberOfFields = 2; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -36,69 +21,10 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, expectedNumberOfErrors, pageTitle, ); - -describe(`${pageTitle} - custom-field-label`, () => { - it('renders custom-field-label - person 3rd-party', () => { - mockDataPerson3rdParty.thirdPartyType = THIRD_PARTY_TYPES.PERSON; - - const screen = render( - , - ); - - expect( - screen.container.querySelector('#root_informationScope-label > h3'), - ).to.include.text(getFullNameString(mockDataPerson3rdParty.personFullName)); - }); - - it('renders custom-field-label - person 3rd-party with middle initial', () => { - mockDataPerson3rdPartyB.thirdPartyType = THIRD_PARTY_TYPES.PERSON; - - const screen = render( - , - ); - - expect( - screen.container.querySelector('#root_informationScope-label > h3'), - ).to.include.text( - getFullNameString(mockDataPerson3rdPartyB.personFullName), - ); - }); - - it('renders custom-field-label - organization 3rd-party', () => { - mockDataOrganization3rdParty.thirdPartyType = - THIRD_PARTY_TYPES.ORGANIZATION; - - const screen = render( - , - ); - - expect( - screen.container.querySelector('#root_informationScope-label > h3'), - ).to.include.text(mockDataOrganization3rdParty.organizationName); - }); -}); diff --git a/src/applications/simple-forms/21-0845/tests/pages/organizationName.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/organizationName.unit.spec.jsx index 360b95d97fc9..3fc31f400f9c 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/organizationName.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/organizationName.unit.spec.jsx @@ -1,6 +1,6 @@ import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; @@ -12,7 +12,7 @@ const { const pageTitle = 'Organization’s name'; const expectedNumberOfFields = 1; -testNumberOfFields( +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -21,7 +21,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/tests/pages/organizationReps.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/organizationReps.unit.spec.jsx index ebb374e07ba4..a13796db6ece 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/organizationReps.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/organizationReps.unit.spec.jsx @@ -1,6 +1,6 @@ import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; @@ -12,7 +12,7 @@ const { const pageTitle = 'Organization’s representatives'; const expectedNumberOfFields = 2; -testNumberOfFields( +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -21,7 +21,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/tests/pages/releaseDuration.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/releaseDuration.unit.spec.jsx index 691afc55eb65..fd52c0bb81d4 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/releaseDuration.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/releaseDuration.unit.spec.jsx @@ -1,6 +1,6 @@ import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; @@ -11,8 +11,8 @@ const { const pageTitle = 'Release duration'; -const expectedNumberOfFields = 3; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -21,7 +21,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/tests/pages/releaseEndDate.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/releaseEndDate.unit.spec.jsx index 142a84ff7dcb..0b57778b6b42 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/releaseEndDate.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/releaseEndDate.unit.spec.jsx @@ -1,6 +1,6 @@ import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; @@ -11,8 +11,8 @@ const { const pageTitle = 'Release end date'; -const expectedNumberOfFields = 3; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -21,7 +21,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/tests/pages/securityQuestion.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/securityQuestion.unit.spec.jsx index f6ea9ca3ca4c..5bcc1f9500a4 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/securityQuestion.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/securityQuestion.unit.spec.jsx @@ -1,33 +1,18 @@ -import React from 'react'; - -import { expect } from 'chai'; -import { render } from '@testing-library/react'; -import { cloneDeep } from 'lodash'; - -import { DefinitionTester } from 'platform/testing/unit/schemaform-utils'; import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; -import authTypeVet from '../e2e/fixtures/data/authTypeVet.json'; -import authTypeNonVet from '../e2e/fixtures/data/authTypeNonVet.json'; -import { THIRD_PARTY_TYPES } from '../../definitions/constants'; -import { getFullNameString } from '../e2e/helpers'; - -const mockDataPerson3rdParty = cloneDeep(authTypeVet.data); -const mockDataOrganization3rdParty = cloneDeep(authTypeNonVet.data); const { - defaultDefinitions, schema, uiSchema, } = formConfig.chapters.securityInfoChapter.pages.secQuestionPage; const pageTitle = 'Security question'; -const expectedNumberOfFields = 5; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -36,48 +21,10 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, expectedNumberOfErrors, pageTitle, ); - -describe(`${pageTitle} - custom-field-label`, () => { - it('renders custom-field-label - person 3rd-party', () => { - mockDataPerson3rdParty.thirdPartyType = THIRD_PARTY_TYPES.PERSON; - - const screen = render( - , - ); - - expect( - screen.container.querySelector('#root_securityQuestion-label > h3'), - ).to.include.text(getFullNameString(mockDataPerson3rdParty.personFullName)); - }); - it('renders custom-field-label - organization 3rd-party', () => { - mockDataOrganization3rdParty.thirdPartyType = - THIRD_PARTY_TYPES.ORGANIZATION; - - const screen = render( - , - ); - - expect(screen.container.querySelector('legend h3')).to.include.text( - mockDataOrganization3rdParty.organizationName, - ); - }); -}); diff --git a/src/applications/simple-forms/21-0845/tests/pages/thirdPartyType.unit.spec.jsx b/src/applications/simple-forms/21-0845/tests/pages/thirdPartyType.unit.spec.jsx index f029ecf576f3..8a8a9ad887eb 100644 --- a/src/applications/simple-forms/21-0845/tests/pages/thirdPartyType.unit.spec.jsx +++ b/src/applications/simple-forms/21-0845/tests/pages/thirdPartyType.unit.spec.jsx @@ -1,6 +1,6 @@ import { - testNumberOfErrorsOnSubmit, - testNumberOfFields, + testNumberOfErrorsOnSubmitForWebComponents, + testNumberOfWebComponentFields, } from '../../../shared/tests/pages/pageTests.spec'; import formConfig from '../../config/form'; @@ -11,8 +11,8 @@ const { const pageTitle = 'Third-party type'; -const expectedNumberOfFields = 2; -testNumberOfFields( +const expectedNumberOfFields = 1; +testNumberOfWebComponentFields( formConfig, schema, uiSchema, @@ -21,7 +21,7 @@ testNumberOfFields( ); const expectedNumberOfErrors = 1; -testNumberOfErrorsOnSubmit( +testNumberOfErrorsOnSubmitForWebComponents( formConfig, schema, uiSchema, diff --git a/src/applications/simple-forms/21-0845/validations.js b/src/applications/simple-forms/21-0845/validations.js index 4013ca4eb903..f1331eceb84b 100644 --- a/src/applications/simple-forms/21-0845/validations.js +++ b/src/applications/simple-forms/21-0845/validations.js @@ -1,5 +1,3 @@ -import React from 'react'; - import moment from 'moment'; export const releaseEndDateValidation = (errors = {}, fieldData, formData) => { @@ -12,12 +10,7 @@ export const releaseEndDateValidation = (errors = {}, fieldData, formData) => { if (endDate.isBefore(endOfToday)) { errors.addError( - <> - Please provide a future date{' '} - - [{earliestValidEndDateString} or later] - - , + `Please provide a future date: ${earliestValidEndDateString} or later`, ); } }; diff --git a/src/applications/simple-forms/form-upload/config/constants.js b/src/applications/simple-forms/form-upload/config/constants.js index cc7234a7e8ca..0457f2723c2a 100644 --- a/src/applications/simple-forms/form-upload/config/constants.js +++ b/src/applications/simple-forms/form-upload/config/constants.js @@ -56,19 +56,12 @@ export const MUST_MATCH_ALERT = (variant, onCloseEvent, formData) => { export const UPLOAD_GUIDELINES = Object.freeze( <> -

    Upload your file

    -

    - You’ll need to scan your document onto the device you’re using to submit - this application, such as your computer, tablet, or mobile phone. You can - upload your document from there. +

    Your file

    +

    + Note: After you upload + your file, you’ll need to continue to the next screen to submit it. If you + leave before you submit it, you’ll need to upload it again.

    -
    -

    Guidelines for uploading a file:

    -
      -
    • You can upload a .pdf, .jpeg, or .png file
    • -
    • Your file should be no larger than 25MB
    • -
    -
    , ); diff --git a/src/applications/simple-forms/form-upload/pages/upload.jsx b/src/applications/simple-forms/form-upload/pages/upload.jsx index d1a237e3b869..f5bc06989b66 100644 --- a/src/applications/simple-forms/form-upload/pages/upload.jsx +++ b/src/applications/simple-forms/form-upload/pages/upload.jsx @@ -23,17 +23,6 @@ export const uploadPage = { uiSchema: { 'view:uploadGuidelines': { 'ui:description': UPLOAD_GUIDELINES, - 'ui:options': { - updateUiSchema: formData => { - return { - 'ui:description': warningsPresent(formData) ? ( -

    Your file

    - ) : ( - UPLOAD_GUIDELINES - ), - }; - }, - }, }, uploadedFile: { ...fileInputUI({ @@ -41,6 +30,8 @@ export const uploadPage = { name: 'form-upload-file-input', fileUploadUrl, title, + hint: + 'You can upload a .pdf, .jpeg, or .png file. Your file should be no larger than 25MB', formNumber, required: () => true, // Disallow uploads greater than 25 MB diff --git a/src/applications/simple-forms/form-upload/tests/unit/pages/UploadPage.unit.spec.jsx b/src/applications/simple-forms/form-upload/tests/unit/pages/UploadPage.unit.spec.jsx index 54c323bb015e..969cb59dfcd8 100644 --- a/src/applications/simple-forms/form-upload/tests/unit/pages/UploadPage.unit.spec.jsx +++ b/src/applications/simple-forms/form-upload/tests/unit/pages/UploadPage.unit.spec.jsx @@ -3,7 +3,6 @@ import { render } from '@testing-library/react'; import { expect } from 'chai'; import { UploadPage } from '../../../pages/upload'; import formConfig from '../../../config/form'; -import { UPLOAD_GUIDELINES } from '../../../config/constants'; describe('UploadPage', () => { const { uiSchema } = formConfig().chapters.uploadChapter.pages.uploadPage; @@ -15,31 +14,6 @@ describe('UploadPage', () => { expect(container).to.exist; }); - it('updates the description when there are no warnings', () => { - const result = uiSchema['view:uploadGuidelines'][ - 'ui:options' - ].updateUiSchema({}); - - expect(result).to.deep.equal({ - 'ui:description': UPLOAD_GUIDELINES, - }); - }); - - it('updates the description when there are warnings', () => { - const formData = { - uploadedFile: { - warnings: ['bad news'], - }, - }; - const result = uiSchema['view:uploadGuidelines'][ - 'ui:options' - ].updateUiSchema(formData); - - expect(result).to.deep.equal({ - 'ui:description':

    Your file

    , - }); - }); - it('updates the title when there are no warnings', () => { const result = uiSchema.uploadedFile['ui:options'].updateUiSchema({}); diff --git a/src/applications/find-forms/README.md b/src/applications/static-pages/find-forms/README.md similarity index 100% rename from src/applications/find-forms/README.md rename to src/applications/static-pages/find-forms/README.md diff --git a/src/applications/find-forms/actions/index.js b/src/applications/static-pages/find-forms/actions/index.js similarity index 96% rename from src/applications/find-forms/actions/index.js rename to src/applications/static-pages/find-forms/actions/index.js index 683d83e83e21..aceef1db87c6 100644 --- a/src/applications/find-forms/actions/index.js +++ b/src/applications/static-pages/find-forms/actions/index.js @@ -1,11 +1,11 @@ import URLSearchParams from 'url-search-params'; -import recordEvent from '~/platform/monitoring/record-event'; +import recordEvent from 'platform/monitoring/record-event'; import { fetchFormsApi } from '../api'; -import { MAX_PAGE_LIST_LENGTH } from '../containers/SearchResults'; import { FETCH_FORMS, FETCH_FORMS_FAILURE, FETCH_FORMS_SUCCESS, + MAX_PAGE_LIST_LENGTH, UPDATE_HOW_TO_SORT, UPDATE_PAGINATION, UPDATE_RESULTS, diff --git a/src/applications/find-forms/api/index.js b/src/applications/static-pages/find-forms/api/index.js similarity index 94% rename from src/applications/find-forms/api/index.js rename to src/applications/static-pages/find-forms/api/index.js index 19b799f89ed4..3c5223bc6afd 100644 --- a/src/applications/find-forms/api/index.js +++ b/src/applications/static-pages/find-forms/api/index.js @@ -1,5 +1,5 @@ import appendQuery from 'append-query'; -import { apiRequest } from '~/platform/utilities/api'; +import { apiRequest } from 'platform/utilities/api'; import STUBBED_RESPONSE from '../constants/stub.json'; export const fetchFormsApi = async (query, options = {}) => { diff --git a/src/applications/find-forms/components/FindVaForms.jsx b/src/applications/static-pages/find-forms/components/FindVaForms.jsx similarity index 96% rename from src/applications/find-forms/components/FindVaForms.jsx rename to src/applications/static-pages/find-forms/components/FindVaForms.jsx index b3bed4eab249..a8cafad29b4b 100644 --- a/src/applications/find-forms/components/FindVaForms.jsx +++ b/src/applications/static-pages/find-forms/components/FindVaForms.jsx @@ -2,8 +2,8 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { toggleValues } from '~/platform/site-wide/feature-toggles/selectors'; -import FEATURE_FLAG_NAMES from '~/platform/utilities/feature-toggles/featureFlagNames'; +import { toggleValues } from 'platform/site-wide/feature-toggles/selectors'; +import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; import SearchForm from '../containers/SearchForm'; import SearchResults from '../containers/SearchResults'; diff --git a/src/applications/find-forms/components/FormTitle.jsx b/src/applications/static-pages/find-forms/components/FormTitle.jsx similarity index 100% rename from src/applications/find-forms/components/FormTitle.jsx rename to src/applications/static-pages/find-forms/components/FormTitle.jsx diff --git a/src/applications/find-forms/components/PdfModal.jsx b/src/applications/static-pages/find-forms/components/PdfModal.jsx similarity index 100% rename from src/applications/find-forms/components/PdfModal.jsx rename to src/applications/static-pages/find-forms/components/PdfModal.jsx diff --git a/src/applications/find-forms/components/SearchResult.jsx b/src/applications/static-pages/find-forms/components/SearchResult.jsx similarity index 98% rename from src/applications/find-forms/components/SearchResult.jsx rename to src/applications/static-pages/find-forms/components/SearchResult.jsx index e7d4e57848bd..356d4e96f145 100644 --- a/src/applications/find-forms/components/SearchResult.jsx +++ b/src/applications/static-pages/find-forms/components/SearchResult.jsx @@ -2,8 +2,8 @@ import React from 'react'; import { format, parseISO, isAfter } from 'date-fns'; import PropTypes from 'prop-types'; import { replaceWithStagingDomain } from 'platform/utilities/environment/stagingDomains'; -import environment from '~/platform/utilities/environment'; -import recordEvent from '~/platform/monitoring/record-event'; +import environment from 'platform/utilities/environment'; +import recordEvent from 'platform/monitoring/record-event'; import * as customPropTypes from '../prop-types'; import { FORM_MOMENT_PRESENTATION_DATE_FORMAT } from '../constants'; import FormTitle from './FormTitle'; diff --git a/src/applications/find-forms/constants/index.js b/src/applications/static-pages/find-forms/constants/index.js similarity index 95% rename from src/applications/find-forms/constants/index.js rename to src/applications/static-pages/find-forms/constants/index.js index bfef52530aae..b21aca4ed594 100644 --- a/src/applications/find-forms/constants/index.js +++ b/src/applications/static-pages/find-forms/constants/index.js @@ -15,3 +15,4 @@ export const FORM_MOMENT_CONSTRUCTOR_DATE_FORMAT = 'yyyy-mm-dd'; export const FORM_MOMENT_PRESENTATION_DATE_FORMAT = 'MMMM yyyy'; export const UPDATE_HOW_TO_SORT = 'findVAForms/UPDATE_HOW_TO_SORT'; export const UPDATE_RESULTS = 'findVAForms/UPDATE_RESULTS'; +export const MAX_PAGE_LIST_LENGTH = 10; diff --git a/src/applications/find-forms/constants/stub.json b/src/applications/static-pages/find-forms/constants/stub.json similarity index 100% rename from src/applications/find-forms/constants/stub.json rename to src/applications/static-pages/find-forms/constants/stub.json diff --git a/src/applications/find-forms/containers/SearchForm.jsx b/src/applications/static-pages/find-forms/containers/SearchForm.jsx similarity index 100% rename from src/applications/find-forms/containers/SearchForm.jsx rename to src/applications/static-pages/find-forms/containers/SearchForm.jsx diff --git a/src/applications/find-forms/containers/SearchResults.jsx b/src/applications/static-pages/find-forms/containers/SearchResults.jsx similarity index 97% rename from src/applications/find-forms/containers/SearchResults.jsx rename to src/applications/static-pages/find-forms/containers/SearchResults.jsx index c18ac2096f6d..ac9840569630 100644 --- a/src/applications/find-forms/containers/SearchResults.jsx +++ b/src/applications/static-pages/find-forms/containers/SearchResults.jsx @@ -1,12 +1,12 @@ import React, { useEffect, useRef, useState } from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { VaPagination, VaSelect, } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import { connect } from 'react-redux'; -import recordEvent from '~/platform/monitoring/record-event'; -import { focusElement } from '~/platform/utilities/ui'; +import recordEvent from 'platform/monitoring/record-event'; +import { focusElement } from 'platform/utilities/ui'; import * as customPropTypes from '../prop-types'; import { updateSortByPropertyNameThunk, @@ -14,11 +14,10 @@ import { } from '../actions'; import { deriveDefaultModalState } from '../helpers'; import { getFindFormsAppState } from '../helpers/selectors'; -import { FAF_SORT_OPTIONS } from '../constants'; +import { FAF_SORT_OPTIONS, MAX_PAGE_LIST_LENGTH } from '../constants'; import SearchResult from '../components/SearchResult'; import PdfModal from '../components/PdfModal'; -export const MAX_PAGE_LIST_LENGTH = 10; const usePreviousProps = value => { // This is a mirror to storing and assessing prevProps vs current props const ref = useRef(); // Refs are like a class instance var, in react their values don't change unless that same ref is redefined. @@ -208,7 +207,7 @@ export const SearchResults = ({ <> Showing {startLabel}{lastLabel}{' '} - of {results.length} results for "{' '} + of {results.length} results for " {query}" diff --git a/src/applications/find-forms/createFindVaForms.js b/src/applications/static-pages/find-forms/createFindVaForms.js similarity index 100% rename from src/applications/find-forms/createFindVaForms.js rename to src/applications/static-pages/find-forms/createFindVaForms.js diff --git a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadHandler.js b/src/applications/static-pages/find-forms/download-widget/DownloadHandler.js similarity index 96% rename from src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadHandler.js rename to src/applications/static-pages/find-forms/download-widget/DownloadHandler.js index 10c154989170..cb5a3cb66ed7 100644 --- a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadHandler.js +++ b/src/applications/static-pages/find-forms/download-widget/DownloadHandler.js @@ -4,10 +4,10 @@ import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import DownloadPDFModal from './DownloadPDFModal'; import InvalidFormDownload from './InvalidFormAlert'; -import { sentryLogger } from './index'; +import { sentryLogger } from './sentryLogger'; const removeReactRoot = () => { - const pdf = document.querySelector('.faf-pdf-alert-modal-root'); + const pdf = document.querySelector('.faf-pdf-alert-modal'); pdf.remove(); }; diff --git a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadPDFModal.jsx b/src/applications/static-pages/find-forms/download-widget/DownloadPDFModal.jsx similarity index 93% rename from src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadPDFModal.jsx rename to src/applications/static-pages/find-forms/download-widget/DownloadPDFModal.jsx index bb4c9bf36a3a..0d175a17fb47 100644 --- a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/DownloadPDFModal.jsx +++ b/src/applications/static-pages/find-forms/download-widget/DownloadPDFModal.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import PdfModal from '../../components/PdfModal'; +import PdfModal from '../components/PdfModal'; // DownloadPDFModal is state wrapper + modal for PDF guidance upon PDf being valid const DownloadPDFModal = ({ clickedId, formNumber, removeNode, url }) => { @@ -44,6 +44,7 @@ const DownloadPDFModal = ({ clickedId, formNumber, removeNode, url }) => { }; DownloadPDFModal.propTypes = { + clickedId: PropTypes.string, formNumber: PropTypes.string, removeNode: PropTypes.func, url: PropTypes.string, diff --git a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/InvalidFormAlert.jsx b/src/applications/static-pages/find-forms/download-widget/InvalidFormAlert.jsx similarity index 100% rename from src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/InvalidFormAlert.jsx rename to src/applications/static-pages/find-forms/download-widget/InvalidFormAlert.jsx diff --git a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/index.js b/src/applications/static-pages/find-forms/download-widget/index.js similarity index 82% rename from src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/index.js rename to src/applications/static-pages/find-forms/download-widget/index.js index c0482bca2df1..7ca4a0213f7d 100644 --- a/src/applications/find-forms/widgets/createFindVaFormsPDFDownloadHelper/index.js +++ b/src/applications/static-pages/find-forms/download-widget/index.js @@ -1,17 +1,7 @@ -import * as Sentry from '@sentry/browser'; -import { fetchFormsApi } from '../../api'; +import { fetchFormsApi } from '../api'; +import { sentryLogger } from './sentryLogger'; import DownloadHandler from './DownloadHandler'; -// HOF for reusable situations in Component. -export function sentryLogger(form, formNumber, downloadUrl, message) { - return Sentry.withScope(scope => { - scope.setExtra('form API response', form); - scope.setExtra('form number', formNumber); - scope.setExtra('download link (invalid)', downloadUrl); - Sentry.captureMessage(message); - }); -} - export async function onDownloadLinkClick(event, reduxStore) { // This function purpose is to determine if the PDF is valid on click. // Once it's done, it passes information to DownloadHandler() which determines what to render. diff --git a/src/applications/static-pages/find-forms/download-widget/sentryLogger.js b/src/applications/static-pages/find-forms/download-widget/sentryLogger.js new file mode 100644 index 000000000000..ce7ae2466692 --- /dev/null +++ b/src/applications/static-pages/find-forms/download-widget/sentryLogger.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +// HOF for reusable situations in Component. +export function sentryLogger(form, formNumber, downloadUrl, message) { + return Sentry.withScope(scope => { + scope.setExtra('form API response', form); + scope.setExtra('form number', formNumber); + scope.setExtra('download link (invalid)', downloadUrl); + Sentry.captureMessage(message); + }); +} diff --git a/src/applications/find-forms/find-va-forms-entry.js b/src/applications/static-pages/find-forms/find-va-forms-entry.js similarity index 100% rename from src/applications/find-forms/find-va-forms-entry.js rename to src/applications/static-pages/find-forms/find-va-forms-entry.js diff --git a/src/applications/find-forms/helpers/index.js b/src/applications/static-pages/find-forms/helpers/index.js similarity index 100% rename from src/applications/find-forms/helpers/index.js rename to src/applications/static-pages/find-forms/helpers/index.js diff --git a/src/applications/find-forms/helpers/selectors.js b/src/applications/static-pages/find-forms/helpers/selectors.js similarity index 100% rename from src/applications/find-forms/helpers/selectors.js rename to src/applications/static-pages/find-forms/helpers/selectors.js diff --git a/src/applications/find-forms/prop-types.js b/src/applications/static-pages/find-forms/prop-types.js similarity index 100% rename from src/applications/find-forms/prop-types.js rename to src/applications/static-pages/find-forms/prop-types.js diff --git a/src/applications/find-forms/reducers/findVAFormsReducer.js b/src/applications/static-pages/find-forms/reducers/findVAFormsReducer.js similarity index 100% rename from src/applications/find-forms/reducers/findVAFormsReducer.js rename to src/applications/static-pages/find-forms/reducers/findVAFormsReducer.js diff --git a/src/applications/find-forms/reducers/index.js b/src/applications/static-pages/find-forms/reducers/index.js similarity index 100% rename from src/applications/find-forms/reducers/index.js rename to src/applications/static-pages/find-forms/reducers/index.js diff --git a/src/applications/find-forms/sass/find-va-forms.scss b/src/applications/static-pages/find-forms/sass/find-va-forms.scss similarity index 100% rename from src/applications/find-forms/sass/find-va-forms.scss rename to src/applications/static-pages/find-forms/sass/find-va-forms.scss diff --git a/src/applications/find-forms/tests/actions/index.unit.spec.js b/src/applications/static-pages/find-forms/tests/actions/index.unit.spec.js similarity index 100% rename from src/applications/find-forms/tests/actions/index.unit.spec.js rename to src/applications/static-pages/find-forms/tests/actions/index.unit.spec.js diff --git a/src/applications/find-forms/tests/components/FindVaForms.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/components/FindVaForms.unit.spec.jsx similarity index 100% rename from src/applications/find-forms/tests/components/FindVaForms.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/components/FindVaForms.unit.spec.jsx diff --git a/src/applications/find-forms/tests/components/FormTitle.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/components/FormTitle.unit.spec.jsx similarity index 100% rename from src/applications/find-forms/tests/components/FormTitle.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/components/FormTitle.unit.spec.jsx diff --git a/src/applications/find-forms/tests/components/PdfModal.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/components/PdfModal.unit.spec.jsx similarity index 100% rename from src/applications/find-forms/tests/components/PdfModal.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/components/PdfModal.unit.spec.jsx diff --git a/src/applications/find-forms/tests/components/SearchResult.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/components/SearchResult.unit.spec.jsx similarity index 100% rename from src/applications/find-forms/tests/components/SearchResult.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/components/SearchResult.unit.spec.jsx diff --git a/src/applications/find-forms/tests/containers/SearchForm.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/containers/SearchForm.unit.spec.jsx similarity index 100% rename from src/applications/find-forms/tests/containers/SearchForm.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/containers/SearchForm.unit.spec.jsx diff --git a/src/applications/find-forms/tests/containers/SearchResults.unit.spec.jsx b/src/applications/static-pages/find-forms/tests/containers/SearchResults.unit.spec.jsx similarity index 93% rename from src/applications/find-forms/tests/containers/SearchResults.unit.spec.jsx rename to src/applications/static-pages/find-forms/tests/containers/SearchResults.unit.spec.jsx index f78f3214c8f0..f10810388951 100644 --- a/src/applications/find-forms/tests/containers/SearchResults.unit.spec.jsx +++ b/src/applications/static-pages/find-forms/tests/containers/SearchResults.unit.spec.jsx @@ -3,11 +3,8 @@ import sinon from 'sinon'; import { expect } from 'chai'; import { mount, shallow } from 'enzyme'; import times from 'lodash/times'; -import { INITIAL_SORT_STATE } from '../../constants'; -import { - SearchResults, - MAX_PAGE_LIST_LENGTH, -} from '../../containers/SearchResults'; +import { INITIAL_SORT_STATE, MAX_PAGE_LIST_LENGTH } from '../../constants'; +import { SearchResults } from '../../containers/SearchResults'; describe('Find VA Forms ', () => { const results = times(MAX_PAGE_LIST_LENGTH + 1, () => ({ diff --git a/src/applications/find-forms/tests/cypress/SearchForm.cypress.spec.js b/src/applications/static-pages/find-forms/tests/cypress/SearchForm.cypress.spec.js similarity index 100% rename from src/applications/find-forms/tests/cypress/SearchForm.cypress.spec.js rename to src/applications/static-pages/find-forms/tests/cypress/SearchForm.cypress.spec.js diff --git a/src/applications/find-forms/tests/cypress/find-forms-results.cypress.spec.js b/src/applications/static-pages/find-forms/tests/cypress/find-forms-results.cypress.spec.js similarity index 99% rename from src/applications/find-forms/tests/cypress/find-forms-results.cypress.spec.js rename to src/applications/static-pages/find-forms/tests/cypress/find-forms-results.cypress.spec.js index fb67975e983e..78f224f01bae 100644 --- a/src/applications/find-forms/tests/cypress/find-forms-results.cypress.spec.js +++ b/src/applications/static-pages/find-forms/tests/cypress/find-forms-results.cypress.spec.js @@ -1,6 +1,6 @@ import chunk from 'lodash/chunk'; -import { FAF_SORT_OPTIONS } from '../../constants'; import stub from '../../constants/stub.json'; +import { FAF_SORT_OPTIONS } from '../../constants'; import { SELECTORS as s } from './helpers'; describe('functionality of Find Forms', () => { @@ -20,7 +20,7 @@ describe('functionality of Find Forms', () => { cy.get(s.FINDFORM_INPUT_ROOT) .shadow() - .find('button') + .find(s.FINDFORM_SEARCH) .should('exist') .click(); diff --git a/src/applications/find-forms/tests/cypress/helpers.js b/src/applications/static-pages/find-forms/tests/cypress/helpers.js similarity index 94% rename from src/applications/find-forms/tests/cypress/helpers.js rename to src/applications/static-pages/find-forms/tests/cypress/helpers.js index dcaf52859dab..68325e2b8358 100644 --- a/src/applications/find-forms/tests/cypress/helpers.js +++ b/src/applications/static-pages/find-forms/tests/cypress/helpers.js @@ -3,7 +3,7 @@ export const SELECTORS = { WIDGET: '[data-widget-type="find-va-forms"]', FINDFORM_INPUT_ROOT: 'va-search-input', FINDFORM_INPUT: 'input', - FINDFORM_SEARCH: 'button', + FINDFORM_SEARCH: 'button[type="submit"]', FINDFORM_ERROR_BODY: '[data-e2e-id="find-form-error-body"]', FINDFORM_REQUIRED: '[data-e2e-id="find-form-required"]', FINDFORM_ERROR_MSG: '[data-e2e-id="find-form-error-message"]', diff --git a/src/applications/find-forms/tests/widgets/DownloadHandler.unit.spec.js b/src/applications/static-pages/find-forms/tests/download-widget/DownloadHandler.unit.spec.js similarity index 89% rename from src/applications/find-forms/tests/widgets/DownloadHandler.unit.spec.js rename to src/applications/static-pages/find-forms/tests/download-widget/DownloadHandler.unit.spec.js index d61ea344ddf3..314c8b8d508c 100644 --- a/src/applications/find-forms/tests/widgets/DownloadHandler.unit.spec.js +++ b/src/applications/static-pages/find-forms/tests/download-widget/DownloadHandler.unit.spec.js @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import sinon from 'sinon'; import { expect } from 'chai'; -import DownloadHandler from '../../widgets/createFindVaFormsPDFDownloadHelper/DownloadHandler'; +import DownloadHandler from '../../download-widget/DownloadHandler'; describe('DownloadHandler', () => { const insertSpy = sinon.spy(); diff --git a/src/applications/find-forms/tests/helpers/index.unit.spec.js b/src/applications/static-pages/find-forms/tests/helpers/index.unit.spec.js similarity index 100% rename from src/applications/find-forms/tests/helpers/index.unit.spec.js rename to src/applications/static-pages/find-forms/tests/helpers/index.unit.spec.js diff --git a/src/applications/find-forms/tests/reducers/findVAFormsReducer.unit.spec.js b/src/applications/static-pages/find-forms/tests/reducers/findVAFormsReducer.unit.spec.js similarity index 100% rename from src/applications/find-forms/tests/reducers/findVAFormsReducer.unit.spec.js rename to src/applications/static-pages/find-forms/tests/reducers/findVAFormsReducer.unit.spec.js diff --git a/src/applications/static-pages/static-pages-entry.js b/src/applications/static-pages/static-pages-entry.js index 2cf2c5889995..633e563f8c28 100644 --- a/src/applications/static-pages/static-pages-entry.js +++ b/src/applications/static-pages/static-pages-entry.js @@ -49,8 +49,8 @@ import createAppointARepLandingContent from './representative-appoint'; import createRepresentativeStatus from './representative-status'; import createFindVaForms, { findVaFormsWidgetReducer, -} from '../find-forms/createFindVaForms'; -import createFindVaFormsPDFDownloadHelper from '../find-forms/widgets/createFindVaFormsPDFDownloadHelper'; +} from './find-forms/createFindVaForms'; +import createFindVaFormsPDFDownloadHelper from './find-forms/download-widget'; import createHCAPerformanceWarning from './hca-performance-warning'; import createHomepageEmailSignup from './homepage-email-signup'; import createManageVADebtCTA from './manage-va-debt/createManageVADebtCTA'; diff --git a/src/applications/vaos/.eslintrc b/src/applications/vaos/.eslintrc index 30ea67eb4921..2a37b4afe3ed 100644 --- a/src/applications/vaos/.eslintrc +++ b/src/applications/vaos/.eslintrc @@ -26,6 +26,10 @@ { "target": "./src/applications/vaos/appointment-list", "from": "./src/applications/vaos/express-care" + }, + { + "target": "./src/applications/vaos/referral-appointments", + "from": "./src/applications/vaos/appointment-list" } ] } diff --git a/src/applications/vaos/components/FacilityPhone.jsx b/src/applications/vaos/components/FacilityPhone.jsx index 4da1a596f8a0..1dddf5109147 100644 --- a/src/applications/vaos/components/FacilityPhone.jsx +++ b/src/applications/vaos/components/FacilityPhone.jsx @@ -3,12 +3,12 @@ import PropTypes from 'prop-types'; import { VaTelephone } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; export default function FacilityPhone({ - contact, + contact = '800-698-2411', extension, - className = 'vads-u-font-weight--bold', + className = '', level, icon, - heading = 'Main phone:', + heading = 'Phone: ', }) { if (!contact) { return null; @@ -26,6 +26,13 @@ export default function FacilityPhone({ const isClinic = !!heading.includes('Clinic'); const Heading = `h${level}`; + let dataTestId = 'facility-telephone'; + if (number === '800-698-2411') { + dataTestId = 'main-telephone'; + } else if (isClinic) { + dataTestId = 'clinic-telephone'; + } + return ( <> {!!icon === false && @@ -37,12 +44,13 @@ export default function FacilityPhone({ )} {typeof icon === 'undefined' && - typeof level === 'undefined' && - `${heading} `} + typeof level === 'undefined' && ( + {heading} + )} {!isClinic && ( diff --git a/src/applications/vaos/components/FacilityPhone.unit.spec.js b/src/applications/vaos/components/FacilityPhone.unit.spec.js index b3de89076c1b..d0d34d94bc7b 100644 --- a/src/applications/vaos/components/FacilityPhone.unit.spec.js +++ b/src/applications/vaos/components/FacilityPhone.unit.spec.js @@ -14,7 +14,7 @@ describe('VAOS Component: FacilityPhone', () => { }, ); - expect(screen.getByText(new RegExp(`Main phone:`))).to.exist; + expect(screen.getByText(new RegExp(`Phone:`))).to.exist; const vaPhone = screen.getByTestId('facility-telephone'); expect(vaPhone).to.exist; @@ -32,7 +32,7 @@ describe('VAOS Component: FacilityPhone', () => { }, ); - expect(screen.getByText(new RegExp(`Main phone:`))).to.exist; + expect(screen.getByText(new RegExp(`Phone:`))).to.exist; const vaPhone = screen.getByTestId('facility-telephone'); expect(vaPhone).to.exist; @@ -50,7 +50,7 @@ describe('VAOS Component: FacilityPhone', () => { }, ); - expect(screen.getByText(new RegExp(`Main phone:`))).to.exist; + expect(screen.getByText(new RegExp(`Phone:`))).to.exist; const vaPhone = screen.getByTestId('facility-telephone'); expect(vaPhone).to.exist; diff --git a/src/applications/vaos/components/layouts/CCLayout.jsx b/src/applications/vaos/components/layouts/CCLayout.jsx index 3d1007dcecec..fd05ed6befec 100644 --- a/src/applications/vaos/components/layouts/CCLayout.jsx +++ b/src/applications/vaos/components/layouts/CCLayout.jsx @@ -92,7 +92,7 @@ export default function CCLayout({ data: appointment }) { {!!ccProvider && ( <>
    - + )} diff --git a/src/applications/vaos/components/layouts/ClaimExamLayout.jsx b/src/applications/vaos/components/layouts/ClaimExamLayout.jsx index cfa8f273c57f..01c314c59d40 100644 --- a/src/applications/vaos/components/layouts/ClaimExamLayout.jsx +++ b/src/applications/vaos/components/layouts/ClaimExamLayout.jsx @@ -153,9 +153,7 @@ export default function ClaimExamLayout({ data: appointment }) { <> {facility.name}
    - {facilityPhone && ( - - )} + {facilityPhone && } {!facilityPhone && <>Not available} )} diff --git a/src/applications/vaos/components/layouts/ClaimExamLayout.unit.spec.js b/src/applications/vaos/components/layouts/ClaimExamLayout.unit.spec.js index 13d1116af678..8d6902b995bd 100644 --- a/src/applications/vaos/components/layouts/ClaimExamLayout.unit.spec.js +++ b/src/applications/vaos/components/layouts/ClaimExamLayout.unit.spec.js @@ -343,13 +343,12 @@ describe('VAOS Component: ClaimExamLayout', () => { expect(screen.container.querySelector('va-icon[icon="directions"]')).to.be .ok; - expect(screen.getByText(/Location:/i)); expect(screen.getByText(/CHEYENNE/)); expect(screen.getByText(/Clinic:/i)); expect(screen.getByText(/Clinic 1/i)); - expect(screen.getByText(/Phone:/i)); + expect(screen.getAllByText(/Clinic phone:/i)); expect( screen.container.querySelector('va-telephone[contact="500-500-5000"]'), ).to.be.ok; @@ -478,7 +477,7 @@ describe('VAOS Component: ClaimExamLayout', () => { expect(screen.getByText(/Clinic:/i)); expect(screen.getByText(/Clinic 1/i)); - expect(screen.getByText(/Phone/i)); + expect(screen.getByText(/Clinic phone/i)); expect( screen.container.querySelector('va-telephone[contact="500-500-5000"]'), ).to.be.ok; @@ -583,7 +582,7 @@ describe('VAOS Component: ClaimExamLayout', () => { expect(screen.getByText(/Clinic:/i)); expect(screen.getByText(/Clinic 1/i)); - expect(screen.getByText(/Phone:/i)); + expect(screen.getByText(/Clinic phone:/i)); expect( screen.container.querySelector('va-telephone[contact="500-500-5000"]'), ).to.be.ok; diff --git a/src/applications/vaos/components/layouts/DetailPageLayout.jsx b/src/applications/vaos/components/layouts/DetailPageLayout.jsx index 94f5a318bc31..f175e7cbddd1 100644 --- a/src/applications/vaos/components/layouts/DetailPageLayout.jsx +++ b/src/applications/vaos/components/layouts/DetailPageLayout.jsx @@ -1,9 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - VaButton, - VaTelephone, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { VaButton } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { useDispatch, useSelector } from 'react-redux'; import recordEvent from '@department-of-veterans-affairs/platform-monitoring/record-event'; import { useParams } from 'react-router-dom'; @@ -138,21 +135,17 @@ export function ClinicOrFacilityPhone({ if (clinicPhone) { return ( ); } if (facilityPhone) { - return ; + return ; } - return ( -
    - Phone:   - -
    - ); + // if no clinic or facility phone number, it will default to VA main phone number + return ; } ClinicOrFacilityPhone.propTypes = { clinicPhone: PropTypes.string, diff --git a/src/applications/vaos/components/layouts/VARequestLayout.jsx b/src/applications/vaos/components/layouts/VARequestLayout.jsx index 4193857dd91f..49b51222e74d 100644 --- a/src/applications/vaos/components/layouts/VARequestLayout.jsx +++ b/src/applications/vaos/components/layouts/VARequestLayout.jsx @@ -107,9 +107,7 @@ export default function VARequestLayout({ data: appointment }) {
    - {facilityPhone && ( - - )} + {facilityPhone && } {!facilityPhone && <>Not available}
    { 'href', 'https://maps.google.com?saddr=Current+Location&daddr=2360 East Pershing Boulevard, Cheyenne, WY 82001-5356', ); - expect(screen.baseElement).to.contain.text('Main phone:'); + expect(screen.baseElement).to.contain.text('Phone:'); expect(screen.getByTestId('facility-telephone')).to.exist; expect(screen.getByTestId('add-to-calendar-link')).to.exist; diff --git a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSelect.jsx b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSelect.jsx index 997ff14913f9..5e7fa9856a48 100644 --- a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSelect.jsx +++ b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSelect.jsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import recordEvent from '@department-of-veterans-affairs/platform-monitoring/record-event'; import PropTypes from 'prop-types'; +import React, { useState } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; -import recordEvent from '@department-of-veterans-affairs/platform-monitoring/record-event'; -import { selectProviderSelectionInfo } from '../../redux/selectors'; import { GA_PREFIX } from '../../../utils/constants'; +import { selectProviderSelectionInfo } from '../../redux/selectors'; import RemoveProviderModal from './RemoveProviderModal'; export default function SelectedProvider({ @@ -24,18 +24,16 @@ export default function SelectedProvider({ return (
    {!providerSelected && ( - + uswds + data-testid="choose-a-provider-button" + /> )} {providerSelected && (
    @@ -50,27 +48,26 @@ export default function SelectedProvider({ {formData.address?.city}, {formData.address?.state} {`${formData[sortMethod]} miles`} -
    - - +
    +
    + { + setProvidersListLength(initialProviderDisplayCount); + setShowProvidersList(true); + }} + uswds + /> +
    +
    + setShowRemoveProviderModal(true)} + uswds + /> +
    )} diff --git a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSortVariant.unit.spec.js b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSortVariant.unit.spec.js index 274e4fe14f22..c6625abe1e42 100644 --- a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSortVariant.unit.spec.js +++ b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/ProviderSortVariant.unit.spec.js @@ -96,12 +96,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // When the user clicks the choose a provider button userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // Then providers should be displayed expect(await screen.findByTestId('providersSelect')).to.exist; @@ -141,12 +142,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // When the user selects to sort providers by distance from current location userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); const providersSelect = await screen.findByTestId('providersSelect'); @@ -201,12 +203,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider based on home address userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // When the user selects to sort providers by distance from current location @@ -264,12 +267,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); await waitFor(() => expect(screen.getAllByRole('radio').length).to.equal(5), @@ -360,12 +364,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider based on home address userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // When the user selects to sort providers by distance from a specific facility @@ -440,12 +445,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // When the user tries to choose a provider // Trigger provider list loading userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByTestId('providersSelect')).to.exist; @@ -514,13 +520,14 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // When the user tries to choose a provider // Trigger provider list loading userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByTestId('providersSelect')).to.exist; @@ -553,12 +560,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { CC_PROVIDERS_DATA, true, ); + await screen.findByText(/Continue/i); // When the user clicks the choose a provider button userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // Then they should see an error message expect(await screen.findByText(/We can’t load provider information/i)).to diff --git a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/index.unit.spec.js b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/index.unit.spec.js index 2ea3ede5430d..946b9ca71170 100644 --- a/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/index.unit.spec.js +++ b/src/applications/vaos/new-appointment/components/CommunityCareProviderSelectionPage/index.unit.spec.js @@ -106,7 +106,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Trigger provider list loading userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByText(/Displaying 5 of 16 providers/i)).to.be.ok; expect(screen.getAllByRole('radio').length).to.equal(5); @@ -168,7 +170,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Trigger provider list loading userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByText(/Displaying 5 of 16 providers/i)).to.be.ok; @@ -211,7 +215,7 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Change Provider userEvent.click( - await screen.findByRole('button', { name: /change provider/i }), + await screen.container.querySelector('va-button[text="Change provider"]'), ); userEvent.click(await screen.findByText(/OH, JANICE/i)); userEvent.click( @@ -222,7 +226,7 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Cancel Selection (not clearing of a selected provider) userEvent.click( - await screen.findByRole('button', { name: /change provider/i }), + await screen.container.querySelector('va-button[text="Change provider"]'), ); expect(await screen.findByText(/displaying 5 of 16 providers/i)).to.exist; userEvent.click(await screen.findByRole('button', { name: /cancel/i })); @@ -242,7 +246,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Choose Provider that is buried 2 clicks deep userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); userEvent.click(await screen.findByText(/more providers$/i)); userEvent.click(await screen.findByText(/more providers$/i)); @@ -252,12 +258,18 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { ); // Remove Provider - userEvent.click(await screen.findByRole('button', { name: /remove/i })); + userEvent.click( + await screen.container.querySelector('va-button[text="Remove"]'), + ); expect(await screen.findByTestId('removeProviderModal')).to.exist; userEvent.click( await screen.findByRole('button', { name: /Remove provider/i }), ); - expect(await screen.findByRole('button', { name: /Find a provider/i })); + expect( + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), + ); expect(screen.baseElement).not.to.contain.text( 'AJADI, ADEDIWURAWASHINGTON, DC', ); @@ -311,9 +323,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // When the user tries to choose a provider // Trigger provider list loading userEvent.click( - await screen.findByText(/Find a provider/i, { - selector: 'button', - }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByTestId('providersSelect')).to.exist; @@ -329,12 +341,18 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { expect(screen.baseElement).to.contain.text('OH, JANICEANNANDALE, VA'); // Remove Provider - userEvent.click(await screen.findByRole('button', { name: /remove/i })); + userEvent.click( + await screen.container.querySelector('va-button[text="Remove"]'), + ); expect(await screen.findByTestId('removeProviderModal')).to.exist; userEvent.click( await screen.findByRole('button', { name: /Remove provider/i }), ); - expect(await screen.findByRole('button', { name: /Find a provider/i })); + expect( + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), + ); }); it('should display an error when choose a provider clicked and provider fetch error', async () => { @@ -361,7 +379,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Trigger provider list loading userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect( await screen.findByRole('heading', { @@ -398,7 +418,9 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { // Trigger provider list loading userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); expect(await screen.findByText(/To request this appointment, you can/i)).to .exist; @@ -441,10 +463,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // await waitFor(async () => { expect(await screen.findByText(/Displaying 5 of/i)).to.be.ok; @@ -494,10 +519,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider based on home address userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); userEvent.click(await screen.findByText(/Show 5 more providers$/i)); userEvent.click(await screen.findByText(/Show 5 more providers$/i)); @@ -547,10 +575,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider based on home address userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); // Choose Provider based on current location @@ -610,10 +641,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider based on home address userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); userEvent.click(await screen.findByLabelText(/OH, JANICE/i)); @@ -622,7 +656,7 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { ); // make sure it saves successfully - await screen.findByRole('button', { name: /remove/i }); + await screen.container.querySelector('va-button[text="Remove"]'); // remove the page and change the type of care await cleanup(); @@ -642,10 +676,14 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { screen = renderWithStoreAndRouter(, { store, }); + await screen.findByText(/Continue/i); // the provider should no longer be set - expect(await screen.findByRole('button', { name: /Find a provider/i })).to - .exist; + expect( + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), + ).to.exist; expect(screen.queryByText(/OH, JANICE/i)).to.not.exist; }); @@ -674,10 +712,13 @@ describe('VAOS Page: CommunityCareProviderSelectionPage', () => { store, }, ); + await screen.findByText(/Continue/i); // Choose Provider userEvent.click( - await screen.findByRole('button', { name: /Find a provider/i }), + await screen.container.querySelector( + 'va-button[text="Choose a provider"]', + ), ); await waitFor(() => expect(screen.getAllByRole('radio').length).to.equal(5), diff --git a/src/applications/vaos/new-appointment/components/ContactInfoPage.unit.spec.js b/src/applications/vaos/new-appointment/components/ContactInfoPage.unit.spec.js index f998c93ba07f..ebd725a35a8f 100644 --- a/src/applications/vaos/new-appointment/components/ContactInfoPage.unit.spec.js +++ b/src/applications/vaos/new-appointment/components/ContactInfoPage.unit.spec.js @@ -12,7 +12,7 @@ import { FACILITY_TYPES, FLOW_TYPES } from '../../utils/constants'; describe('VAOS Page: ContactInfoPage', () => { // Flaky test: https://github.com/department-of-veterans-affairs/va.gov-team/issues/82968 - it('should accept email, phone, and preferred time and continue', async () => { + it.skip('should accept email, phone, and preferred time and continue', async () => { const store = createTestStore({ user: { profile: { diff --git a/src/applications/vaos/new-appointment/components/VAFacilityPage/FacilitiesNotShown.jsx b/src/applications/vaos/new-appointment/components/VAFacilityPage/FacilitiesNotShown.jsx index 258f47748556..f9c587d38f5f 100644 --- a/src/applications/vaos/new-appointment/components/VAFacilityPage/FacilitiesNotShown.jsx +++ b/src/applications/vaos/new-appointment/components/VAFacilityPage/FacilitiesNotShown.jsx @@ -78,15 +78,15 @@ export default function FacilitiesNotShown({ contact={ facility.telecom.find(t => t.system === 'phone')?.value } - level={3} + className="vads-u-font-weight--bold" /> ))}
    -

    +

    What you can do -

    +

    Call the facility directly to schedule your appointment,{' '} or diff --git a/src/applications/vaos/new-appointment/components/VAFacilityPage/getEligibilityMessage.js b/src/applications/vaos/new-appointment/components/VAFacilityPage/getEligibilityMessage.js index 5f8ed230f2e0..8e2450834320 100644 --- a/src/applications/vaos/new-appointment/components/VAFacilityPage/getEligibilityMessage.js +++ b/src/applications/vaos/new-appointment/components/VAFacilityPage/getEligibilityMessage.js @@ -72,8 +72,11 @@ export default function getEligibilityMessage({

    Or you can go back and choose a different facility.

    ); - } else if (requestReason === ELIGIBILITY_REASONS.error) { - title = 'You can’t schedule this appointment online right now'; + } else if ( + directReason === ELIGIBILITY_REASONS.error || + requestReason === ELIGIBILITY_REASONS.error + ) { + title = 'You can’t schedule an appointment online right now'; content = 'We’re sorry. There’s a problem with our system. Try again later.'; status = 'error'; diff --git a/src/applications/vaos/referral-appointments/CompleteReferral.jsx b/src/applications/vaos/referral-appointments/CompleteReferral.jsx new file mode 100644 index 000000000000..9f54f98a4b60 --- /dev/null +++ b/src/applications/vaos/referral-appointments/CompleteReferral.jsx @@ -0,0 +1,246 @@ +import PropTypes from 'prop-types'; +import React, { useEffect } from 'react'; +import { + useLocation, + useRouteMatch, + Redirect, + useHistory, +} from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { format, intervalToDuration } from 'date-fns'; +import { formatInTimeZone } from 'date-fns-tz'; + +import ReferralLayout from './components/ReferralLayout'; +import AddToCalendarButton from '../components/AddToCalendarButton'; +import { setFormCurrentPage } from './redux/actions'; +import { useGetProviderById } from './hooks/useGetProviderById'; +import { getReferralSlotKey } from './utils/referrals'; +import { + getTimezoneAbbrByFacilityId, + getTimezoneByFacilityId, +} from '../utils/timezone'; +import { getSlotById } from './utils/provider'; +import FacilityDirectionsLink from '../components/FacilityDirectionsLink'; +import State from '../components/State'; +import { routeToCCPage } from './flow'; +import CCAppointmentCard from './components/CCAppointmentCard'; + +export default function CompleteReferral(props) { + const { currentReferral } = props; + const { search } = useLocation(); + const history = useHistory(); + const dispatch = useDispatch(); + useEffect( + () => { + dispatch(setFormCurrentPage('complete')); + }, + [dispatch], + ); + + const basePath = useRouteMatch(); + const { provider, loading, failed } = useGetProviderById( + currentReferral?.providerId, + ); + + if (failed) { + return ( + + + +

    + We’re sorry. There was a problem with our system. We couldn’t + process this appointment. Call us at 877-470-5947. Monday through + Friday, 8:00 a.m. to 8:00 p.m. ET. +

    +
    +
    +
    + ); + } + + const savedSelectedSlotKey = getReferralSlotKey(currentReferral.UUID); + const savedSlotId = sessionStorage.getItem(savedSelectedSlotKey); + + if (loading && !failed) { + return ( +
    + +
    + ); + } + + if (!savedSlotId || !provider) { + return ; + } + + const params = new URLSearchParams(search); + const comfirmMessage = params.get('confirmMsg') === 'true'; + + const savedSlot = getSlotById(provider.slots, savedSlotId); + + const savedDate = new Date(savedSlot.start); + const timeZoneAbr = getTimezoneAbbrByFacilityId( + currentReferral.ReferringFacilityInfo.FacilityCode, + ); + const timeZone = getTimezoneByFacilityId( + currentReferral.ReferringFacilityInfo.FacilityCode, + ); + + const appointmentDuration = intervalToDuration({ + start: new Date(savedSlot.start), + end: new Date(savedSlot.end), + }); + + return ( + + +
    +

    + {comfirmMessage + ? 'We’ve scheduled and confirmed your appointment.' + : 'You already scheduled your first appointment for this referral'} +

    + {comfirmMessage && ( + <> + { + e.preventDefault(); + routeToCCPage(history, 'appointments'); + }} + text="Review your appointments" + /> + + )} + {!comfirmMessage && ( +

    + Contact your referring VA if you have questions. +

    + )} +
    +
    When
    +

    + {`${format(savedDate, 'EEEE, MMMM dd, yyyy')}`} +
    + {`${formatInTimeZone( + savedDate, + timeZone, + 'h:mm aaaa', + )} ${timeZoneAbr}`} +

    + +
    What
    +

    + {currentReferral.CategoryOfCare} +

    +
    Who
    +

    + {provider.providerName} +

    +
    Where to attend
    +

    + {provider.orgName} +

    +

    + {provider.orgAddress.street1} + {provider.orgAddress.street2 && ( + <> +
    + {provider.orgAddress.street2} + + )} +
    + {provider.orgAddress.city},{' '} + {provider.orgAddress.zip} +

    +
    + +
    +

    + Phone:{' '} + +

    +
    + Details you shared with your referring VA provider +
    +

    + {`Reason: ${currentReferral.reason}`} +

    +

    + {`Other details: ${currentReferral.details}`} +

    +
    Need to make changes?
    +

    + Contact this referring VA facility if you need to reschedule or cancel + your appointment and notify the VA of any changes. +

    +

    + {`Faciliy: ${provider.orgName}`} +

    +

    + Phone:{' '} + {' '} + +

    +
    + window.print()} + text="Print" + data-testid="print-button" + uswds + secondary + /> +
    +
    +
    + ); +} +CompleteReferral.propTypes = { + currentReferral: PropTypes.object, +}; diff --git a/src/applications/vaos/referral-appointments/CompleteReferral.unit.spec.js b/src/applications/vaos/referral-appointments/CompleteReferral.unit.spec.js new file mode 100644 index 000000000000..d362da292ceb --- /dev/null +++ b/src/applications/vaos/referral-appointments/CompleteReferral.unit.spec.js @@ -0,0 +1,174 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { fireEvent } from '@testing-library/react'; +import CompleteReferral from './CompleteReferral'; +import * as flow from './flow'; + +import { + createTestStore, + renderWithStoreAndRouter, +} from '../tests/mocks/setup'; +import { createReferral, getReferralSlotKey } from './utils/referrals'; +import { FETCH_STATUS } from '../utils/constants'; +import { createProviderDetails } from './utils/provider'; +import * as getProviderByIdModule from '../services/referral'; + +describe('CompleteReferral', () => { + const providerDetails = createProviderDetails(1, '111'); + providerDetails.slots[0].start = '2024-11-29T16:00:00.000Z'; + providerDetails.slots[0].end = '2024-11-29T17:00:00.000Z'; + const sandbox = sinon.createSandbox(); + + const initialState = { + referral: { + selectedProvider: providerDetails, + providerFetchStatus: FETCH_STATUS.succeeded, + selectedSlot: '0', + currentPage: 'complete', + }, + }; + + beforeEach(() => { + global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); + sandbox + .stub(getProviderByIdModule, 'getProviderById') + .resolves(providerDetails); + const selectedSlotKey = getReferralSlotKey('UUID'); + sessionStorage.setItem(selectedSlotKey, 0); + }); + afterEach(() => { + sandbox.restore(); + sessionStorage.clear(); + global.XMLHttpRequest.restore(); + }); + + describe('when confirmMsg is true', () => { + it('should render review and schedule links', () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + path: '/complete/UUID?confirmMsg=true', + }, + ); + + expect(getByTestId('review-appointments-link')).to.exist; + }); + it('should call routeToCCPage when "Review your appointments" link is clicked', () => { + const routeToCCPage = sandbox.stub(flow, 'routeToCCPage'); + + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + path: '/complete/UUID?confirmMsg=true', + }, + ); + + fireEvent.click(getByTestId('review-appointments-link')); + expect(routeToCCPage.calledOnce).to.be.true; + }); + }); + + it('should render error alert when provider loading fails', () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore({ + ...initialState, + referral: { + ...initialState.referral, + selectedProvider: {}, + providerFetchStatus: FETCH_STATUS.failed, + }, + }), + }, + ); + + expect(getByTestId('error-alert')).to.exist; + }); + + it('should redirect to home if no saved slot key or provider', () => { + const selectedSlotKey = getReferralSlotKey('UUID'); + sessionStorage.removeItem(selectedSlotKey); + + const { history } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + path: '/complete/UUID', + }, + ); + + sandbox.assert.notCalled(getProviderByIdModule.getProviderById); + expect(history.location.pathname).to.equal('/'); + }); + + it('should render appointment details correctly when provider and slot are available', () => { + providerDetails.slots[0].start = '2024-11-29T16:00:00.000Z'; + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore({ + ...initialState, + referral: { + ...initialState.referral, + selectedProvider: providerDetails, + }, + }), + }, + ); + + expect(getByTestId('referral-content')).to.exist; + expect(getByTestId('contact-va-for-questions')).to.exist; + expect(getByTestId('add-to-calendar-button')).to.exist; + expect(getByTestId('appointment-date-time')).to.have.text( + 'Friday, November 29, 202411:00 a.m. ET', + ); + expect(getByTestId('provider-name')).to.exist; + expect(getByTestId('provider-org-name')).to.exist; + expect(getByTestId('provider-address')).to.have.text( + '111 Lori Ln.New York, New York 10016', + ); + expect(getByTestId('provider-facility-org-name')).to.exist; + expect(getByTestId('changes-copy')).to.exist; + expect(getByTestId('referral-other-details')).to.exist; + expect(getByTestId('referral-reason')).to.exist; + expect(getByTestId('referring-facility-telephone')).to.exist; + expect(getByTestId('referring-facility-telephone-tty')).to.exist; + expect(getByTestId('print-button')).to.exist; + }); + + it('should redirect to appointments if no slot selected', async () => { + const selectedSlotKey = getReferralSlotKey('UUID'); + sessionStorage.removeItem(selectedSlotKey); + + const noSelectState = { + ...initialState, + ...{ referral: { ...initialState.referral, selectedSlot: '' } }, + }; + const { history } = renderWithStoreAndRouter( + , + { + store: createTestStore(noSelectState), + path: '/complete/UUID', + }, + ); + sandbox.assert.notCalled(getProviderByIdModule.getProviderById); + expect(history.location.pathname).to.equal('/'); + }); +}); diff --git a/src/applications/vaos/referral-appointments/ReviewAndConfirm.jsx b/src/applications/vaos/referral-appointments/ReviewAndConfirm.jsx index a5df14f6b684..60e19683818f 100644 --- a/src/applications/vaos/referral-appointments/ReviewAndConfirm.jsx +++ b/src/applications/vaos/referral-appointments/ReviewAndConfirm.jsx @@ -12,7 +12,11 @@ import { setSelectedSlot, } from './redux/actions'; import ReferralLayout from './components/ReferralLayout'; -import { routeToPreviousReferralPage, routeToCCPage } from './flow'; +import { + routeToPreviousReferralPage, + routeToCCPage, + routeToNextReferralPage, +} from './flow'; import { getReferralSlotKey } from './utils/referrals'; import { getSlotById } from './utils/provider'; import { @@ -207,6 +211,15 @@ const ReviewAndConfirm = props => { label="Continue" text="Continue" uswds + onClick={e => { + e.preventDefault(); + // TODO: submit the referral here and poll for status + routeToNextReferralPage( + history, + 'reviewAndConfirm', + currentReferral.UUID, + ); + }} />
    diff --git a/src/applications/vaos/referral-appointments/components/CCAppointmentCard.jsx b/src/applications/vaos/referral-appointments/components/CCAppointmentCard.jsx new file mode 100644 index 000000000000..59547948d080 --- /dev/null +++ b/src/applications/vaos/referral-appointments/components/CCAppointmentCard.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import AppointmentCardIcon from '../../components/AppointmentCard/AppointmentCardIcon'; + +const ccAppointmentStub = { + vaos: { + isCommunityCare: true, + }, +}; +export default function CCAppointmentCard({ children }) { + return ( +
    + +

    + Comunity care appointment +

    + {children} +
    + ); +} + +CCAppointmentCard.propTypes = { + children: PropTypes.node, +}; diff --git a/src/applications/vaos/referral-appointments/components/CCAppointmentCard.unit.spec.js b/src/applications/vaos/referral-appointments/components/CCAppointmentCard.unit.spec.js new file mode 100644 index 000000000000..f047e1a5b02c --- /dev/null +++ b/src/applications/vaos/referral-appointments/components/CCAppointmentCard.unit.spec.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { expect } from 'chai'; + +import { render } from '@testing-library/react'; + +import CCAppointmentCard from './CCAppointmentCard'; + +describe('VAOS Component: CCAppointmentCard', () => { + it('renders with defaults', () => { + const { getByTestId } = render(); + expect(getByTestId('cc-appointment-card')).to.exist; + expect(getByTestId('appointment-icon')).to.exist; + expect(getByTestId('cc-appointment-card-header')).to.exist; + }); + + it('renders children correctly', () => { + const { getByTestId } = render( + +

    Test Child

    +
    , + ); + expect(getByTestId('test-child')).to.contain.text('Test Child'); + }); +}); diff --git a/src/applications/vaos/referral-appointments/components/ReferralLayout.jsx b/src/applications/vaos/referral-appointments/components/ReferralLayout.jsx index 17a1a55af55c..0bf7761bcdd4 100644 --- a/src/applications/vaos/referral-appointments/components/ReferralLayout.jsx +++ b/src/applications/vaos/referral-appointments/components/ReferralLayout.jsx @@ -13,14 +13,23 @@ import { selectCurrentPage } from '../redux/selectors'; import { routeToPreviousReferralPage } from '../flow'; import ErrorAlert from './ErrorAlert'; +const getBackLinkText = currentPage => { + switch (currentPage) { + case 'referralsAndRequests': + case 'scheduleReferral': + return 'Appointments'; + case 'complete': + return 'Back to Appointments'; + default: + return 'Back'; + } +}; + function BreadCrumbNav() { const history = useHistory(); const currentPage = useSelector(selectCurrentPage); - const text = - currentPage === 'referralsAndRequests' || currentPage === 'scheduleReferral' - ? 'Appointments' - : 'Back'; + const text = getBackLinkText(currentPage); const { search } = useLocation(); const params = new URLSearchParams(search); const id = params.get('id'); diff --git a/src/applications/vaos/referral-appointments/flow.js b/src/applications/vaos/referral-appointments/flow.js index 41bd74110d98..1eb1c58d1fe6 100644 --- a/src/applications/vaos/referral-appointments/flow.js +++ b/src/applications/vaos/referral-appointments/flow.js @@ -4,7 +4,7 @@ import { startReferralTimer } from './utils/timer'; * Function to get referral page flow. * * @export - * @param {boolean} state - New COVID appointment state + * @param {string} referralId - The referral unique identifier * @returns {object} Referral appointment workflow object */ export default function getPageFlow(referralId) { @@ -40,7 +40,7 @@ export default function getPageFlow(referralId) { previous: 'scheduleAppointment', }, complete: { - url: 'appointments/[ID]?confirmMsg=true', + url: `/schedule-referral/complete?id=${referralId}&confirmMsg=true`, label: 'Your appointment is scheduled', next: '', previous: 'reviewAndConfirm', diff --git a/src/applications/vaos/referral-appointments/hooks/useGetProviderById.jsx b/src/applications/vaos/referral-appointments/hooks/useGetProviderById.jsx new file mode 100644 index 000000000000..f68a9f1632a0 --- /dev/null +++ b/src/applications/vaos/referral-appointments/hooks/useGetProviderById.jsx @@ -0,0 +1,53 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; + +import { fetchProviderDetails, setInitReferralFlow } from '../redux/actions'; +import { getProviderInfo } from '../redux/selectors'; +import { FETCH_STATUS } from '../../utils/constants'; + +function useGetProviderById(providerId) { + const dispatch = useDispatch(); + const { provider, providerFetchStatus } = useSelector( + state => getProviderInfo(state), + shallowEqual, + ); + const selectedProviderId = provider?.id; + + const loading = + providerFetchStatus === FETCH_STATUS.loading || + providerFetchStatus === FETCH_STATUS.notStarted; + const failed = providerFetchStatus === FETCH_STATUS.failed; + + useEffect( + () => { + const isSameProvider = providerId === selectedProviderId; + if (!providerId || isSameProvider || failed) { + return; + } + + dispatch(setInitReferralFlow()); + }, + [dispatch, failed, providerId, selectedProviderId], + ); + + useEffect( + () => { + if (!providerId) { + return; + } + + if (providerFetchStatus === FETCH_STATUS.notStarted) { + dispatch(fetchProviderDetails(providerId)); + } + }, + [dispatch, providerFetchStatus, providerId], + ); + + return { + provider: providerId === provider?.id ? provider : undefined, + loading, + failed, + }; +} + +export { useGetProviderById }; diff --git a/src/applications/vaos/referral-appointments/index.jsx b/src/applications/vaos/referral-appointments/index.jsx index b2f40ae3a21d..1900778ad947 100644 --- a/src/applications/vaos/referral-appointments/index.jsx +++ b/src/applications/vaos/referral-appointments/index.jsx @@ -8,7 +8,6 @@ import { } from 'react-router-dom'; import ScheduleReferral from './ScheduleReferral'; import ReviewAndConfirm from './ReviewAndConfirm'; -import ConfirmReferral from './ConfirmReferral'; import ChooseDateAndTime from './ChooseDateAndTime'; import useManualScrollRestoration from '../hooks/useManualScrollRestoration'; import { useGetReferralById } from './hooks/useGetReferralById'; @@ -16,6 +15,7 @@ import { useIsInCCPilot } from './hooks/useIsInCCPilot'; import { FETCH_STATUS } from '../utils/constants'; import FormLayout from '../new-appointment/components/FormLayout'; import { scrollAndFocus } from '../utils/scrollAndFocus'; +import CompleteReferral from './CompleteReferral'; import ReferralLayout from './components/ReferralLayout'; export default function ReferralAppointments() { @@ -23,7 +23,6 @@ export default function ReferralAppointments() { const basePath = useRouteMatch(); const { isInCCPilot } = useIsInCCPilot(); const { search } = useLocation(); - const params = new URLSearchParams(search); const id = params.get('id'); const { @@ -75,9 +74,8 @@ export default function ReferralAppointments() { - {/* TODO: remove this mock page when referral complete page is built */} - - + + diff --git a/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/TestComponent.jsx b/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/TestComponent.jsx new file mode 100644 index 000000000000..c276aa27dfe0 --- /dev/null +++ b/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/TestComponent.jsx @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useGetProviderById } from '../../../hooks/useGetProviderById'; + +export default function TestComponent({ providerId }) { + const { provider, loading, failed } = useGetProviderById(providerId); + return ( +
    +

    Test component

    +

    {`name: ${provider?.providerName}`}

    +

    {`id: ${provider?.id}`}

    +

    {`loading: ${loading}`}

    +

    {`fail status: ${failed}`}

    +
    + ); +} + +TestComponent.propTypes = { + providerId: PropTypes.string, +}; diff --git a/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/useGetProviderByReferral.unit.spec.js b/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/useGetProviderByReferral.unit.spec.js new file mode 100644 index 000000000000..ffbab0613947 --- /dev/null +++ b/src/applications/vaos/referral-appointments/tests/hooks/useGetProviderByReferral/useGetProviderByReferral.unit.spec.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { waitFor } from '@testing-library/dom'; + +import { + renderWithStoreAndRouter, + createTestStore, +} from '../../../../tests/mocks/setup'; +import { createProviderDetails } from '../../../utils/provider'; +import { FETCH_STATUS } from '../../../../utils/constants'; +import * as getProviderByIdModule from '../../../../services/referral'; + +import TestComponent from './TestComponent'; + +describe('Community Care Referrals', () => { + describe('useGetProviderByReferral hook', () => { + const sandbox = sinon.createSandbox(); + const providerDetails = createProviderDetails(1, '111'); + const initialState = { + referral: { + selectedProvider: providerDetails, + providerFetchStatus: FETCH_STATUS.succeeded, + }, + }; + beforeEach(() => { + global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); + sandbox + .stub(getProviderByIdModule, 'getProviderById') + .resolves(providerDetails); + }); + afterEach(() => { + sandbox.restore(); + global.XMLHttpRequest.restore(); + }); + it('should not return or fetch provider if no providerId is passed', async () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + }, + ); + expect(getByTestId('test-component')).to.exist; + expect(getByTestId('loading')).to.contain.text('loading: false'); + expect(getByTestId('fail-status')).contains.text('fail status: false'); + expect(getByTestId('provider-name')).to.be.empty; + expect(getByTestId('provider-id')).to.be.empty; + sandbox.assert.notCalled(getProviderByIdModule.getProviderById); + }); + it('should not fetch provider if it exists in redux', async () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + }, + ); + expect(getByTestId('test-component')).to.exist; + await waitFor(() => { + expect(getByTestId('loading')).to.contain.text('loading: false'); + }); + expect(getByTestId('fail-status')).contains.text('fail status: false'); + expect(getByTestId('provider-name')).contains.text( + providerDetails.providerName, + ); + expect(getByTestId('provider-id')).to.contain.text(providerDetails.id); + sandbox.assert.notCalled(getProviderByIdModule.getProviderById); + }); + it('should fetch provider if not in redux', async () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore({ + ...initialState, + referral: { + selectedProvider: {}, + providerFetchStatus: FETCH_STATUS.notStarted, + }, + }), + }, + ); + await waitFor(() => { + expect(getByTestId('loading')).to.contain.text('loading: true'); + }); + sandbox.assert.calledOnce(getProviderByIdModule.getProviderById); + }); + it('should fetch new provider if provider in redux is not the one requested', async () => { + const otherProvider = createProviderDetails(1, '222'); + sandbox.restore(); + sandbox + .stub(getProviderByIdModule, 'getProviderById') + .resolves(otherProvider); + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore(initialState), + }, + ); + await waitFor(() => { + expect(getByTestId('loading')).to.contain.text('loading: true'); + }); + sandbox.assert.calledOnce(getProviderByIdModule.getProviderById); + }); + it('should show the error message if fetch fails', async () => { + const { getByTestId } = renderWithStoreAndRouter( + , + { + store: createTestStore({ + ...initialState, + referral: { + selectedProvider: {}, + providerFetchStatus: FETCH_STATUS.failed, + }, + }), + }, + ); + expect(getByTestId('fail-status')).to.contain.text('fail status: true'); + }); + }); +}); diff --git a/src/applications/vaos/referral-appointments/utils/provider.js b/src/applications/vaos/referral-appointments/utils/provider.js index b7db5b61e02c..fc40344d98a9 100644 --- a/src/applications/vaos/referral-appointments/utils/provider.js +++ b/src/applications/vaos/referral-appointments/utils/provider.js @@ -4,6 +4,7 @@ const dateFnsTz = require('date-fns-tz'); const providers = { '0': { + id: '0', providerName: 'Dr. Perpetually Unavailable', typeOfCare: 'Physical Therapy', orgName: 'Ethereal Adjunct of Deferred Care', @@ -21,6 +22,7 @@ const providers = { location: 'Hypothetical Adjunct Node, Sublime Care Complex', }, '111': { + id: '111', providerName: 'Dr. Bones', typeOfCare: 'Physical Therapy', orgName: 'Stronger Bone Technologies', @@ -38,6 +40,7 @@ const providers = { location: 'Stronger bone technologies bldg 2', }, '222': { + id: '222', providerName: 'Dr. Peetee', typeOfCare: 'Physical Therapy', orgName: 'Physical Therapy Solutions', @@ -55,6 +58,7 @@ const providers = { location: 'Physical Therapy Solutions World Headquarters', }, '333': { + id: '333', providerName: 'Dr. Smith', typeOfCare: 'Mental Health', orgName: 'Smith Mental Health Clinic', diff --git a/src/applications/vaos/referral-appointments/utils/referrals.js b/src/applications/vaos/referral-appointments/utils/referrals.js index 814c30ecb62a..2aa4b7a6c23d 100644 --- a/src/applications/vaos/referral-appointments/utils/referrals.js +++ b/src/applications/vaos/referral-appointments/utils/referrals.js @@ -62,6 +62,7 @@ const createReferral = ( providerLocation: provider.providerLocation, providerId, details: 'Back pain', + reason: 'Referral', }; }; diff --git a/src/applications/vaos/tests/e2e/page-objects/CommunityCarePreferencesPageObject.js b/src/applications/vaos/tests/e2e/page-objects/CommunityCarePreferencesPageObject.js index f32cd1b1b5b2..5a2c1b317e08 100644 --- a/src/applications/vaos/tests/e2e/page-objects/CommunityCarePreferencesPageObject.js +++ b/src/applications/vaos/tests/e2e/page-objects/CommunityCarePreferencesPageObject.js @@ -32,7 +32,10 @@ export class CommunityCarePreferencesPageObject extends PageObject { } expandAccordian() { - cy.findByText(/Find a provider/).click(); + cy.findByTestId('choose-a-provider-button') + .first() + .should('exist') + .click(); cy.axeCheckBestPractice(); return this; diff --git a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/multiple-facilities-dead-ends.cypress.spec.js b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/multiple-facilities-dead-ends.cypress.spec.js index 1f3a33e5e465..72cf89c7d689 100644 --- a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/multiple-facilities-dead-ends.cypress.spec.js +++ b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/multiple-facilities-dead-ends.cypress.spec.js @@ -3,6 +3,7 @@ import MockUser from '../../fixtures/MockUser'; import { mockAppointmentsGetApi, mockClinicsApi, + mockEligibilityApi, mockEligibilityCCApi, mockEligibilityDirectApi, mockEligibilityRequestApi, @@ -59,6 +60,49 @@ describe('VAOS direct schedule flow - Multiple facilities dead ends', () => { // Assert cy.axeCheckBestPractice(); }); + + it('should display error message - direct schedule', () => { + // Arrange + const mockUser = new MockUser({ addressLine1: '123 Main St.' }); + mockEligibilityCCApi({ cceType, isEligible: false }); + + mockFacilitiesApi({ + response: MockFacilityResponse.createResponses({ + facilityIds: ['983', '984'], + }), + }); + mockSchedulingConfigurationApi({ + facilityIds: ['983', '984'], + typeOfCareId: 'primaryCare', + isDirect: true, + isRequest: false, + }); + mockEligibilityApi({ responseCode: 502 }); + mockClinicsApi({ + locationId: '983', + response: [], + }); + // Act + cy.login(mockUser); + + AppointmentListPageObject.visit().scheduleAppointment(); + + TypeOfCarePageObject.assertUrl() + .assertAddressAlert({ exist: false }) + .selectTypeOfCare(/Primary care/i) + .clickNextButton(); + + VAFacilityPageObject.assertUrl() + .selectLocation(/Facility 983/i) + .clickNextButton(); + + cy.get('[data-testid="eligibilityModal"]') + .contains('We’re sorry. There’s a problem with our system') + .should('exist'); + + // Assert + cy.axeCheckBestPractice(); + }); }); describe('And no clinics configured', () => { diff --git a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-clinic-dead-ends.cypress.spec.js b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-clinic-dead-ends.cypress.spec.js index c31c1a95da15..bb63df33e479 100644 --- a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-clinic-dead-ends.cypress.spec.js +++ b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-clinic-dead-ends.cypress.spec.js @@ -160,7 +160,7 @@ describe('VAOS direct schedule flow - Single clinic dead ends', () => { VAFacilityPageObject.assertUrl() .assertErrorAlert({ - text: /You can.t schedule this appointment online/i, + text: /You can.t schedule an appointment online/i, }) .assertText({ text: /We.re sorry. There.s a problem with our system. Try again later./i, diff --git a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-facility-dead-ends.cypress.spec.js b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-facility-dead-ends.cypress.spec.js index 5d81b4a44755..dcde5602d2bb 100644 --- a/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-facility-dead-ends.cypress.spec.js +++ b/src/applications/vaos/tests/e2e/workflows/direct-schedule-workflow/single-facility-dead-ends.cypress.spec.js @@ -34,6 +34,46 @@ describe('VAOS direct schedule flow - Single facility dead ends', () => { }); describe('When one facility supports online scheduling', () => { + describe('And 502 "Bad gateway" error', () => { + it('should display error', () => { + // Arrange + const mockUser = new MockUser({ addressLine1: '123 Main St.' }); + mockEligibilityCCApi({ cceType, isEligible: false }); + + mockFacilitiesApi({ + response: MockFacilityResponse.createResponses({ + facilityIds: ['983'], + }), + }); + mockSchedulingConfigurationApi({ + facilityIds: ['983'], + typeOfCareId: 'primaryCare', + isDirect: true, + isRequest: false, + }); + mockEligibilityApi({ responseCode: 502 }); + mockClinicsApi({ + locationId: '983', + response: [], + }); + // Act + cy.login(mockUser); + + AppointmentListPageObject.visit().scheduleAppointment(); + + TypeOfCarePageObject.assertUrl() + .assertAddressAlert({ exist: false }) + .selectTypeOfCare(/Primary care/i) + .clickNextButton(); + + VAFacilityPageObject.assertUrl().assertErrorAlert({ + text: /You can.t schedule an appointment online right now/i, + }); + + // Assert + cy.axeCheckBestPractice(); + }); + }); describe('And no available clinics and no requests', () => { it('should display warning', () => { // Arrange @@ -80,7 +120,7 @@ describe('VAOS direct schedule flow - Single facility dead ends', () => { }); }); - describe('And no past apptointments and request only', () => { + describe('And no past appointments and request only', () => { it('should display warning', () => { // Arrange const mockUser = new MockUser({ addressLine1: '123 Main St.' }); @@ -122,7 +162,7 @@ describe('VAOS direct schedule flow - Single facility dead ends', () => { }); }); - describe('And no past apptointments and direct only', () => { + describe('And no past appointments and direct only', () => { it('should display warning', () => { // Arrange const mockUser = new MockUser({ addressLine1: '123 Main St.' }); @@ -179,7 +219,7 @@ describe('VAOS direct schedule flow - Single facility dead ends', () => { }); }); - describe('And no past apptointments and request only and limit reached', () => { + describe('And no past appointments and request only and limit reached', () => { it('should display warning', () => { // Arrange const mockUser = new MockUser({ addressLine1: '123 Main St.' }); diff --git a/src/applications/verify-your-enrollment/components/PeriodsToVerify.jsx b/src/applications/verify-your-enrollment/components/PeriodsToVerify.jsx index 235d3e7739ee..c6dae7fa4e04 100644 --- a/src/applications/verify-your-enrollment/components/PeriodsToVerify.jsx +++ b/src/applications/verify-your-enrollment/components/PeriodsToVerify.jsx @@ -7,6 +7,7 @@ import VerifiedSuccessStatement from './VerifiedSuccessStatement'; import { getPeriodsToVerify, getPeriodsToVerifyDGIB, + isVerificationDateValid, isVerificationEndDateValid, } from '../helpers'; import Alert from './Alert'; @@ -25,8 +26,10 @@ const PeriodsToVerify = ({ const idRef = useRef(); const showEnrollmentVerifications = enrollmentVerifications?.some( verification => - !verification.verificationMethod && - isVerificationEndDateValid(verification.verificationEndDate), + !isVerificationDateValid( + verification.verificationEndDate, + enrollmentVerifications, + ) && isVerificationEndDateValid(verification.verificationEndDate), ); useEffect( () => { @@ -128,7 +131,6 @@ const PeriodsToVerify = ({ enrollmentData?.verifications.length !== 0) || (!showEnrollmentVerifications && claimantId && - error && enrollmentVerifications?.length !== 0)) && !justVerified && (
    diff --git a/src/applications/verify-your-enrollment/components/PreviousEnrollmentVerifications.jsx b/src/applications/verify-your-enrollment/components/PreviousEnrollmentVerifications.jsx index 17bb5e4dc120..33cff27ff892 100644 --- a/src/applications/verify-your-enrollment/components/PreviousEnrollmentVerifications.jsx +++ b/src/applications/verify-your-enrollment/components/PreviousEnrollmentVerifications.jsx @@ -100,15 +100,18 @@ const PreviousEnrollmentVerifications = ({ ? pastAndCurrentAwards : combinedEnrollmentsObj; Object.values(enrollmentsToProcess).forEach(month => { + const verificationEndDateArr = month.filter(m => + isVerificationEndDateValid(m.verificationEndDate), + ); if (month.length > 1) { const tempGroupEnrollment = !claimantId ? getGroupedPreviousEnrollments(month) - : getGroupedPreviousEnrollmentsDGIB(month); + : getGroupedPreviousEnrollmentsDGIB(verificationEndDateArr); enrollments.push(tempGroupEnrollment); } if (month.length === 1) { const tempSingleEnrollment = claimantId - ? getSignlePreviousEnrollmentsDGIB(month[0]) + ? getSignlePreviousEnrollmentsDGIB(month[0], enrollmentVerifications) : getSignlePreviousEnrollments(month[0]); enrollments.push(tempSingleEnrollment); } diff --git a/src/applications/verify-your-enrollment/helpers.jsx b/src/applications/verify-your-enrollment/helpers.jsx index 9d0b0cbf1aa0..845935ca8555 100644 --- a/src/applications/verify-your-enrollment/helpers.jsx +++ b/src/applications/verify-your-enrollment/helpers.jsx @@ -9,6 +9,20 @@ import { TIMS_DOCUMENTS, } from './constants'; +export function getLatestVerificationThroughDate(enrollmentVerifications) { + let latestDate = null; + + for (const verification of enrollmentVerifications) { + if (verification.verificationThroughDate) { + const currentDate = new Date(verification.verificationThroughDate); + if (!latestDate || currentDate > latestDate) { + latestDate = currentDate; + } + } + } + + return latestDate ? latestDate?.toISOString().split('T')[0] : null; +} export function splitAddressLine(addressLine, maxLength) { if (addressLine?.length <= maxLength) { return { line1: addressLine, line2: '' }; @@ -304,6 +318,18 @@ export const isVerificationEndDateValid = verificationEndDate => { return endDate <= today; }; +export const isVerificationDateValid = ( + verificationEndDate, + enrollmentVerifications, +) => { + const latestVerificationThroughDate = getLatestVerificationThroughDate( + enrollmentVerifications, + ); + const targetDate = new Date(latestVerificationThroughDate); + const endDate = new Date(verificationEndDate); + + return endDate <= targetDate; +}; export const getPeriodsToVerifyDGIB = ( pendingEnrollments, shouldReverse = false, @@ -316,13 +342,12 @@ export const getPeriodsToVerifyDGIB = ( verificationEndDate, // lastDepositAmount, totalCreditHours, - verificationMethod, } = enrollmentToBeVerified; const myUUID = uuidv4(); return (
    - {!verificationMethod && + {!isVerificationDateValid(verificationEndDate, pendingEnrollments) && isVerificationEndDateValid(verificationEndDate) && (

    @@ -531,13 +556,10 @@ export const getGroupedPreviousEnrollments = month => { ); }; export const getGroupedPreviousEnrollmentsDGIB = enrollment => { - const { - verificationBeginDate, - verificationEndDate, - verificationMethod, - } = enrollment[0]; + const verificationBeginDate = enrollment[0]?.verificationBeginDate; + const verificationEndDate = enrollment[0]?.verificationEndDate; + const verificationMethod = enrollment[0]?.verificationMethod; const myUUID = uuidv4(); - return (

    {verificationMethod && isVerificationEndDateValid(verificationEndDate) ? ( @@ -615,45 +637,50 @@ export const getGroupedPreviousEnrollmentsDGIB = enrollment => { ) : ( <> - {!verificationMethod && - isVerificationEndDateValid(verificationEndDate) && ( - <> -

    - {translateDateIntoMonthYearFormat(verificationBeginDate)} -

    -
    - +

    + {translateDateIntoMonthYearFormat(verificationBeginDate)} +

    +
    + +

    -

    - You haven’t verified your enrollment for the month. -

    -
    -
    - - )} + You haven’t verified your enrollment for the month. +

    +
    +
    + + )} )}
    ); }; -export const getSignlePreviousEnrollmentsDGIB = enrollment => { +export const getSignlePreviousEnrollmentsDGIB = ( + enrollment, + enrollmentData, +) => { const myUUID = uuidv4(); return (
    - {!!enrollment?.verificationMethod && - isVerificationEndDateValid(enrollment.verificationEndDate) && ( + {isVerificationDateValid( + enrollment?.verificationEndDate, + enrollmentData, + ) && + isVerificationEndDateValid(enrollment?.verificationEndDate) && ( <>

    @@ -719,8 +746,11 @@ export const getSignlePreviousEnrollmentsDGIB = enrollment => { )} - {!enrollment?.verificationMethod && - isVerificationEndDateValid(enrollment.verificationEndDate) && ( + {!isVerificationDateValid( + enrollment?.verificationBeginDate, + enrollmentData, + ) && + isVerificationEndDateValid(enrollment?.verificationBeginDate) && ( <>

    {translateDateIntoMonthYearFormat( diff --git a/src/applications/verify-your-enrollment/hooks/useData.js b/src/applications/verify-your-enrollment/hooks/useData.js index cf9506986cab..c56812c74c2b 100644 --- a/src/applications/verify-your-enrollment/hooks/useData.js +++ b/src/applications/verify-your-enrollment/hooks/useData.js @@ -39,14 +39,16 @@ export const useData = () => { ?.remainingEntitlement; const enrollmentVerifications = response?.personalInfo?.recordResponse?.enrollmentVerifications; + const verifiedThroughDate = + response?.personalInfo?.recordResponse?.verifiedDetails[0] + .verifiedThroughDate; const updated = getCurrentDateFormatted(userInfo?.dateLastCertified); const { month, day } = remainingBenefits( userInfo?.remEnt || remainingEntitlement, ); - const fullName = `${profile?.userFullName?.first} ${ - profile.fullName?.middle - } ${profile.fullName?.last}`; + const fullName = `${profile?.userFullName?.first} ${profile.userFullName + ?.middle || ''} ${profile.userFullName?.last}`; const chapter = response?.personalInfo?.recordResponse?.verifiedDetails.at(-1) .benefitType; return { @@ -63,5 +65,6 @@ export const useData = () => { profile, fullName, enrollmentVerifications, + verifiedThroughDate, }; }; diff --git a/src/applications/verify-your-enrollment/tests/helpers/data.js b/src/applications/verify-your-enrollment/tests/helpers/data.js index 585ff1b4c040..a59d1ca91a18 100644 --- a/src/applications/verify-your-enrollment/tests/helpers/data.js +++ b/src/applications/verify-your-enrollment/tests/helpers/data.js @@ -104,3 +104,66 @@ export const verificationRecords = { ], paymentOnHold: true, }; + +export const enrollmentVerifications = [ + { + verificationMonth: 'January 2025', + verificationBeginDate: '2025-01-01', + verificationEndDate: '2025-01-14', + verificationResponse: 'NR', + totalCreditHours: null, + lastDepositAmount: 1100, + remainingEntitlement: '31-24', + }, + { + verificationMonth: 'January 2025', + verificationBeginDate: '2025-01-01', + verificationEndDate: '2025-01-16', + verificationResponse: 'NR', + totalCreditHours: null, + lastDepositAmount: 1662, + remainingEntitlement: '31-24', + }, + { + verificationMonth: 'January 2025', + verificationBeginDate: '2025-01-01', + verificationEndDate: '2025-01-31', + verificationResponse: 'r', + totalCreditHours: null, + lastDepositAmount: 2490, + remainingEntitlement: '31-25', + }, + { + verificationMonth: 'December 2024', + verificationBeginDate: '2024-12-20', + verificationEndDate: '2024-12-31', + verificationThroughDate: '2024-12-31', + verificationMethod: 'manual', + totalCreditHours: null, + verificationResponse: 'Y', + createdDate: '2025-01-02', + paymentTransmissionDate: '2025-01-02', + lastDepositAmount: 0, + remainingEntitlement: '31-24', + }, + { + verificationMonth: 'November 2024', + verificationBeginDate: '2024-11-01', + verificationEndDate: '2024-11-30', + verificationResponse: 'NR', + totalCreditHours: null, + paymentTransmissionDate: '2025-01-11', + lastDepositAmount: 1551, + remainingEntitlement: '31-24', + }, + { + verificationMonth: 'October 2024', + verificationBeginDate: '2024-10-01', + verificationEndDate: '2024-10-30', + verificationResponse: 'NR', + totalCreditHours: null, + paymentTransmissionDate: '2025-01-11', + lastDepositAmount: 1551, + remainingEntitlement: '31-24', + }, +]; diff --git a/src/applications/verify-your-enrollment/tests/helpers/getPeriodsToVerifyDGIB.unit.spec.js b/src/applications/verify-your-enrollment/tests/helpers/getPeriodsToVerifyDGIB.unit.spec.js index 20ae705e0e30..a20d827bc6c4 100644 --- a/src/applications/verify-your-enrollment/tests/helpers/getPeriodsToVerifyDGIB.unit.spec.js +++ b/src/applications/verify-your-enrollment/tests/helpers/getPeriodsToVerifyDGIB.unit.spec.js @@ -102,29 +102,4 @@ describe('getPeriodsToVerify', () => { expect(wrapper.children()).to.have.length(0); wrapper.unmount(); }); - it('should returns "Hours unavailable" for Total credit hours if numberHours is null', () => { - const wrapper = shallow( -
    - {getPeriodsToVerifyDGIB([ - { - verificationMonth: 'September 2023', - verificationBeginDate: '2024-09-12', - verificationEndDate: '2024-09-28', - verificationThroughDate: '2024-10-12', - createdDate: '2024-10-12', - verificationMethod: '', - verificationResponse: 'Y', - facilityName: 'San Francisco State university', - totalCreditHours: null, - paymentTransmissionDate: '2024-11-12', - lastDepositAmount: null, - remainingEntitlement: '05-07', - }, - ])} -
    , - ); - const divs = wrapper.find('[data-testid="total-credit-hours"]'); - expect(divs.text()).to.include('Hours unavailable'); - wrapper.unmount(); - }); }); diff --git a/src/applications/verify-your-enrollment/tests/helpers/getSignlePreviousEnrollmentsDGIB.unit.spec.js b/src/applications/verify-your-enrollment/tests/helpers/getSignlePreviousEnrollmentsDGIB.unit.spec.js index 64d25cb2d8f7..04aacfd06c7f 100644 --- a/src/applications/verify-your-enrollment/tests/helpers/getSignlePreviousEnrollmentsDGIB.unit.spec.js +++ b/src/applications/verify-your-enrollment/tests/helpers/getSignlePreviousEnrollmentsDGIB.unit.spec.js @@ -1,6 +1,7 @@ import { shallow } from 'enzyme'; import { expect } from 'chai'; import { getSignlePreviousEnrollmentsDGIB } from '../../helpers'; +import { enrollmentVerifications } from './data'; describe('getSignlePreviousEnrollmentsDGIB', () => { it('should render correctly when enrollment has verificationMethod and valid verificationEndDate', () => { @@ -13,7 +14,9 @@ describe('getSignlePreviousEnrollmentsDGIB', () => { facilityName: 'Test Facility', }; - const wrapper = shallow(getSignlePreviousEnrollmentsDGIB(enrollment)); + const wrapper = shallow( + getSignlePreviousEnrollmentsDGIB(enrollment, enrollmentVerifications), + ); expect(wrapper.find('h3').text()).to.include('Verified'); expect( wrapper @@ -36,21 +39,6 @@ describe('getSignlePreviousEnrollmentsDGIB', () => { wrapper.unmount(); }); - it('should render correctly when enrollment has no verificationMethod but valid verificationEndDate', () => { - const enrollment = { - verificationMethod: false, - verificationEndDate: '2023-01-31', - verificationBeginDate: '2023-01-01', - }; - - const wrapper = shallow(getSignlePreviousEnrollmentsDGIB(enrollment)); - expect(wrapper.find('h3').text()).to.equal('January 2023'); - expect( - wrapper.find('p[data-testid="have-not-verified"]').text(), - ).to.include('You haven’t verified your enrollment for the month.'); - wrapper.unmount(); - }); - it('should not render anything if verificationEndDate is invalid', () => { const enrollment = { verificationMethod: true, @@ -58,7 +46,9 @@ describe('getSignlePreviousEnrollmentsDGIB', () => { verificationBeginDate: null, }; - const wrapper = shallow(getSignlePreviousEnrollmentsDGIB(enrollment)); + const wrapper = shallow( + getSignlePreviousEnrollmentsDGIB(enrollment, enrollmentVerifications), + ); expect(wrapper.isEmptyRender()).to.be.false; wrapper.unmount(); }); diff --git a/src/applications/verify/components/AuthenticatedVerify.jsx b/src/applications/verify/components/AuthenticatedVerify.jsx deleted file mode 100644 index 9d4246aff556..000000000000 --- a/src/applications/verify/components/AuthenticatedVerify.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useEffect } from 'react'; -import { SERVICE_PROVIDERS } from 'platform/user/authentication/constants'; -import { focusElement } from '~/platform/utilities/ui'; -import { useSelector } from 'react-redux'; -import { selectProfile } from 'platform/user/selectors'; -import { - VerifyIdmeButton, - VerifyLogingovButton, -} from 'platform/user/authentication/components/VerifyButton'; - -export default function Authentication() { - const profile = useSelector(selectProfile); - useEffect( - () => { - if (!profile?.loading) { - focusElement('h1'); - } - }, - [profile.loading, profile.verified], - ); - - if (profile?.loading) { - return ( - - ); - } - const { idme, logingov } = SERVICE_PROVIDERS; - const signInMethod = profile?.signIn?.serviceName; - const singleVerifyButton = - signInMethod === 'logingov' ? ( - - ) : ( - - ); - - const deprecationDates = `${ - signInMethod === 'mhv' ? `January 31,` : `September 30,` - } 2025.`; - const { label } = SERVICE_PROVIDERS[signInMethod]; - const deprecationDatesContent = ( -

    - You’ll need to sign in with a different account after{' '} - {deprecationDates}. After this date, we’ll remove the{' '} - {label} sign-in option. You’ll need to sign in using a{' '} - Login.gov or ID.me account. -

    - ); - - return ( -
    -
    -
    -
    -

    Verify your identity

    - {![idme.policy, logingov.policy].includes(signInMethod) ? ( - <> - {deprecationDatesContent} -
    - - -
    - - ) : ( - <> -

    - We need you to verify your identity for your{' '} - {label} account. This step helps us protect - all Veterans’ information and prevent scammers from stealing - your benefits. -

    -

    - This one-time process often takes about 10 minutes. You’ll - need to provide certain personal information and - identification. -

    -
    {singleVerifyButton}
    - - Learn more about verifying your identity - - - )} -
    -
    -
    -
    - ); -} diff --git a/src/applications/verify/components/UnauthenticatedVerify.jsx b/src/applications/verify/components/UnauthenticatedVerify.jsx deleted file mode 100644 index c4c5d730e084..000000000000 --- a/src/applications/verify/components/UnauthenticatedVerify.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { - VerifyIdmeButton, - VerifyLogingovButton, -} from 'platform/user/authentication/components/VerifyButton'; - -export default function Authentication() { - return ( -
    -
    -
    -
    -

    Verify your identity

    - <> -

    - We need you to verify your identity for your{' '} - Login.gov or ID.me account. - This step helps us protect all Veterans’ information and prevent - scammers from stealing your benefits. -

    -

    - This one-time process often takes about 10 minutes. You’ll need - to provide certain personal information and identification. -

    -
    - - -
    - - Learn more about verifying your identity - - -
    -
    -
    -
    - ); -} diff --git a/src/applications/verify/components/UnifiedVerify.jsx b/src/applications/verify/components/UnifiedVerify.jsx new file mode 100644 index 000000000000..19c590dff3e4 --- /dev/null +++ b/src/applications/verify/components/UnifiedVerify.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { + isAuthenticatedWithOAuth, + signInServiceName, +} from 'platform/user/authentication/selectors'; +import { + VerifyIdmeButton, + VerifyLogingovButton, +} from 'platform/user/authentication/components/VerifyButton'; +import { hasSession } from 'platform/user/profile/utilities'; + +const Verify = () => { + const isAuthenticated = hasSession(); + const isAuthenticatedOAuth = useSelector(isAuthenticatedWithOAuth); + const loginServiceName = useSelector(signInServiceName); // Get the current SIS (e.g., idme or logingov) + + let buttonContent; + + if (isAuthenticated) { + <> + + + ; + } else if (isAuthenticatedOAuth) { + // Use the loginServiceName to determine which button to show + if (loginServiceName === 'idme') { + buttonContent = ; + } else if (loginServiceName === 'logingov') { + buttonContent = ; + } + } else { + buttonContent = ( + <> + + + + ); + } + + const renderServiceNames = () => { + if (isAuthenticated) { + return ( + {loginServiceName === 'idme' ? 'ID.me' : 'Login.gov'} + ); + } + return ( + <> + Login.gov or ID.me + + ); + }; + + return ( +
    +
    +
    +
    +

    Verify your identity

    +

    + We need you to verify your identity for your{' '} + {renderServiceNames()} account. This step helps us protect all + Veterans’ information and prevent scammers from stealing your + benefits. +

    +

    + This one-time process often takes about 10 minutes. You’ll need to + provide certain personal information and identification. +

    +
    {buttonContent}
    + + Learn more about verifying your identity + +
    +
    +
    +
    + ); +}; + +Verify.propTypes = { + loginType: PropTypes.string, +}; + +export default Verify; diff --git a/src/applications/verify/components/verifyButton.jsx b/src/applications/verify/components/verifyButton.jsx deleted file mode 100644 index 28f7f0d1e8cc..000000000000 --- a/src/applications/verify/components/verifyButton.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { updateStateAndVerifier } from 'platform/utilities/oauth/utilities'; - -import { signupOrVerify } from 'platform/user/authentication/utilities'; - -export const VerifyButton = ({ className, label, image, policy, useOAuth }) => { - const verifyHandler = async () => { - const url = await signupOrVerify({ - policy, - allowVerification: true, - isSignup: false, - isLink: true, - useOAuth, - }); - - if (useOAuth) { - updateStateAndVerifier(policy); - } - - window.location = url; - }; - - return ( - - ); -}; - -VerifyButton.propTypes = { - className: PropTypes.string, - image: PropTypes.node, - label: PropTypes.string, - policy: PropTypes.string, - useOAuth: PropTypes.bool, -}; diff --git a/src/applications/verify/containers/VerifyApp.jsx b/src/applications/verify/containers/VerifyApp.jsx index be49e6ced646..bad062d49cf5 100644 --- a/src/applications/verify/containers/VerifyApp.jsx +++ b/src/applications/verify/containers/VerifyApp.jsx @@ -1,31 +1,15 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import environment from '@department-of-veterans-affairs/platform-utilities/environment'; -import AuthenticatedVerify from '../components/AuthenticatedVerify'; -import UnauthenticatedVerify from '../components/UnauthenticatedVerify'; +import Verify from '../components/UnifiedVerify'; -export default function VerifyApp({ env = environment }) { - const isUnauthenticated = localStorage.getItem('hasSession') === null; - const isProduction = env?.isProduction(); - - useEffect( - () => { - document.title = `Verify your identity`; // title should match h1 tag - - if (isUnauthenticated && isProduction) { - window.location.replace('/'); - } - }, - [isUnauthenticated, isProduction], - ); +export default function VerifyApp() { + useEffect(() => { + document.title = `Verify your identity`; // Set the document title + }, []); return ( <> - {isUnauthenticated && !isProduction ? ( - - ) : ( - - )} + ); } diff --git a/src/applications/verify/tests/components/AuthenticatedVerify.unit.spec.js b/src/applications/verify/tests/components/AuthenticatedVerify.unit.spec.js deleted file mode 100644 index 1ffc18b93a36..000000000000 --- a/src/applications/verify/tests/components/AuthenticatedVerify.unit.spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { renderInReduxProvider } from 'platform/testing/unit/react-testing-library-helpers'; -import { $ } from 'platform/forms-system/src/js/utilities/ui'; -import AuthenticatedVerify from '../../components/AuthenticatedVerify'; - -const generateStore = ({ - csp = 'logingov', - verified = false, - loading = false, -} = {}) => ({ - user: { - profile: { - loading, - signIn: { serviceName: csp }, - verified, - session: { authBroker: 'iam' }, - }, - }, -}); - -describe('AuthenticatedVerify', () => { - afterEach(() => { - window.localStorage.clear(); - }); - - it('renders AuthenticatedVerify component', () => { - const initialState = generateStore(); - window.localStorage.setItem('hasSession', true); - const { getByTestId } = renderInReduxProvider(, { - initialState, - }); - - expect(getByTestId('authenticated-verify-app')).to.exist; - }); - - it('renders loading indicator when profile is loading', () => { - const initialState = generateStore({ loading: true }); - window.localStorage.setItem('hasSession', true); - - const { container } = renderInReduxProvider(, { - initialState, - }); - - const loadingIndicator = $('va-loading-indicator', container); - expect(loadingIndicator).to.exist; - }); - - ['mhv', 'dslogon'].forEach(csp => { - it(`displays both Login.gov and ID.me buttons for ${csp} users`, () => { - const initialState = generateStore({ csp }); - const { getByTestId } = renderInReduxProvider(, { - initialState, - }); - - const buttonGroup = getByTestId('verify-button-group'); - expect(buttonGroup.children.length).to.equal(2); - }); - }); - - ['logingov', 'idme'].forEach(csp => { - it(`displays single verification button for ${csp} users`, () => { - const initialState = generateStore({ csp }); - const { container } = renderInReduxProvider(, { - initialState, - }); - - const cspVerifyButton = $(`.${csp}-verify-button`, container); - expect(cspVerifyButton).to.exist; - }); - }); - - it('displays deprecation notice for mhv users', () => { - const initialState = generateStore({ csp: 'mhv' }); - const { container } = renderInReduxProvider(, { - initialState, - }); - - expect(container.textContent).to.include( - 'You’ll need to sign in with a different account after January 31, 2025.', - ); - }); - - it('displays deprecation notice for dslogon users', () => { - const initialState = generateStore({ csp: 'dslogon' }); - const { container } = renderInReduxProvider(, { - initialState, - }); - - expect(container.textContent).to.include( - 'You’ll need to sign in with a different account after September 30, 2025.', - ); - }); -}); diff --git a/src/applications/verify/tests/components/Unauthenticated.unit.spec.js b/src/applications/verify/tests/components/Unauthenticated.unit.spec.js deleted file mode 100644 index e4703fd9f0a6..000000000000 --- a/src/applications/verify/tests/components/Unauthenticated.unit.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { renderInReduxProvider } from 'platform/testing/unit/react-testing-library-helpers'; -import UnauthenticatedVerify from '../../components/UnauthenticatedVerify'; - -describe('UnauthenticatedVerify', () => { - it('renders the UnauthenticatedVerify component', () => { - const { getByTestId } = renderInReduxProvider(); - expect(getByTestId('unauthenticated-verify-app')).to.exist; - }); - - it('displays deprecation notice for My HealtheVet users', () => { - const { container } = renderInReduxProvider(); - expect(container.textContent).to.include( - 'We need you to verify your identity for your', - ); - }); - - it('renders both Login.gov and ID.me buttons', () => { - const { getByTestId } = renderInReduxProvider(); - const buttonGroup = getByTestId('verify-button-group'); - expect(buttonGroup.children.length).to.equal(2); - }); - - it('includes a link to learn more about verifying identity', () => { - const { container } = renderInReduxProvider(); - const link = container.querySelector( - 'a[href="/resources/verifying-your-identity-on-vagov/"]', - ); - expect(link).to.exist; - expect(link.textContent).to.include( - 'Learn more about verifying your identity', - ); - }); - - it('renders the "Verify your identity" heading', () => { - const { container } = renderInReduxProvider(); - const heading = container.querySelector('h1'); - expect(heading).to.exist; - expect(heading.textContent).to.equal('Verify your identity'); - }); -}); diff --git a/src/applications/verify/tests/components/UnifiedVerify.unit.spec.js b/src/applications/verify/tests/components/UnifiedVerify.unit.spec.js new file mode 100644 index 000000000000..af88305f7459 --- /dev/null +++ b/src/applications/verify/tests/components/UnifiedVerify.unit.spec.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { renderInReduxProvider } from 'platform/testing/unit/react-testing-library-helpers'; +import * as selectors from 'platform/user/authentication/selectors'; +import { $, fixSelector } from 'platform/forms-system/src/js/utilities/ui'; +import Verify from '../../components/UnifiedVerify'; + +describe('Verify', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + window.localStorage.clear(); + }); + + it('renders the Verify component', () => { + const { getByTestId } = renderInReduxProvider(); + expect(getByTestId('unauthenticated-verify-app')).to.exist; + }); + + it('displays the "Verify your identity" heading', () => { + const { container } = renderInReduxProvider(); + const heading = $('h1', container); + expect(heading).to.exist; + expect(heading.textContent).to.equal('Verify your identity'); + }); + + it('displays both Login.gov and ID.me buttons when unauthenticated', () => { + const { getByTestId } = renderInReduxProvider(); + const buttonGroup = getByTestId('verify-button-group'); + expect(buttonGroup.children.length).to.equal(2); + }); + + it('displays only the ID.me button when authenticated with ID.me', () => { + sandbox.stub(selectors, 'isAuthenticatedWithOAuth').returns(true); + sandbox.stub(selectors, 'signInServiceName').returns('idme'); + + const { container } = renderInReduxProvider(); + const idmeButton = $(fixSelector('.idme-verify-button'), container); + expect(idmeButton).to.exist; + + const logingovButton = $(fixSelector('.logingov-verify-button'), container); + expect(logingovButton).to.not.exist; + }); + + it('displays only the Login.gov button when authenticated with Login.gov', () => { + sandbox.stub(selectors, 'isAuthenticatedWithOAuth').returns(true); + sandbox.stub(selectors, 'signInServiceName').returns('logingov'); + + const { container } = renderInReduxProvider(); + const logingovButton = $(fixSelector('.logingov-verify-button'), container); + expect(logingovButton).to.exist; + + const idmeButton = $(fixSelector('.idme-verify-button'), container); + expect(idmeButton).to.not.exist; + }); + + it('includes a link to learn more about verifying identity', () => { + const { container } = renderInReduxProvider(); + const link = $( + 'a[href="/resources/verifying-your-identity-on-vagov/"]', + container, + ); + expect(link).to.exist; + expect(link.textContent).to.include( + 'Learn more about verifying your identity', + ); + }); +}); diff --git a/src/applications/verify/tests/containers/VerifyApp.unit.spec.js b/src/applications/verify/tests/containers/VerifyApp.unit.spec.js index 09fae080a963..1d781146c5f4 100644 --- a/src/applications/verify/tests/containers/VerifyApp.unit.spec.js +++ b/src/applications/verify/tests/containers/VerifyApp.unit.spec.js @@ -2,29 +2,14 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { renderInReduxProvider } from 'platform/testing/unit/react-testing-library-helpers'; import { expect } from 'chai'; -import sinon from 'sinon'; import VerifyApp from '../../containers/VerifyApp'; -const generateStore = ({ serviceName = 'logingov' } = {}) => ({ - user: { - profile: { - loading: false, - signIn: { serviceName }, - verified: false, - session: { authBroker: 'iam' }, - }, - }, -}); - describe('VerifyApp Component', () => { - const oldLocation = global.window.location; - afterEach(() => { - global.window.location = oldLocation; localStorage.clear(); }); - it('renders the correct title', async () => { + it('sets the correct document title', async () => { renderInReduxProvider(); await waitFor(() => { @@ -32,34 +17,11 @@ describe('VerifyApp Component', () => { }); }); - it('renders unauthenticated and not in production: UnauthenticatedVerify', async () => { - const screen = renderInReduxProvider(); - - await waitFor(() => { - expect(screen.queryByTestId('unauthenticated-verify-app')).to.exist; - expect(screen.queryByTestId('authenticated-verify-app')).to.not.exist; - }); - }); - - it('renders authenticated and not in production: AuthenticatedVerify', async () => { - localStorage.setItem('hasSession', true); - const screen = renderInReduxProvider(, { - initialState: generateStore(), - }); - - await waitFor(() => { - expect(screen.queryByTestId('unauthenticated-verify-app')).to.not.exist; - expect(screen.queryByTestId('authenticated-verify-app')).to.exist; - }); - }); - - it('redirects when unauthenticated and is production', async () => { - global.window.location.pathname = '/verify'; - const isProduction = sinon.stub().returns(true); - renderInReduxProvider(); + it('renders the UnauthenticatedVerify component', async () => { + const { getByTestId } = renderInReduxProvider(); await waitFor(() => { - expect(global.window.location.pathname).to.eql('/'); + expect(getByTestId('unauthenticated-verify-app')).to.exist; }); }); }); diff --git a/src/applications/virtual-agent/README.md b/src/applications/virtual-agent/README.md index 8e7f03a2b60b..d085c18b677e 100644 --- a/src/applications/virtual-agent/README.md +++ b/src/applications/virtual-agent/README.md @@ -1,4 +1,10 @@ # VA Virtual Agent on vets-website Quick Guide +```` +nvm use +```` +```` +yarn install +```` - To generate an index.html code coverage report for the virtual-agent app ``` yarn test:coverage-app virtual-agent @@ -9,4 +15,8 @@ yarn test:coverage-app virtual-agent - To run all unit tests in a specific file ``` npm run test:unit -- "path to file" +``` +- to run just the virtual agent website +``` +yarn watch --env entry=static-pages,auth,virtual-agent ``` \ No newline at end of file diff --git a/src/applications/virtual-agent/app-entry.jsx b/src/applications/virtual-agent/app-entry.jsx index 5bb475776e6b..ea890c52a555 100644 --- a/src/applications/virtual-agent/app-entry.jsx +++ b/src/applications/virtual-agent/app-entry.jsx @@ -6,6 +6,7 @@ import startApp from 'platform/startup'; import routes from './routes'; import reducer from './reducers'; import manifest from './manifest.json'; +import './utils/logging'; // Initialize Datadog const script = document.createElement('script'); script.nonce = '**CSP_NONCE**'; diff --git a/src/applications/virtual-agent/components/WebChat.jsx b/src/applications/virtual-agent/components/WebChat.jsx index 0475c623936f..90425b72661f 100644 --- a/src/applications/virtual-agent/components/WebChat.jsx +++ b/src/applications/virtual-agent/components/WebChat.jsx @@ -91,12 +91,17 @@ const WebChat = ({ TOGGLE_NAMES.virtualAgentEnableRootBot, ); + const isDatadogLoggingEnabled = useToggleValue( + TOGGLE_NAMES.virtualAgentEnableDatadogLogging, + ); + validateParameters({ csrfToken, apiSession, userFirstName, userUuid, setParamLoadingStatus, + isDatadogLoggingEnabled, }); const store = useWebChatStore({ @@ -112,7 +117,7 @@ const WebChat = ({ }); clearBotSessionStorageEventListener(isLoggedIn); - signOutEventListener(); + signOutEventListener(isDatadogLoggingEnabled); useBotPonyFill(setBotPonyfill, environment); useRxSkillEventListener(setIsRXSkill); diff --git a/src/applications/virtual-agent/event-listeners/signOutEventListener.js b/src/applications/virtual-agent/event-listeners/signOutEventListener.js index fbd1ff54f8cd..2added83f783 100644 --- a/src/applications/virtual-agent/event-listeners/signOutEventListener.js +++ b/src/applications/virtual-agent/event-listeners/signOutEventListener.js @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/browser'; import { clearBotSessionStorage } from '../utils/sessionStorage'; +import { logErrorToDatadog } from '../utils/logging'; -export default function signOutEventListener() { +export default function signOutEventListener(isDatadogLoggingEnabled) { const links = document.querySelectorAll('div#account-menu ul li a'); for (const link of links) { if (link.textContent === 'Sign Out') { @@ -12,7 +13,9 @@ export default function signOutEventListener() { } } - Sentry.captureException( - new TypeError('Virtual Agent chatbot could not find sign out link in menu'), + const error = new TypeError( + 'Virtual Agent chatbot could not find sign out link in menu', ); + Sentry.captureException(error); + logErrorToDatadog(isDatadogLoggingEnabled, error.message, error); } diff --git a/src/applications/virtual-agent/hooks/useDatadogLogging.js b/src/applications/virtual-agent/hooks/useDatadogLogging.js new file mode 100644 index 000000000000..00dff83c6db5 --- /dev/null +++ b/src/applications/virtual-agent/hooks/useDatadogLogging.js @@ -0,0 +1,6 @@ +import { useFeatureToggle } from 'platform/utilities/feature-toggles'; + +export function useDatadogLogging() { + const { useToggleValue, TOGGLE_NAMES } = useFeatureToggle(); + return useToggleValue(TOGGLE_NAMES.virtualAgentEnableDatadogLogging); +} diff --git a/src/applications/virtual-agent/hooks/useVirtualAgentToken.js b/src/applications/virtual-agent/hooks/useVirtualAgentToken.js index 7172b93ad15a..58dac77646c7 100644 --- a/src/applications/virtual-agent/hooks/useVirtualAgentToken.js +++ b/src/applications/virtual-agent/hooks/useVirtualAgentToken.js @@ -9,6 +9,8 @@ import { setConversationIdKey, setTokenKey, } from '../utils/sessionStorage'; +import { logErrorToDatadog } from '../utils/logging'; +import { useDatadogLogging } from './useDatadogLogging'; export function callVirtualAgentTokenApi( virtualAgentEnableMsftPvaTesting, @@ -30,7 +32,13 @@ export function callVirtualAgentTokenApi( }; } -async function getToken(props, setToken, setApiSession, setLoadingStatus) { +async function getToken( + props, + setToken, + setApiSession, + setLoadingStatus, + isDatadogLoggingEnabled, +) { try { const apiCall = callVirtualAgentTokenApi( props.virtualAgentEnableMsftPvaTesting, @@ -45,9 +53,9 @@ async function getToken(props, setToken, setApiSession, setLoadingStatus) { setApiSession(response.apiSession); setLoadingStatus(COMPLETE); } catch (ex) { - Sentry.captureException( - new Error('Could not retrieve virtual agent token'), - ); + const error = new Error('Could not retrieve virtual agent token'); + Sentry.captureException(error); + logErrorToDatadog(isDatadogLoggingEnabled, error.message, error); setLoadingStatus(ERROR); } } @@ -57,6 +65,7 @@ export default function useVirtualAgentToken(props) { const [apiSession, setApiSession] = useState(''); const [csrfTokenLoading, csrfTokenLoadingError] = useWaitForCsrfToken(props); const [loadingStatus, setLoadingStatus] = useState(LOADING); + const isDatadogLoggingEnabled = useDatadogLogging(); useEffect( () => { @@ -67,9 +76,15 @@ export default function useVirtualAgentToken(props) { clearBotSessionStorage(); - getToken(props, setToken, setApiSession, setLoadingStatus); + getToken( + props, + setToken, + setApiSession, + setLoadingStatus, + isDatadogLoggingEnabled, + ); }, - [csrfTokenLoading, csrfTokenLoadingError], + [csrfTokenLoading, csrfTokenLoadingError, props, isDatadogLoggingEnabled], ); return { token, loadingStatus, apiSession }; diff --git a/src/applications/virtual-agent/hooks/useWaitForCsrfToken.js b/src/applications/virtual-agent/hooks/useWaitForCsrfToken.js index 3dfc22718c2e..feb3a67ced64 100644 --- a/src/applications/virtual-agent/hooks/useWaitForCsrfToken.js +++ b/src/applications/virtual-agent/hooks/useWaitForCsrfToken.js @@ -2,19 +2,24 @@ import { useSelector } from 'react-redux'; import { useState, useEffect } from 'react'; import * as Sentry from '@sentry/browser'; import selectLoadingFeatureToggle from '../selectors/selectFeatureTogglesLoading'; +import { logErrorToDatadog } from '../utils/logging'; +import { useDatadogLogging } from './useDatadogLogging'; export function useWaitForCsrfToken(props) { // Once the feature toggles have loaded, the csrf token updates const csrfTokenLoading = useSelector(selectLoadingFeatureToggle); const [csrfTokenLoadingError, setCsrfTokenLoadingError] = useState(false); + const isDatadogLoggingEnabled = useDatadogLogging(); useEffect(() => { const timeout = setTimeout(() => { if (csrfTokenLoading) { setCsrfTokenLoadingError(true); - Sentry.captureException( - new Error('Could not load feature toggles within timeout'), + const error = new Error( + 'Could not load feature toggles within timeout', ); + Sentry.captureException(error); + logErrorToDatadog(isDatadogLoggingEnabled, error.message, error); } }, props.timeout); return function cleanup() { diff --git a/src/applications/virtual-agent/hooks/useWebChatFramework.js b/src/applications/virtual-agent/hooks/useWebChatFramework.js index 62cecc2b1391..36b3dca99605 100644 --- a/src/applications/virtual-agent/hooks/useWebChatFramework.js +++ b/src/applications/virtual-agent/hooks/useWebChatFramework.js @@ -2,10 +2,17 @@ import { useState } from 'react'; import * as Sentry from '@sentry/browser'; import { COMPLETE, ERROR, LOADING } from '../utils/loadingStatus'; import useLoadWebChat from './useLoadWebChat'; +import { logErrorToDatadog } from '../utils/logging'; +import { useDatadogLogging } from './useDatadogLogging'; const TIMEOUT_DURATION_MS = 250; -function checkForWebchat(setLoadingStatus, MAX_INTERVAL_CALL_COUNT, timeout) { +function checkForWebchat( + setLoadingStatus, + MAX_INTERVAL_CALL_COUNT, + timeout, + isDatadogLoggingEnabled, +) { let intervalCallCount = 0; const intervalId = setInterval(() => { intervalCallCount += 1; @@ -13,7 +20,13 @@ function checkForWebchat(setLoadingStatus, MAX_INTERVAL_CALL_COUNT, timeout) { setLoadingStatus(COMPLETE); clearInterval(intervalId); } else if (intervalCallCount > MAX_INTERVAL_CALL_COUNT) { - Sentry.captureException(new Error('Failed to load webchat framework')); + const errorMessage = new Error('Failed to load webchat framework'); + Sentry.captureException(errorMessage); + logErrorToDatadog( + isDatadogLoggingEnabled, + errorMessage.message, + errorMessage, + ); setLoadingStatus(ERROR); clearInterval(intervalId); } @@ -28,12 +41,13 @@ export default function useWebChatFramework(props) { useLoadWebChat(); const MAX_INTERVAL_CALL_COUNT = props.timeout / TIMEOUT_DURATION_MS; - + const isDatadogLoggingEnabled = useDatadogLogging(); if (loadingStatus === LOADING) { checkForWebchat( setLoadingStatus, MAX_INTERVAL_CALL_COUNT, TIMEOUT_DURATION_MS, + isDatadogLoggingEnabled, ); } diff --git a/src/applications/virtual-agent/tests/event-listeners/signOutEventListener.unit.spec.js b/src/applications/virtual-agent/tests/event-listeners/signOutEventListener.unit.spec.js index 34ecdef1307c..d9f4160f8199 100644 --- a/src/applications/virtual-agent/tests/event-listeners/signOutEventListener.unit.spec.js +++ b/src/applications/virtual-agent/tests/event-listeners/signOutEventListener.unit.spec.js @@ -4,6 +4,7 @@ import { expect } from 'chai'; import * as Sentry from '@sentry/browser'; import * as SessionStorageModule from '../../utils/sessionStorage'; import signOutEventListener from '../../event-listeners/signOutEventListener'; +import * as logging from '../../utils/logging'; describe('signOutEventListener', () => { let sandbox; @@ -59,9 +60,10 @@ describe('signOutEventListener', () => { }); it('should capture exception when sign out link is not found', () => { const captureExceptionStub = sandbox.stub(Sentry, 'captureException'); + const logErrorToDatadogStub = sandbox.stub(logging, 'logErrorToDatadog'); sandbox.stub(document, document.querySelectorAll.name).returns([]); - signOutEventListener(); + signOutEventListener(true); expect(captureExceptionStub.calledOnce).to.be.true; expect(captureExceptionStub.firstCall.args[0]).to.be.an.instanceOf( @@ -70,6 +72,14 @@ describe('signOutEventListener', () => { expect(captureExceptionStub.firstCall.args[0].message).to.equal( 'Virtual Agent chatbot could not find sign out link in menu', ); + expect(logErrorToDatadogStub.calledOnce).to.be.true; + expect(logErrorToDatadogStub.firstCall.args[0]).to.be.true; + expect(logErrorToDatadogStub.firstCall.args[1]).to.equal( + 'Virtual Agent chatbot could not find sign out link in menu', + ); + expect(logErrorToDatadogStub.firstCall.args[2]).to.be.an.instanceOf( + TypeError, + ); }); }); }); diff --git a/src/applications/virtual-agent/tests/hooks/useVirtualAgentToken.unit.spec.js b/src/applications/virtual-agent/tests/hooks/useVirtualAgentToken.unit.spec.js index c7de0fc194d2..ed1a14019409 100644 --- a/src/applications/virtual-agent/tests/hooks/useVirtualAgentToken.unit.spec.js +++ b/src/applications/virtual-agent/tests/hooks/useVirtualAgentToken.unit.spec.js @@ -11,6 +11,8 @@ import useVirtualAgentToken, { } from '../../hooks/useVirtualAgentToken'; import { ERROR, COMPLETE } from '../../utils/loadingStatus'; import * as UseWaitForCsrfTokenModule from '../../hooks/useWaitForCsrfToken'; +import * as LoggingModule from '../../utils/logging'; +import * as UseDatadogLoggingModule from '../../hooks/useDatadogLogging'; describe('useVirtualAgentToken', () => { let sandbox; @@ -79,9 +81,15 @@ describe('useVirtualAgentToken', () => { ) .returns([true, true]); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + let result; await act(async () => { - result = renderHook(() => useVirtualAgentToken({ timeout: 1 })); + result = renderHook(() => + useVirtualAgentToken({ + timeout: 1, + }), + ); }); expect(result.result.current.loadingStatus).to.equal(ERROR); @@ -98,6 +106,8 @@ describe('useVirtualAgentToken', () => { apiSession: 'ghi', }); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + let result; await act(async () => { result = renderHook(() => useVirtualAgentToken({ timeout: 1 })); @@ -107,7 +117,7 @@ describe('useVirtualAgentToken', () => { expect(result.result.current.loadingStatus).to.equal(COMPLETE); expect(result.result.current.apiSession).to.equal('ghi'); }); - it('should call Sentry when an exception is thrown', async () => { + it('should call Sentry and Datadog when an exception is thrown and the Datadog feature flag is enabled', async () => { sandbox .stub( UseWaitForCsrfTokenModule, @@ -118,6 +128,13 @@ describe('useVirtualAgentToken', () => { .stub(ApiModule, ApiModule.apiRequest.name) .throws(new Error('test')); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + const SentryCaptureExceptionSpy = sandbox.spy(); sandbox .stub(Sentry, 'captureException') @@ -125,11 +142,62 @@ describe('useVirtualAgentToken', () => { let result; await act(async () => { - result = renderHook(() => useVirtualAgentToken({ timeout: 1 })); + result = renderHook(() => + useVirtualAgentToken({ + timeout: 1, + }), + ); }); expect(result.result.current.loadingStatus).to.equal(ERROR); - expect(SentryCaptureExceptionSpy.calledOnce).to.be.true; + expect(logErrorToDatadogSpy.args[0][0]).to.be.true; + expect(logErrorToDatadogSpy.args[0][1]).to.equal( + 'Could not retrieve virtual agent token', + ); + expect(logErrorToDatadogSpy.args[0][2]).to.be.an.instanceOf(Error); + expect(SentryCaptureExceptionSpy.args[0][0]).to.be.an.instanceOf(Error); + expect(SentryCaptureExceptionSpy.args[0][0].message).to.equal( + 'Could not retrieve virtual agent token', + ); + }); + it('should call Sentry and not call Datadog when an exception is thrown and the Datadog feature flag is disabled', async () => { + sandbox + .stub( + UseWaitForCsrfTokenModule, + UseWaitForCsrfTokenModule.useWaitForCsrfToken.name, + ) + .returns([false, false]); + sandbox + .stub(ApiModule, ApiModule.apiRequest.name) + .throws(new Error('test')); + + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(false); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + + const SentryCaptureExceptionSpy = sandbox.spy(); + sandbox + .stub(Sentry, 'captureException') + .callsFake(SentryCaptureExceptionSpy); + + let result; + await act(async () => { + result = renderHook(() => + useVirtualAgentToken({ + timeout: 1, + }), + ); + }); + + expect(result.result.current.loadingStatus).to.equal(ERROR); + expect(logErrorToDatadogSpy.args[0][0]).to.be.false; + expect(logErrorToDatadogSpy.args[0][1]).to.equal( + 'Could not retrieve virtual agent token', + ); + expect(logErrorToDatadogSpy.args[0][2]).to.be.an.instanceOf(Error); expect(SentryCaptureExceptionSpy.args[0][0]).to.be.an.instanceOf(Error); expect(SentryCaptureExceptionSpy.args[0][0].message).to.equal( 'Could not retrieve virtual agent token', diff --git a/src/applications/virtual-agent/tests/hooks/useWaitForCsrfToken.unit.spec.js b/src/applications/virtual-agent/tests/hooks/useWaitForCsrfToken.unit.spec.js index 9e47f3f79a10..ab077bb1f7ae 100644 --- a/src/applications/virtual-agent/tests/hooks/useWaitForCsrfToken.unit.spec.js +++ b/src/applications/virtual-agent/tests/hooks/useWaitForCsrfToken.unit.spec.js @@ -4,6 +4,8 @@ import * as ReactRedux from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; import * as Sentry from '@sentry/browser'; import { useWaitForCsrfToken } from '../../hooks/useWaitForCsrfToken'; +import * as LoggingModule from '../../utils/logging'; +import * as UseDatadogLoggingModule from '../../hooks/useDatadogLogging'; describe('useWaitForCsrfToken', () => { let sandbox; @@ -21,7 +23,7 @@ describe('useWaitForCsrfToken', () => { clock.restore(); }); - it('should set error and call sentry when csrf token loading times out', async () => { + it('should set error and call sentry when csrf token loading times out and call Datadog if flag is enabled', async () => { sandbox.stub(ReactRedux, ReactRedux.useSelector.name).returns(true); const SentryCaptureExceptionSpy = sandbox.spy(); @@ -29,6 +31,13 @@ describe('useWaitForCsrfToken', () => { .stub(Sentry, 'captureException') .callsFake(SentryCaptureExceptionSpy); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + const { result } = renderHook(() => useWaitForCsrfToken({ timeout: 1 })); clock.tick(1); @@ -39,6 +48,12 @@ describe('useWaitForCsrfToken', () => { expect(SentryCaptureExceptionSpy.args[0][0].message).to.equal( 'Could not load feature toggles within timeout', ); + expect(logErrorToDatadogSpy.calledOnce).to.be.true; + expect(logErrorToDatadogSpy.args[0][0]).to.be.true; + expect(logErrorToDatadogSpy.args[0][1]).to.equal( + 'Could not load feature toggles within timeout', + ); + expect(logErrorToDatadogSpy.args[0][2]).to.be.an.instanceOf(Error); }); it('should not call sentry when csrf token loads in time', async () => { const SentryCaptureExceptionSpy = sandbox.spy(); @@ -49,11 +64,19 @@ describe('useWaitForCsrfToken', () => { .stub(Sentry, 'captureException') .callsFake(SentryCaptureExceptionSpy); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(false); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + const { result } = renderHook(() => useWaitForCsrfToken({ timeout: 1 })); clock.tick(1); expect(result.current[0]).to.equal(false); expect(result.current[1]).to.equal(false); expect(SentryCaptureExceptionSpy.calledOnce).to.be.false; + expect(logErrorToDatadogSpy.called).to.be.false; }); }); diff --git a/src/applications/virtual-agent/tests/hooks/useWebChatFramework.unit.spec.js b/src/applications/virtual-agent/tests/hooks/useWebChatFramework.unit.spec.js index b45338e8f1e5..2fc92c6183c1 100644 --- a/src/applications/virtual-agent/tests/hooks/useWebChatFramework.unit.spec.js +++ b/src/applications/virtual-agent/tests/hooks/useWebChatFramework.unit.spec.js @@ -7,6 +7,9 @@ import useWebChatFramework from '../../hooks/useWebChatFramework'; import { COMPLETE, ERROR, LOADING } from '../../utils/loadingStatus'; import * as UseLoadWebChatModule from '../../hooks/useLoadWebChat'; +import * as LoggingModule from '../../utils/logging'; +import * as UseDatadogLoggingModule from '../../hooks/useDatadogLogging'; + describe('useWebChatFramework', () => { let sandbox; let clock; @@ -34,16 +37,32 @@ describe('useWebChatFramework', () => { it('should return loadingStatus=LOADING while loading', async () => { sandbox.stub(UseLoadWebChatModule, 'default'); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + const { result } = renderHook(() => useWebChatFramework({ timeout: 1000 }), ); expect(result.current.loadingStatus).to.equal(LOADING); expect(result.current.webChatFramework).to.equal(global.window.WebChat); + + expect(logErrorToDatadogSpy.called).to.be.false; }); it('should return loadingStatus=COMPLETE and correct webChatFramework when web chat loads', async () => { sandbox.stub(UseLoadWebChatModule, 'default'); + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + const { result } = renderHook(() => useWebChatFramework({ timeout: 1000 }), ); @@ -54,6 +73,8 @@ describe('useWebChatFramework', () => { expect(result.current.loadingStatus).to.equal(COMPLETE); expect(result.current.webChatFramework).to.equal(global.window.WebChat); + + expect(logErrorToDatadogSpy.called).to.be.false; }); it('should call Sentry and return loadingStatus=ERROR if webchat fails to load in time', async () => { sandbox.stub(UseLoadWebChatModule, 'default'); @@ -61,6 +82,14 @@ describe('useWebChatFramework', () => { SentryModule, 'captureException', ); + + sandbox.stub(UseDatadogLoggingModule, 'useDatadogLogging').returns(true); + + const logErrorToDatadogSpy = sandbox.spy(); + sandbox + .stub(LoggingModule, 'logErrorToDatadog') + .callsFake(logErrorToDatadogSpy); + global.window.WebChat = null; const { result } = renderHook(() => @@ -70,6 +99,8 @@ describe('useWebChatFramework', () => { expect(captureExceptionStub.calledOnce).to.be.true; expect(result.current.loadingStatus).to.equal(ERROR); + + expect(logErrorToDatadogSpy.calledOnce).to.be.true; }); }); }); diff --git a/src/applications/virtual-agent/tests/utils/logging.unit.spec.js b/src/applications/virtual-agent/tests/utils/logging.unit.spec.js new file mode 100644 index 000000000000..2954dc7da7db --- /dev/null +++ b/src/applications/virtual-agent/tests/utils/logging.unit.spec.js @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { datadogLogs } from '@datadog/browser-logs'; +import { logErrorToDatadog } from '../../utils/logging'; + +describe('logErrorToDatadog', () => { + let datadogSpy; + + beforeEach(() => { + datadogSpy = sinon.spy(datadogLogs.logger, 'error'); + }); + + afterEach(() => { + datadogLogs.logger.error.restore(); + }); + + it('should log error to Datadog when logging is enabled', () => { + const message = 'Test error message'; + const context = { key: 'value' }; + + logErrorToDatadog(true, message, context); + + expect(datadogSpy.calledOnce).to.be.true; + expect(datadogSpy.calledWith(message, context)).to.be.true; + }); + + it('should not log error to Datadog when logging is disabled', () => { + const message = 'Test error message'; + const context = { key: 'value' }; + + logErrorToDatadog(false, message, context); + + expect(datadogSpy.notCalled).to.be.true; + }); +}); diff --git a/src/applications/virtual-agent/tests/utils/validateParameters.unit.spec.js b/src/applications/virtual-agent/tests/utils/validateParameters.unit.spec.js index 782038d0207a..4a46b1c12518 100644 --- a/src/applications/virtual-agent/tests/utils/validateParameters.unit.spec.js +++ b/src/applications/virtual-agent/tests/utils/validateParameters.unit.spec.js @@ -4,6 +4,7 @@ import * as Sentry from '@sentry/browser'; import validateParameters from '../../utils/validateParameters'; import { ERROR } from '../../utils/loadingStatus'; +import * as logging from '../../utils/logging'; describe('validateParameters', () => { let sandbox; @@ -57,6 +58,7 @@ describe('validateParameters', () => { Sentry, Sentry.captureException.name, ); + const logErrorToDatadogStub = sandbox.stub(logging, 'logErrorToDatadog'); validateParameters({ csrfToken: null, @@ -74,6 +76,15 @@ describe('validateParameters', () => { ), ), ); + + expect(logErrorToDatadogStub.calledOnce).to.be.true; + expect( + logErrorToDatadogStub.calledWith( + new TypeError( + 'Virtual Agent chatbot bad start - missing required variables: {"csrfToken":null}', + ), + ), + ); }); it('should capture exception when apiSession is missing', () => { const setParamLoadingStatusFn = sandbox.spy(); @@ -81,6 +92,7 @@ describe('validateParameters', () => { Sentry, Sentry.captureException.name, ); + const logErrorToDatadogStub = sandbox.stub(logging, 'logErrorToDatadog'); validateParameters({ csrfToken, @@ -98,6 +110,15 @@ describe('validateParameters', () => { ), ), ); + + expect(logErrorToDatadogStub.calledOnce).to.be.true; + expect( + logErrorToDatadogStub.calledWith( + new TypeError( + 'Virtual Agent chatbot bad start - missing required variables: {"apiSession":null}', + ), + ), + ); }); it('should capture exception when userFirstName is not a string', () => { const setParamLoadingStatusFn = sandbox.spy(); @@ -106,6 +127,8 @@ describe('validateParameters', () => { Sentry.captureException.name, ); + const logErrorToDatadogStub = sandbox.stub(logging, 'logErrorToDatadog'); + validateParameters({ csrfToken, apiSession, @@ -122,6 +145,15 @@ describe('validateParameters', () => { ), ), ); + + expect(logErrorToDatadogStub.calledOnce).to.be.true; + expect( + logErrorToDatadogStub.calledWith( + new TypeError( + 'Virtual Agent chatbot bad start - missing required variables: {"apiSession":null}', + ), + ), + ); }); it('should capture exception when userUuid is not a string', () => { const setParamLoadingStatusFn = sandbox.spy(); @@ -130,6 +162,8 @@ describe('validateParameters', () => { Sentry.captureException.name, ); + const logErrorToDatadogStub = sandbox.stub(logging, 'logErrorToDatadog'); + validateParameters({ csrfToken, apiSession, @@ -146,6 +180,15 @@ describe('validateParameters', () => { ), ), ); + + expect(logErrorToDatadogStub.calledOnce).to.be.true; + expect( + logErrorToDatadogStub.calledWith( + new TypeError( + 'Virtual Agent chatbot bad start - missing required variables: {"apiSession":null}', + ), + ), + ); }); }); }); diff --git a/src/applications/virtual-agent/utils/logging.js b/src/applications/virtual-agent/utils/logging.js new file mode 100644 index 000000000000..5734bcb63006 --- /dev/null +++ b/src/applications/virtual-agent/utils/logging.js @@ -0,0 +1,31 @@ +import { datadogLogs } from '@datadog/browser-logs'; + +// Conditional added to prevent initialization of Datadog as it was causing tests to hang indefinitely and prevented coverage report generation +if (!process.env.NODE_ENV === 'test') { + // Initialize Datadog logging + datadogLogs.init({ + clientToken: 'pubf64b43174e3eb74fa640b1ec28781c07', // Replace with your Datadog API key + site: 'ddog-gov.com', + forwardErrorsToLogs: true, // Automatically capture unhandled errors + sampleRate: 100, // Percentage of sessions to log (adjust as needed) + }); + + // Add a global context attribute to identify the source of the logs as the chatbot + datadogLogs.addLoggerGlobalContext('source', 'chatbot'); +} + +/** + * Logs an error to Datadog if the logging feature toggle is enabled. + * @param {Error|string} message - The error message or object to log. + * @param {Object} [context] - Additional context to log (optional). + */ +export function logErrorToDatadog( + isDatadogLoggingEnabled, + message, + context = {}, +) { + if (isDatadogLoggingEnabled) { + // Replace with your actual feature toggle name + datadogLogs.logger.error(message, context); + } +} diff --git a/src/applications/virtual-agent/utils/validateParameters.js b/src/applications/virtual-agent/utils/validateParameters.js index 11a88872b8fe..5051c1b7dd3f 100644 --- a/src/applications/virtual-agent/utils/validateParameters.js +++ b/src/applications/virtual-agent/utils/validateParameters.js @@ -1,6 +1,8 @@ import * as Sentry from '@sentry/browser'; import { ERROR } from './loadingStatus'; +import { logErrorToDatadog } from './logging'; + function hasAllParams(csrfToken, apiSession, userFirstName, userUuid) { return ( csrfToken && @@ -23,6 +25,7 @@ export default function validateParameters({ userFirstName, userUuid, setParamLoadingStatusFn, + isDatadogLoggingEnabled, }) { if (hasAllParams(csrfToken, apiSession, userFirstName, userUuid)) { return; @@ -45,11 +48,11 @@ export default function validateParameters({ userUuid: sanitizedUserUuid, }; - Sentry.captureException( - new TypeError( - `Virtual Agent chatbot bad start - missing required variables: ${JSON.stringify( - params, - )}`, - ), + const error = new TypeError( + `Virtual Agent chatbot bad start - missing required variables: ${JSON.stringify( + params, + )}`, ); + Sentry.captureException(error); + logErrorToDatadog(isDatadogLoggingEnabled, error.message, error); } diff --git a/src/platform/forms-system/src/js/actions.js b/src/platform/forms-system/src/js/actions.js index cd44c3e723ef..3f02b715fe2b 100644 --- a/src/platform/forms-system/src/js/actions.js +++ b/src/platform/forms-system/src/js/actions.js @@ -313,9 +313,13 @@ export function uploadFile( onChange({ ...fileData, isEncrypted: !!password }); } else { const fileObj = { file, name: file.name, size: file.size }; - let errorMessage = - 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.'; - const errorAlert = uiOptions?.fileUploadNetworkErrorAlert; + let errorMessage = req.statusText; + try { + // detail contains a better error message + errorMessage = JSON.parse(req?.response)?.errors?.[0]?.detail; + } catch (error) { + // intentionally empty + } if (req.status === 429) { errorMessage = `You’ve reached the limit for the number of submissions we can accept at this time. Please try again in ${timeFromNow( new Date( @@ -326,12 +330,7 @@ export function uploadFile( if (password) { onChange({ ...fileObj, errorMessage, isEncrypted: true }); } else { - const changePayload = { - ...fileObj, - errorMessage, - ...(errorAlert && { alert: errorAlert }), - }; - onChange(changePayload); + onChange({ ...fileObj, errorMessage }); } Sentry.captureMessage(`vets_upload_error: ${errorMessage}`); onError(); @@ -339,7 +338,9 @@ export function uploadFile( }); req.addEventListener('error', () => { - const errorMessage = FILE_UPLOAD_NETWORK_ERROR_MESSAGE; + const errorMessage = + uiOptions?.fileUploadNetworkErrorMessage || + FILE_UPLOAD_NETWORK_ERROR_MESSAGE; const errorAlert = uiOptions?.fileUploadNetworkErrorAlert; if (password) { diff --git a/src/platform/forms-system/src/js/fields/FileField.jsx b/src/platform/forms-system/src/js/fields/FileField.jsx index 53d253d96e2a..3ba008d1a57c 100644 --- a/src/platform/forms-system/src/js/fields/FileField.jsx +++ b/src/platform/forms-system/src/js/fields/FileField.jsx @@ -1,10 +1,10 @@ /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ import PropTypes from 'prop-types'; import React, { useEffect, useState, useRef } from 'react'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; +import { isLoggedIn } from 'platform/user/selectors'; import classNames from 'classnames'; import { VaModal } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - import { toggleValues } from '../../../../site-wide/feature-toggles/selectors'; import get from '../../../../utilities/data/get'; import set from '../../../../utilities/data/set'; @@ -27,6 +27,7 @@ import { checkTypeAndExtensionMatches, checkIsEncryptedPdf, FILE_TYPE_MISMATCH_ERROR, + reMapErrorMessage, } from '../utilities/file'; import { usePreviousValue } from '../helpers'; import { @@ -75,12 +76,131 @@ import { * @property {DOMFileObject} file - (Encrypted PDF only) File object, used * when user submits password */ + /** * Optional alert to override error message * @typedef {Object} Alert - * @property {string} header - The header text of the alert. - * @property {string[]} body - An array of strings representing the body content of the alert. + * @property {string} header - The title or headline of the alert. + * @property {Array} body - An array of strings or JSX elements to be displayed as the main body of the alert. + * @property {string} [formName] - Optional. The name of a form to display within the alert message. + * @property {string} [formNumber] - Optional. The form number (e.g., "21-526EZ") to include in the alert. + * @property {string} [formLink] - Optional. A URL to the form (e.g., a VA.gov form page) for users to download or view. + * @property {boolean} [showMailingAddress] - Optional. Whether to show the VA mailing address for submitting forms and documents. + */ +const MAILING_ADDRESS = ( + <> +

    + Mail your completed form and copies of supporting documents to this + address: +

    +

    + Department of Veterans Affairs
    + Pension Intake Center
    + PO Box 5365
    + Janesville, WI 53547-5365
    +

    + +); + +const AlertComponent = ({ alert }) => { + const { + header, + body, + formName, + formNumber, + formLink, + showMailingAddress, + } = alert; + return ( + + {header &&

    {header}

    } + {Array.isArray(body) && + body.map((content, i) => ( +

    + {content} +

    + ))} + {formName && ( +

    + Fill out an {formName} + {formNumber && ` (VA Form ${formNumber}).`} +

    + )} + {formLink && ( +

    + + Get VA Form {formNumber} to download + +

    + )} + {showMailingAddress && MAILING_ADDRESS} +
    + ); +}; + +AlertComponent.propTypes = { + alert: PropTypes.shape({ + header: PropTypes.string, + body: PropTypes.arrayOf(PropTypes.string), + formName: PropTypes.string, + formNumber: PropTypes.string, + formLink: PropTypes.string, + showMailingAddress: PropTypes.bool, + }).isRequired, +}; + +/** + * Conditionally displays either an error message or an alert based on the provided props and user login status. + * @typedef {Object} ErrorMessageOrAlertComponentProps + * @property {string} error - The error message string to display when no alert is rendered. + * @property {Object} [alert] - Optional. An object containing alert details for rendering an alert instead of an error message. + * @property {boolean} [hideAlertIfLoggedIn=false] - Optional. If `true`, the alert will not render if the user is logged in, + * and the error message will be shown instead. Defaults to `false`. */ +const ErrorMessageOrAlertComponent = ({ + hasVisibleError, + hasVisibleAlert, + error, + alert, + hideAlertIfLoggedIn = false, +}) => { + const loggedIn = useSelector(isLoggedIn); + const shouldHideAlert = hideAlertIfLoggedIn && loggedIn; + return ( + <> + {/* Show error message if there's no alert or if alert is hidden */} + {hasVisibleError && + (!hasVisibleAlert || shouldHideAlert) && ( + + Error {reMapErrorMessage(error)} + + )} + {/* Show alert if it's visible and not hidden */} + {hasVisibleAlert && + !shouldHideAlert && ( +
    + +
    + )} + + ); +}; + +ErrorMessageOrAlertComponent.propTypes = { + error: PropTypes.string.isRequired, + hasVisibleAlert: PropTypes.bool.isRequired, + hasVisibleError: PropTypes.bool.isRequired, + alert: PropTypes.shape({ + header: PropTypes.string, + body: PropTypes.arrayOf(PropTypes.string), + formName: PropTypes.string, + formNumber: PropTypes.string, + formLink: PropTypes.string, + showMailingAddress: PropTypes.bool, + }), + hideAlertIfLoggedIn: PropTypes.bool, +}; + const FileField = props => { const { enableShortWorkflow, @@ -623,28 +743,27 @@ const FileField = props => { /> )} - {/* Sometimes an error needs to be shown as an alert instead of an error message. - Do not show the error message if an alert is included with the error */} - {!file.uploading && - hasVisibleError && - !hasVisibleAlert && ( - - Error {errors[0]} - - )} - {/* Show the included alert instead of an error message */} + {/* Sometimes an error needs to be shown as an alert instead of an error message. */} {!file.uploading && - hasVisibleAlert && ( -
    - -

    {alerts[0].header}

    - {alerts[0].body.map((body, i) => ( -

    - {body} -

    - ))} -
    -
    + hasVisibleError && ( + )} {showPasswordInput && ( + error.includes(mapKey), + ); + return errormessageMaps?.[result] ?? error; +} diff --git a/src/platform/forms-system/src/js/utilities/file/index.js b/src/platform/forms-system/src/js/utilities/file/index.js index ce75bc873406..4419617e0662 100644 --- a/src/platform/forms-system/src/js/utilities/file/index.js +++ b/src/platform/forms-system/src/js/utilities/file/index.js @@ -10,6 +10,7 @@ import { PasswordSuccess, } from './ShowPdfPassword'; import arrayIncludesArray from './arrayIncludesArray'; +import reMapErrorMessage from './errorMessageMaps'; export { readAndCheckFile, @@ -21,4 +22,5 @@ export { ShowPdfPassword, PasswordLabel, PasswordSuccess, + reMapErrorMessage, }; diff --git a/src/platform/forms-system/src/js/web-component-patterns/radioPattern.jsx b/src/platform/forms-system/src/js/web-component-patterns/radioPattern.jsx index 2286fcf1a826..fca4e4b438ab 100644 --- a/src/platform/forms-system/src/js/web-component-patterns/radioPattern.jsx +++ b/src/platform/forms-system/src/js/web-component-patterns/radioPattern.jsx @@ -43,6 +43,7 @@ import VaRadioField from '../web-component-fields/VaRadioField'; * required?: UISchemaOptions['ui:required'], * errorMessages?: UISchemaOptions['ui:errorMessages'], * labelHeaderLevel?: UISchemaOptions['ui:options']['labelHeaderLevel'], + * labelHeaderLevelStyle?: UISchemaOptions['ui:options']['labelHeaderLevelStyle'], * hint?: string, * }} options * @returns {UISchemaOptions} diff --git a/src/platform/forms-system/test/config/helpers.js b/src/platform/forms-system/test/config/helpers.js index fda917159034..5a0cc7555292 100644 --- a/src/platform/forms-system/test/config/helpers.js +++ b/src/platform/forms-system/test/config/helpers.js @@ -45,6 +45,11 @@ export function axeCheck(component) { export const uploadStore = { getState: () => ({ + user: { + login: { + currentlyLoggedIn: true, + }, + }, featureToggles: { // eslint-disable-next-line camelcase file_upload_short_workflow_enabled: true, diff --git a/src/platform/forms-system/test/js/actions.unit.spec.js b/src/platform/forms-system/test/js/actions.unit.spec.js index 9ae879b09523..27dbe67952a5 100644 --- a/src/platform/forms-system/test/js/actions.unit.spec.js +++ b/src/platform/forms-system/test/js/actions.unit.spec.js @@ -702,8 +702,7 @@ describe('Schemaform actions:', () => { file: { name: '1.jpg', size: 1 }, name: '1.jpg', size: 1, - errorMessage: - 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', + errorMessage: 'Bad Request', }); }); it('should set error on network issue', () => { @@ -785,11 +784,92 @@ describe('Schemaform actions:', () => { file: { name: '1.jpg', size: 42 }, name: '1.jpg', size: 42, + errorMessage: 'Internal Server Error', + }); + }); + it('should NOT set custom error if network 500 response', () => { + const onChange = sinon.spy(); + const thunk = uploadFile( + { + name: '1.jpg', + size: 42, + }, + { + fileTypes: ['jpg'], + maxSize: 50, + fileUploadNetworkErrorMessage: + 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', + createPayload: f => f, + parseResponse: f => f.data.attributes, + }, + f => f, + onChange, + f => f, + ); + const dispatch = sinon.spy(); + const getState = sinon.stub().returns({ + form: { + data: {}, + }, + }); + + thunk(dispatch, getState); + + requests[0].respond(500, null, undefined); + + expect(onChange.firstCall.args[0]).to.eql({ + name: '1.jpg', + uploading: true, + }); + expect(onChange.secondCall.args[0]).to.eql({ + file: { name: '1.jpg', size: 42 }, + name: '1.jpg', + size: 42, + errorMessage: 'Internal Server Error', + }); + }); + it('should set custom error message on network error', () => { + const onChange = sinon.spy(); + const thunk = uploadFile( + { + name: '1.jpg', + size: 42, + }, + { + fileTypes: ['jpg'], + maxSize: 50, + fileUploadNetworkErrorMessage: + 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', + createPayload: f => f, + parseResponse: f => f.data.attributes, + }, + f => f, + onChange, + f => f, + ); + const dispatch = sinon.spy(); + const getState = sinon.stub().returns({ + form: { + data: {}, + }, + }); + + thunk(dispatch, getState); + + requests[0].error(); + + expect(onChange.firstCall.args[0]).to.eql({ + name: '1.jpg', + uploading: true, + }); + expect(onChange.secondCall.args[0]).to.eql({ + file: { name: '1.jpg', size: 42 }, + name: '1.jpg', errorMessage: 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', }); }); - it('should set alert on network issue', () => { + it('should set alert on network error', () => { const onChange = sinon.spy(); const thunk = uploadFile( { @@ -840,7 +920,7 @@ describe('Schemaform actions:', () => { }, }); }); - it('should set alert on failure', () => { + it('should not set alert on network 400 response', () => { const onChange = sinon.spy(); const thunk = uploadFile( { @@ -882,12 +962,7 @@ describe('Schemaform actions:', () => { file: { name: '1.jpg', size: 1 }, name: '1.jpg', size: 1, - errorMessage: - 'We’re sorry. There was problem with our system and we couldn’t upload your file. You can try again later.', - alert: { - header: 'Alert header text', - body: ['Alert body text'], - }, + errorMessage: 'Bad Request', }); }); }); diff --git a/src/platform/forms-system/test/js/fields/FileField.unit.spec.jsx b/src/platform/forms-system/test/js/fields/FileField.unit.spec.jsx index fd76bc1d650a..4eaf05c38f11 100644 --- a/src/platform/forms-system/test/js/fields/FileField.unit.spec.jsx +++ b/src/platform/forms-system/test/js/fields/FileField.unit.spec.jsx @@ -376,17 +376,19 @@ describe('Schemaform ', () => { }, }; const { container } = render( - f} - requiredSchema={requiredSchema} - />, + + f} + requiredSchema={requiredSchema} + /> + , ); // Prepend 'Error' for screenreader @@ -1233,18 +1235,20 @@ describe('Schemaform ', () => { $id: 'myIdSchemaId', }; const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, + + f} + requiredSchema={requiredSchema} + enableShortWorkflow + /> + , ); // id for main upload button is interpolated {idSchema.$id}_add_label @@ -1254,18 +1258,20 @@ describe('Schemaform ', () => { it('should render Upload a new file button for file with error', () => { const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, + + f} + requiredSchema={requiredSchema} + enableShortWorkflow + /> + , ); // This button is specific to the file that has the error @@ -1282,18 +1288,20 @@ describe('Schemaform ', () => { }, }; const { container } = render( - f} - requiredSchema={requiredSchema} - enableShortWorkflow - />, + + f} + requiredSchema={requiredSchema} + enableShortWorkflow + /> + , ); // This button is specific to the file that has the error @@ -1306,18 +1314,20 @@ describe('Schemaform ', () => { it('should render remove file button as cancel', () => { const onChangeSpy = sinon.spy(); const { container } = render( - , + + + , ); // This button is specific to the file that has the error diff --git a/src/platform/forms/constants.js b/src/platform/forms/constants.js index e4ebb43f3f7c..99db6f4beeab 100644 --- a/src/platform/forms/constants.js +++ b/src/platform/forms/constants.js @@ -74,6 +74,7 @@ export const VA_FORM_IDS = Object.freeze({ FORM_WELCOME_VA_SETUP_REVIEW_INFORMATION: 'WELCOME_VA_SETUP_REVIEW_INFORMATION', FORM_21_4140: '21-4140', + FORM_DISPUTE_DEBT: 'DISPUTE-DEBT', }); export const VA_FORM_IDS_SKIP_INFLECTION = Object.freeze([ @@ -156,6 +157,7 @@ export const getAllFormLinks = getAppUrlImpl => { [VA_FORM_IDS.FORM_WELCOME_VA_SETUP_REVIEW_INFORMATION]: `${tryGetAppUrl( 'welcome-va-setup-review-information', )}/`, + [VA_FORM_IDS.FORM_DISPUTE_DEBT]: `${tryGetAppUrl('DISPUTE-DEBT')}/`, }; }; @@ -466,6 +468,13 @@ export const MY_VA_SIP_FORMS = [ description: 'welcome va setup review information form', trackingPrefix: 'welcome-va-setup-review-information-', }, + { + id: VA_FORM_IDS.FORM_DISPUTE_DEBT, + benefit: 'digital dispute for debts', + title: 'Dispute your VA debt', + description: 'digital dispute for debts', + trackingPrefix: 'dispute-debt', + }, ]; export const FORM_BENEFITS = MY_VA_SIP_FORMS.reduce((acc, form) => { diff --git a/src/platform/forms/tests/address.unit.spec.js b/src/platform/forms/tests/address.unit.spec.js index 120690383823..9ba9b06b2ba7 100644 --- a/src/platform/forms/tests/address.unit.spec.js +++ b/src/platform/forms/tests/address.unit.spec.js @@ -1,9 +1,6 @@ import { expect } from 'chai'; import * as addressUtils from '../address/helpers'; -// Examples from: -// https://github.com/department-of-veterans-affairs/vets-api/blob/1efd2c206859b1a261e762a50cdb44dc8b66462d/spec/factories/pciu_address.rb#L34 - const domestic = { addressType: 'DOMESTIC', countryName: 'USA', diff --git a/src/platform/forms/tests/forms-config-validator.unit.spec.js b/src/platform/forms/tests/forms-config-validator.unit.spec.js index 6d399f68029a..096bda1292a9 100644 --- a/src/platform/forms/tests/forms-config-validator.unit.spec.js +++ b/src/platform/forms/tests/forms-config-validator.unit.spec.js @@ -67,6 +67,7 @@ const formConfigKeys = [ 'CustomTopContent', 'defaultDefinitions', 'dev', + 'disableSave', 'downtime', 'errorText', 'footerContent', @@ -279,6 +280,7 @@ const validateForm = async formConfigParam => { const optionalProps = { ariaDescribedBySubmit: 'string', dev: 'object', + disableSave: 'boolean', introduction: 'component', prefillEnabled: 'boolean', prefillTransformer: 'function', diff --git a/src/platform/mhv/exportsFile.js b/src/platform/mhv/exportsFile.js index 966c26735109..fda764f25fba 100644 --- a/src/platform/mhv/exportsFile.js +++ b/src/platform/mhv/exportsFile.js @@ -1,3 +1,4 @@ +export { useBackToTop } from './hooks/useBackToTop'; export { default as MHVDown } from './downtime/components/MHVDown'; export { default as MHVDowntimeApproaching, diff --git a/src/platform/mhv/hooks/useBackToTop.jsx b/src/platform/mhv/hooks/useBackToTop.jsx new file mode 100644 index 000000000000..d4481be81465 --- /dev/null +++ b/src/platform/mhv/hooks/useBackToTop.jsx @@ -0,0 +1,55 @@ +import { useState, useEffect, useRef } from 'react'; + +export const useBackToTop = location => { + const [isHidden, setIsHidden] = useState(true); + const [height, setHeight] = useState(0); + const measuredRef = useRef(); + + const { current } = measuredRef; + + useEffect( + () => { + if (!current) return () => {}; + + let isMounted = true; + + const resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(() => { + if (isMounted && height !== current.offsetHeight) { + setHeight(current.offsetHeight); + } + }); + }); + + resizeObserver.observe(current); + + return () => { + isMounted = false; + resizeObserver.disconnect(); + }; + }, + [current, height], + ); + + useEffect( + () => { + if (height) { + // small screen (mobile) + if (window.innerWidth <= 481 && height > window.innerHeight * 4) { + setIsHidden(false); + } + // medium screen (desktop/tablet) + else if (window.innerWidth > 481 && height > window.innerHeight * 2) { + setIsHidden(false); + } + // default to hidden + else { + setIsHidden(true); + } + } + }, + [height, location], + ); + + return { isHidden, measuredRef }; +}; diff --git a/src/platform/startup/authenticatedLoader.js b/src/platform/startup/authenticatedLoader.js new file mode 100644 index 000000000000..e69e9b363d7d --- /dev/null +++ b/src/platform/startup/authenticatedLoader.js @@ -0,0 +1,24 @@ +import { hasSession } from 'platform/user/profile/utilities'; + +/** + * Wrapper for data loaders that will only run if the user is authenticated. + * + * NB: If you wrap your loader with this function, you must also + * wrap your component with the RequiredLoginView component. + * + * @param {Object} options - The options object. + * @param {function} options.loader - The data loader function to be wrapped. + * @param {Object} [options.fallbackValue={}] - The value to return if the user is not authenticated. + * @returns {function} - A function that runs the loader if the user is authenticated, or returns the fallback value if not. + */ +export const authenticatedLoader = ({ loader, fallbackValue = {} }) => { + const userHasSession = hasSession() === true || hasSession() === 'true'; + + return async (...args) => { + if (!userHasSession) { + return fallbackValue; + } + + return loader(...args); + }; +}; diff --git a/src/platform/startup/exportsFile.js b/src/platform/startup/exportsFile.js index 8d3a59aca9cb..fbbab0da2aaa 100644 --- a/src/platform/startup/exportsFile.js +++ b/src/platform/startup/exportsFile.js @@ -3,9 +3,13 @@ export { default as startAppFromIndex } from './index'; // used outside platform // duplicated vat name: startApp, in platform, adjusted to startAppFromRouter export { default as startAppFromRouter } from './router'; // used outside platform +// React Router V6 +export { default as startAppFromRouterV6 } from './routerV6'; // used outside platform export { default as createCommonStore } from './store'; // used outside platform +export { authenticatedLoader } from './authenticatedLoader'; // used outside platform + // tentative export { default as createAnalyticsMiddleware } from './analytics-middleware'; export { default as startReactApp } from './react'; diff --git a/src/platform/startup/routerV6.js b/src/platform/startup/routerV6.js new file mode 100644 index 000000000000..b8b14becff48 --- /dev/null +++ b/src/platform/startup/routerV6.js @@ -0,0 +1,50 @@ +/** + * Module for functions related to starting up and application + * @module platform/startup + */ +import React from 'react'; +import { Provider } from 'react-redux'; +import { RouterProvider } from 'react-router-dom-v5-compat'; +import startReactApp from './react'; +import setUpCommonFunctionality from './setup'; + +/** + * Starts an application in the default element for standalone React + * applications. It also sets up the common store, starts the site-wide + * components (like the header menus and login widget), and wraps the provided + * routes in the React Router v6 RouterProvider + * + * @param {string} entryName The entryName of the application + * @param {BrowserRouter} router The V6 BrowserRouter component to use + * @param {object} appInfo.reducer An object containing reducer functions. Will have + * combineReducers run on it after being merged with the common, cross-site reducer. + * @param {string} appInfo.url The base url for the React application + * @param {array} appInfo.analyticsEvents An array which contains analytics events to collect + * when the respective actions are fired. + * @param {boolean} preloadScheduledDowntimes Whether to fetch scheduled downtimes - when set + * to true, the maintenance_windows API request is made without having to wait for the + * DowntimeNotification component to mount. This can improve startup time for applications + * that use the DowntimeNotification component. + */ +export default function startAppV6({ + router, + reducer, + url, + analyticsEvents, + entryName = 'unknown', + preloadScheduledDowntimes = false, +}) { + const store = setUpCommonFunctionality({ + entryName, + url, + reducer, + analyticsEvents, + preloadScheduledDowntimes, + }); + + startReactApp( + + + , + ); +} diff --git a/src/platform/startup/tests/authenticatedLoader.unit.spec.js b/src/platform/startup/tests/authenticatedLoader.unit.spec.js new file mode 100644 index 000000000000..eec730c396c9 --- /dev/null +++ b/src/platform/startup/tests/authenticatedLoader.unit.spec.js @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as profileUtilities from 'platform/user/profile/utilities'; +import { authenticatedLoader } from '../authenticatedLoader'; + +describe('authenticatedLoader', () => { + const sandbox = sinon.createSandbox(); + let hasSessionStub; + + beforeEach(() => { + hasSessionStub = sandbox.stub(profileUtilities, 'hasSession'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return an empty object by default if user is not authenticated', async () => { + hasSessionStub.returns(false); + const loader = sinon.stub(); + const wrappedLoader = authenticatedLoader({ loader }); + + const result = await wrappedLoader(); + + expect(result).to.be.empty; + expect(loader.called).to.be.false; + }); + + it('should return a fallback if specified and user is not authenticated', async () => { + hasSessionStub.returns(false); + const loader = sinon.stub(); + const fallbackValue = { foo: {} }; + const wrappedLoader = authenticatedLoader({ loader, fallbackValue }); + + const result = await wrappedLoader(); + + expect(result).to.be.deep.equal(fallbackValue); + expect(loader.called).to.be.false; + }); + + it('should call the loader if user is authenticated', async () => { + hasSessionStub.returns(true); + const loader = sinon.stub().resolves('data'); + const wrappedLoader = authenticatedLoader({ loader }); + + const result = await wrappedLoader('arg1', 'arg2'); + + expect(result).to.equal('data'); + expect(loader.calledWith('arg1', 'arg2')).to.be.true; + }); + + it('should call the loader if user session is true', async () => { + hasSessionStub.returns(true); + const loader = sinon.stub().resolves('data'); + const wrappedLoader = authenticatedLoader({ loader }); + + const result = await wrappedLoader('arg1', 'arg2'); + + expect(result).to.equal('data'); + expect(loader.calledWith('arg1', 'arg2')).to.be.true; + }); + + it('should call the loader if user session is "true"', async () => { + hasSessionStub.returns('true'); + const loader = sinon.stub().resolves('data'); + const wrappedLoader = authenticatedLoader({ loader }); + + const result = await wrappedLoader('arg1', 'arg2'); + + expect(result).to.equal('data'); + expect(loader.calledWith('arg1', 'arg2')).to.be.true; + }); +}); diff --git a/src/platform/startup/tests/routerV6.unit.spec.js b/src/platform/startup/tests/routerV6.unit.spec.js new file mode 100644 index 000000000000..c3f948df22a9 --- /dev/null +++ b/src/platform/startup/tests/routerV6.unit.spec.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { Provider } from 'react-redux'; +import { RouterProvider } from 'react-router-dom-v5-compat'; +import startAppV6 from '../routerV6'; +import * as reactUtils from '../react'; +import * as setupUtils from '../setup'; + +describe('startAppV6', () => { + let setUpCommonFunctionalityStub; + let startReactAppStub; + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + setUpCommonFunctionalityStub = sandbox.stub(setupUtils, 'default'); + startReactAppStub = sandbox.stub(reactUtils, 'default'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set up common functionality and start the React app', () => { + const mockStore = {}; + setUpCommonFunctionalityStub.returns(mockStore); + + const mockRouter = {}; + const mockReducer = {}; + const mockUrl = '/test'; + const mockAnalyticsEvents = []; + const mockEntryName = 'testApp'; + const mockPreloadScheduledDowntimes = true; + + startAppV6({ + router: mockRouter, + reducer: mockReducer, + url: mockUrl, + analyticsEvents: mockAnalyticsEvents, + entryName: mockEntryName, + preloadScheduledDowntimes: mockPreloadScheduledDowntimes, + }); + + expect(setUpCommonFunctionalityStub.calledOnce).to.be.true; + expect( + setUpCommonFunctionalityStub.calledWith({ + entryName: mockEntryName, + url: mockUrl, + reducer: mockReducer, + analyticsEvents: mockAnalyticsEvents, + preloadScheduledDowntimes: mockPreloadScheduledDowntimes, + }), + ).to.be.true; + + expect(startReactAppStub.calledOnce).to.be.true; + expect( + startReactAppStub.calledWith( + + + , + ), + ).to.be.true; + }); +}); diff --git a/src/platform/testing/package.json b/src/platform/testing/package.json index 3af772ce940c..b6b76ddef45d 100644 --- a/src/platform/testing/package.json +++ b/src/platform/testing/package.json @@ -23,7 +23,7 @@ "@cypress/code-coverage": "^3.10.0", "@testing-library/cypress": "^8.0.3", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.6", - "body-parser": "^1.15.2", + "body-parser": "^1.20.3", "cors": "^2.8.5", "cy-mobile-commands": "^0.3.0", "cypress-axe": "^1.0.0", diff --git a/src/platform/user/authentication/components/LoginInfo.jsx b/src/platform/user/authentication/components/LoginInfo.jsx index f10d23149f46..a28d15c1173b 100644 --- a/src/platform/user/authentication/components/LoginInfo.jsx +++ b/src/platform/user/authentication/components/LoginInfo.jsx @@ -1,26 +1,62 @@ import React from 'react'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles/useFeatureToggle'; import SubmitSignInForm from 'platform/static-data/SubmitSignInForm'; -export default () => ( -
    -
    -
    -

    Having trouble signing in?

    -

    - Get answers to common{' '} - - questions about signing in - {' '} - and{' '} - - verifying your identity - - . -

    -

    - We're here 24/7. -

    +export default () => { + const { useToggleValue, TOGGLE_NAMES } = useFeatureToggle(); + const mhvButtonDeprecated = useToggleValue( + TOGGLE_NAMES.mhvCredentialButtonDisabled, + ); + return ( +
    +
    +
    +

    Having trouble signing in?

    + {mhvButtonDeprecated ? ( +
    +

    Get help with questions about:

    + + +
    + ) : ( +

    + Get answers to common{' '} + + questions about signing in + {' '} + and{' '} + + verifying your identity + + . +

    + )} +

    + We're here 24/7. +

    +
    -
    -); + ); +}; diff --git a/src/platform/user/authentication/components/VerifyButton.jsx b/src/platform/user/authentication/components/VerifyButton.jsx index b3e25343c3a0..8a1e8fbf37cb 100644 --- a/src/platform/user/authentication/components/VerifyButton.jsx +++ b/src/platform/user/authentication/components/VerifyButton.jsx @@ -1,19 +1,17 @@ /* eslint-disable @department-of-veterans-affairs/prefer-button-component */ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { verify } from 'platform/user/authentication/utilities'; -import { isAuthenticatedWithOAuth } from 'platform/user/authentication/selectors'; import { updateStateAndVerifier } from 'platform/utilities/oauth/utilities'; import { defaultWebOAuthOptions } from 'platform/user/authentication/config/constants'; import { SERVICE_PROVIDERS } from 'platform/user/authentication/constants'; -export const verifyHandler = ({ policy, useOAuth, queryParams }) => { +export const verifyHandler = ({ policy, queryParams, useOAuth }) => { verify({ policy, - useOAuth, acr: defaultWebOAuthOptions.acrVerify[policy], queryParams, + useOAuth, }); if (useOAuth) { @@ -25,9 +23,8 @@ export const verifyHandler = ({ policy, useOAuth, queryParams }) => { * * @returns The updated design of the ID.me identity-verification button */ -export const VerifyIdmeButton = ({ queryParams }) => { +export const VerifyIdmeButton = ({ queryParams, useOAuth = false }) => { const { altImage, policy } = SERVICE_PROVIDERS.idme; - const useOAuth = useSelector(isAuthenticatedWithOAuth); return ( @@ -80,16 +80,21 @@ export const VerifyLogingovButton = ({ queryParams }) => { * @param {String} config.onClick - Used for unit-testing: DO NOT OVERWRITE * @returns A button with just the Login.gov or ID.me logo that is used to start the identity-verification process */ -export const VerifyButton = ({ csp, onClick = verifyHandler, queryParams }) => { +export const VerifyButton = ({ + csp, + onClick = verifyHandler, + queryParams, + useOAuth = false, +}) => { const { image } = SERVICE_PROVIDERS[csp]; - const useOAuth = useSelector(isAuthenticatedWithOAuth); const className = `usa-button ${csp}-verify-buttons`; + return (