From 24db1427c1261f0ce748445323ebb8ac1e2380f0 Mon Sep 17 00:00:00 2001 From: Adam Whitlock <8332986+adamwhitlock1@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:40:24 -0700 Subject: [PATCH 01/39] VADX - local development portal MVP - server management and configuration (#34070) * restore original mgmt server core files * refactor dev server mgmt add yarn workspace with script * unify exports for vadx panel and app route * refactor tabs into folders, chapter analyzer, depends page evaluator, memory usage component * add manifests endpoint * only display camelCased toggles in toggle tab, but update both in state * move to use routes.js file, logger util just for node context. * add start-fe route, arg parsing, common path utils, and validate start-fe request body * only allow specific mock server paths to be used, and use pre-determined paths as process args * allow process restart and force process quit on start endpoints * use promises for startProcess avoiding setTimeouts and race conditions that kept screwing up the status endpoint data, clean up promise based startup * cleanup file structure and router * nodemon for dev, use getter for manifest cache, fix vamc-ehr.json save location * refactor utils to specific file, add some prelim jsdoc comments * update package.json for POC * route for debug, vadx layout hoc, sidebar navigation, tab styling * split servers page into components and hook for processManager * feat: vadx advanced server mgmt * rename for clarity, cleanup * search by rootUrl for manifests * fix workspace packages path * eslint fixes * cleanup eslint, fix empty effect deps, useCallbacks, remove unused tabs comp --- .../hooks/useLocalStorage.js | 20 +- .../hooks/useMockedLogin.js | 22 +- .../script/drupal-vamc-data/mockLocalDSOT.js | 25 +- .../mocks/server.js | 4 +- .../nodemon.json | 9 + .../package.json | 4 +- .../pattern2/TaskGray/form/config/form.js | 3 +- .../pattern2/TaskGray/form/manifest.json | 7 - .../pattern2/TaskGray/status/manifest.json | 6 - .../pattern2/TaskOrange/manifest.json | 7 - .../_mock-form-ae-design-patterns/routes.jsx | 18 +- .../vadx/app/ApplicationSelector.jsx | 99 ------- .../vadx/app/layout/MainLayout.jsx | 33 +++ .../vadx/app/layout/Navigation.jsx | 56 ++++ .../vadx/app/layout/withLayout.jsx | 16 ++ .../vadx/app/pages/DevPanel.jsx | 243 ------------------ .../vadx/app/pages/debug/Debug.jsx | 5 + .../pages/feature-toggles/FeatureToggles.jsx | 5 + .../pages/servers/FrontendServerColumn.jsx | 66 +++++ .../servers/FrontendServerConfiguration.jsx | 217 ++++++++++++++++ .../app/pages/servers/MockServerColumn.jsx | 31 +++ .../vadx/app/pages/servers/OutputLineItem.jsx | 21 ++ .../vadx/app/pages/servers/ProcessOutput.jsx | 88 +++++++ .../vadx/app/pages/servers/ServerControls.jsx | 58 +++++ .../vadx/app/pages/servers/Servers.jsx | 29 +++ .../vadx/constants.js | 7 + .../vadx/context/processManager.jsx | 239 +++++++++++++++++ .../vadx/context/vadx.js | 17 +- .../vadx/index.jsx | 3 + .../vadx/panel/tabs/form/FormTab.jsx | 5 + .../vadx/server/constants/mockServerPaths.js | 44 ++++ .../vadx/server/index.js | 34 +++ .../vadx/server/routes/events.js | 36 +++ .../vadx/server/routes/manifests.js | 15 ++ .../vadx/server/routes/output.js | 21 ++ .../server/routes/start-frontend-server.js | 85 ++++++ .../vadx/server/routes/start-mock-server.js | 57 ++++ .../vadx/server/routes/status.js | 82 ++++++ .../vadx/server/routes/stop-on-port.js | 45 ++++ .../vadx/server/utils/cors.js | 42 +++ .../vadx/server/utils/logger.js | 73 ++++++ .../vadx/server/utils/manifests.js | 105 ++++++++ .../vadx/server/utils/parseArgs.js | 20 ++ .../vadx/server/utils/paths.js | 40 +++ .../vadx/server/utils/processes.js | 201 +++++++++++++++ .../vadx/server/utils/strings.js | 19 ++ .../vadx/utils/HeadingHierarchyAnalyzer.js | 2 +- .../vadx/utils/dates.js | 10 + 48 files changed, 1906 insertions(+), 388 deletions(-) create mode 100644 src/applications/_mock-form-ae-design-patterns/nodemon.json delete mode 100644 src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/form/manifest.json delete mode 100644 src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskGray/status/manifest.json delete mode 100644 src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskOrange/manifest.json delete mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/ApplicationSelector.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/layout/MainLayout.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/layout/Navigation.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/layout/withLayout.jsx delete mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/DevPanel.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/debug/Debug.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/feature-toggles/FeatureToggles.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerColumn.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/FrontendServerConfiguration.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/MockServerColumn.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/OutputLineItem.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ProcessOutput.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/ServerControls.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/app/pages/servers/Servers.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/constants.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/index.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js create mode 100644 src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js 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/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/server.js b/src/applications/_mock-form-ae-design-patterns/mocks/server.js index 0362d5a358eb..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, 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/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/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/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:

- -
- - -
- ); -}; 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 + } +}; From 6d0cabeab456d84c7fa7d581331360110d50b870 Mon Sep 17 00:00:00 2001 From: Nick Sayre Date: Tue, 14 Jan 2025 16:17:38 -0600 Subject: [PATCH 02/39] 1324 - remove save-in-progress from the My VA review contact information form (#34084) --- .../personalization/review-information/config/form.js | 1 + 1 file changed, 1 insertion(+) 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.', From 3154baff7c10866731de7611c86b53490172e2ec Mon Sep 17 00:00:00 2001 From: Afia Caruso <108290062+acaruso-oddball@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:24:39 -0500 Subject: [PATCH 03/39] Verify update 916 (#33982) * - Redirected unauthenticated users to . Enforced and added IAM query parameters. Simplified rendering and button logic. * first pass, refactor in process * made updates to simplify workflow, created unified verification page, streamlined verify handler and reverted changes * set default oauth param to false --- .../verify/components/AuthenticatedVerify.jsx | 90 ------- .../components/UnauthenticatedVerify.jsx | 42 ---- .../verify/components/UnifiedVerify.jsx | 110 +++++++++ .../verify/components/verifyButton.jsx | 46 ---- .../verify/containers/VerifyApp.jsx | 28 +-- .../AuthenticatedVerify.unit.spec.js | 94 -------- .../components/Unauthenticated.unit.spec.js | 42 ---- .../components/UnifiedVerify.unit.spec.js | 74 ++++++ .../tests/containers/VerifyApp.unit.spec.js | 46 +--- .../components/VerifyButton.jsx | 42 +++- .../components/VerifyButton.unit.spec.js | 221 +++++++++++------- 11 files changed, 363 insertions(+), 472 deletions(-) delete mode 100644 src/applications/verify/components/AuthenticatedVerify.jsx delete mode 100644 src/applications/verify/components/UnauthenticatedVerify.jsx create mode 100644 src/applications/verify/components/UnifiedVerify.jsx delete mode 100644 src/applications/verify/components/verifyButton.jsx delete mode 100644 src/applications/verify/tests/components/AuthenticatedVerify.unit.spec.js delete mode 100644 src/applications/verify/tests/components/Unauthenticated.unit.spec.js create mode 100644 src/applications/verify/tests/components/UnifiedVerify.unit.spec.js 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..bddf1505425b --- /dev/null +++ b/src/applications/verify/components/UnifiedVerify.jsx @@ -0,0 +1,110 @@ +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/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 ( - + ); }; @@ -247,4 +274,13 @@ POARequestDetailsPage.loader = ({ params, request }) => { }); }; +POARequestDetailsPage.createDecisionAction = async ({ request, params }) => { + const key = (await request.formData()).get('decision'); + const decision = DECISION_OPTIONS[key]; + + await api.createPOARequestDecision(params.id, decision); + + return redirect('..'); +}; + export default POARequestDetailsPage; diff --git a/src/applications/accredited-representative-portal/routes.jsx b/src/applications/accredited-representative-portal/routes.jsx index 3ad4169e72cf..40776d591497 100644 --- a/src/applications/accredited-representative-portal/routes.jsx +++ b/src/applications/accredited-representative-portal/routes.jsx @@ -82,6 +82,12 @@ const routes = [ path: 'poa-requests/:id', element: , loader: POARequestDetailsPage.loader, + children: [ + { + path: 'decision', + action: POARequestDetailsPage.createDecisionAction, + }, + ], }, ], }), diff --git a/src/applications/accredited-representative-portal/utilities/api.js b/src/applications/accredited-representative-portal/utilities/api.js index 0fe14865130f..d3eb475bf8c8 100644 --- a/src/applications/accredited-representative-portal/utilities/api.js +++ b/src/applications/accredited-representative-portal/utilities/api.js @@ -40,6 +40,16 @@ const api = { getUser: wrapApiRequest(() => { return ['/user']; }), + + createPOARequestDecision: wrapApiRequest((id, decision) => { + return [ + `/power_of_attorney_requests/${id}/decision`, + { + body: JSON.stringify({ decision }), + method: 'POST', + }, + ]; + }), }; export default api; diff --git a/src/applications/accredited-representative-portal/utilities/mockApi.js b/src/applications/accredited-representative-portal/utilities/mockApi.js index 85a52be78063..8a7c50a9d814 100644 --- a/src/applications/accredited-representative-portal/utilities/mockApi.js +++ b/src/applications/accredited-representative-portal/utilities/mockApi.js @@ -35,6 +35,25 @@ const mockApi = { getUser() { return apiFetch(user); }, + + createPOARequestDecision(id, { type }) { + const poaRequest = poaRequests.find(r => r.id === +id).attributes; + + switch (type) { + case 'acceptance': + poaRequest.status = 'Accepted'; + break; + case 'declination': + poaRequest.status = 'Declined'; + break; + default: + throw new Error(`Unexpected decision type: ${type}`); + } + + poaRequest.acceptedOrDeclinedAt = new Date().toISOString(); + + return apiFetch({}); + }, }; // Convenience runtime toggle between use of mock data and real data. @@ -50,8 +69,12 @@ export function configure() { } if (search.has('useMockData')) { - api.getPOARequest = mockApi.getPOARequest; - api.getPOARequests = mockApi.getPOARequests; + Object.values(mockApi).forEach(method => { + if (typeof method !== 'function') return; + if (method.name === 'getUser') return; + + api[method.name] = method; + }); } } From 7e75ac2f212fd90f1a42bf998ea702c20f1c025e Mon Sep 17 00:00:00 2001 From: Joseph Hall <89649306+joehall-tw@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:04:12 -0500 Subject: [PATCH 27/39] removes feature toggles from othher teams so that they don't show up in global searches and cause confusion (#34110) --- .../fixtures/api_va_gov/feature-toggles.json | 4386 +---------------- 1 file changed, 1 insertion(+), 4385 deletions(-) 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 From 59a31df482ba635084fde212efaa1177d0de7f11 Mon Sep 17 00:00:00 2001 From: Sean Midgley <57480791+Midge-dev@users.noreply.github.com> Date: Thu, 16 Jan 2025 07:39:28 -0800 Subject: [PATCH 28/39] Dependents | 101037: Fix missing submission date and confirmation number (#34092) --- .../components/IntroductionPageHeader.jsx | 2 +- src/applications/686c-674/config/constants.js | 2 ++ .../686c-674/containers/ConfirmationPage.jsx | 7 ++++--- .../686c-674/containers/IntroductionPage.jsx | 18 ++++++++++++++---- .../686c-674/containers/ConfirmationPage.jsx | 7 ++++--- 5 files changed, 25 insertions(+), 11 deletions(-) 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/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}.

Date: Thu, 16 Jan 2025 10:40:39 -0500 Subject: [PATCH 29/39] added alert message when is enrollment up to date (#34120) --- .../verify-your-enrollment/components/PeriodsToVerify.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 && (
From 0098df35aa956422fd255fa893ca527c7e8d9e34 Mon Sep 17 00:00:00 2001 From: Hemesh Patel <49699643+hemeshvpatel@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:44:43 -0600 Subject: [PATCH 30/39] add withRouter for router push (#34122) --- src/applications/ask-va/containers/CategorySelectPage.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/ask-va/containers/CategorySelectPage.jsx b/src/applications/ask-va/containers/CategorySelectPage.jsx index 497502b7e9b6..85ab2895fdf6 100644 --- a/src/applications/ask-va/containers/CategorySelectPage.jsx +++ b/src/applications/ask-va/containers/CategorySelectPage.jsx @@ -4,6 +4,7 @@ import { focusElement } from 'platform/utilities/ui'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import { connect, useDispatch } from 'react-redux'; +import { withRouter } from 'react-router'; import FormNavButtons from '~/platform/forms-system/src/js/components/FormNavButtons'; import { setCategoryID } from '../actions'; import RequireSignInModal from '../components/RequireSignInModal'; @@ -141,4 +142,4 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(CategorySelectPage); +export default connect(mapStateToProps)(withRouter(CategorySelectPage)); From 3d58c33935dccceffe55a7682194f702c6aeb74a Mon Sep 17 00:00:00 2001 From: ConnorJeff <122034971+ConnorJeff@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:10:32 -0500 Subject: [PATCH 31/39] MBMS-71539 Setting up address validation flow for preneed integration (#33435) * MBMS-71539 Setting up address validation flow for preneed integration * Redid address validation page, need to combine both custom pages still * MBMS-71539 fixed zip code error message * Renamed file * MBMS-71539 Dev Complete * MBMS-71539 Passed POR/UI/UX Review and added basic unit tests * MBMS-71359 Postal code error text for preparerContactDetails * MBMS-71539 Curvy punctuation * MBMS-71539 Revamped unit tests for suggested address pages --- .../components/PreparerHelpers.jsx | 4 +- .../components/SuggestedAddressRadio.jsx | 58 +++++++ .../pre-need-integration/config/form.jsx | 121 +++++++++++++- .../config/pages/addressConfirmation.jsx | 87 ++++++++++ .../pages/applicantSuggestedAddress.jsx | 136 +++++++++++++++ .../config/pages/preparerContactDetails.jsx | 58 ++++++- .../pages/preparerContactDetailsCustom.jsx | 158 ------------------ .../config/pages/preparerSuggestedAddress.jsx | 138 +++++++++++++++ .../pages/sponsorContactInformation.jsx | 46 +++-- .../config/pages/sponsorSuggestedAddress.jsx | 131 +++++++++++++++ .../definitions/address.js | 46 ++++- .../SuggestedAddressRadio.unit.spec.jsx | 58 +++++++ .../applicantSuggestedAddress.unit.spec.jsx | 110 ++++++++++++ .../preparerSuggestedAddress.unit.spec.jsx | 112 +++++++++++++ .../sponsorSuggestedAddress.unit.spec.jsx | 110 ++++++++++++ .../pre-need-integration/utils/helpers.js | 27 ++- 16 files changed, 1216 insertions(+), 184 deletions(-) create mode 100644 src/applications/pre-need-integration/components/SuggestedAddressRadio.jsx create mode 100644 src/applications/pre-need-integration/config/pages/addressConfirmation.jsx create mode 100644 src/applications/pre-need-integration/config/pages/applicantSuggestedAddress.jsx delete mode 100644 src/applications/pre-need-integration/config/pages/preparerContactDetailsCustom.jsx create mode 100644 src/applications/pre-need-integration/config/pages/preparerSuggestedAddress.jsx create mode 100644 src/applications/pre-need-integration/config/pages/sponsorSuggestedAddress.jsx create mode 100644 src/applications/pre-need-integration/tests/components/SuggestedAddressRadio.unit.spec.jsx create mode 100644 src/applications/pre-need-integration/tests/config/applicantSuggestedAddress.unit.spec.jsx create mode 100644 src/applications/pre-need-integration/tests/config/preparerSuggestedAddress.unit.spec.jsx create mode 100644 src/applications/pre-need-integration/tests/config/sponsorSuggestedAddress.unit.spec.jsx 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/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/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/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 ''; +}; From 7917c07a6215422ca986d642cea5ce07c8843af4 Mon Sep 17 00:00:00 2001 From: Micah Chiang <15097156+micahchiang@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:14:29 -0800 Subject: [PATCH 32/39] update library versions for component-library and css-library (#34091) * update library versions Signed-off-by: Micah Chiang <15097156+micahchiang@users.noreply.github.com> * update cypress selector for search Signed-off-by: Micah Chiang <15097156+micahchiang@users.noreply.github.com> * update selector for find-forms Signed-off-by: Micah Chiang <15097156+micahchiang@users.noreply.github.com> --------- Signed-off-by: Micah Chiang <15097156+micahchiang@users.noreply.github.com> --- package.json | 4 +- src/applications/search/tests/e2e/helpers.js | 2 +- .../find-forms-results.cypress.spec.js | 2 +- .../find-forms/tests/cypress/helpers.js | 2 +- yarn.lock | 64 +++++-------------- 5 files changed, 20 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index b851b063985a..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", 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/static-pages/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 index 30e8eb367618..78f224f01bae 100644 --- a/src/applications/static-pages/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 @@ -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/static-pages/find-forms/tests/cypress/helpers.js b/src/applications/static-pages/find-forms/tests/cypress/helpers.js index dcaf52859dab..68325e2b8358 100644 --- a/src/applications/static-pages/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/yarn.lock b/yarn.lock index b263bd2ea83f..748bdaee56ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2797,22 +2797,22 @@ react-focus-on "^3.5.1" react-transition-group "^1.0.0" -"@department-of-veterans-affairs/component-library@^48.3.0": - version "48.3.0" - resolved "https://registry.npmjs.org/@department-of-veterans-affairs/component-library/-/component-library-48.3.0.tgz#7c4f5cafa9793b9e5cc55f026e8dfc9ea89e6234" - integrity sha512-RG8N5eu2LVJuXVkCBQAFN+XQalkK//61+Kzy8oGWj49qeZKrdBIEGa9l7tk2vxOCJ9qwv3tPH/lZ6G/QZhvElA== +"@department-of-veterans-affairs/component-library@^48.4.0": + version "48.4.0" + resolved "https://registry.yarnpkg.com/@department-of-veterans-affairs/component-library/-/component-library-48.4.0.tgz#138390fbb904746550d950ecb9c5104ad852abe2" + integrity sha512-ApUpW3nysBSqGkGNjPTzKserqRdKIHktj5xnihmF+dXyjgneegcyKohBqn1QmjgPD2VK/51mmMkAON12cTCb5w== dependencies: "@department-of-veterans-affairs/react-components" "28.1.0" - "@department-of-veterans-affairs/web-components" "16.3.0" + "@department-of-veterans-affairs/web-components" "16.4.0" i18next "^21.6.14" i18next-browser-languagedetector "^6.1.4" react-focus-on "^3.5.1" react-transition-group "^1.0.0" -"@department-of-veterans-affairs/css-library@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@department-of-veterans-affairs/css-library/-/css-library-0.16.1.tgz#3e039e06d533b885d0204dda1ef9552a0db04873" - integrity sha512-gP9p0pvb4BcLNVMVuQq9tCTQ1xI1+GEpZVIvXq010TZVbKsD/of7sPL0Og8yupOAcnIUMxztptfopNYaC7ptdw== +"@department-of-veterans-affairs/css-library@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@department-of-veterans-affairs/css-library/-/css-library-0.17.0.tgz#4959408ddb593201256b13386ab43853bc21dbb6" + integrity sha512-dH+JMPL7qio6/rBxYPB6wXL21WkYHE9+wab1BJ0x+8pdu1nap1ZEkBS5oTu6VmizaFsaTa1t3LKv3fGPNSRD+Q== dependencies: "@divriots/style-dictionary-to-figma" "^0.4.0" "@uswds/uswds" "^3.9.0" @@ -2897,10 +2897,10 @@ classnames "^2.3.1" intersection-observer "^0.12.0" -"@department-of-veterans-affairs/web-components@16.3.0": - version "16.3.0" - resolved "https://registry.npmjs.org/@department-of-veterans-affairs/web-components/-/web-components-16.3.0.tgz#24aea11a21bec926baa2f09e4665b2d2c0d3fbdf" - integrity sha512-FHQoYnEpuDW9ynMBb94AWtqgzirpvcoT9c/uY9Evb/bYFqOhO6FRz6nQ1J0p6KvZOs3T3huhuf2JguZHaO1JBA== +"@department-of-veterans-affairs/web-components@16.4.0": + version "16.4.0" + resolved "https://registry.yarnpkg.com/@department-of-veterans-affairs/web-components/-/web-components-16.4.0.tgz#77e6a56f51e2c283da02da7b9ca6814daf182282" + integrity sha512-fPYEtKryL8kcHyisBdgnrhVac5+joEbhMQLmtGbxTyJkZmtPHGmyIHLCMzISzJac2zT6YDBcbIAJI4qvpqkxsA== dependencies: "@stencil/core" "4.20.0" aria-hidden "^1.1.3" @@ -11830,15 +11830,6 @@ history@3, history@^3.0.0: query-string "^4.2.2" warning "^3.0.0" -history@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/history/-/history-3.0.0.tgz#02cff4e6f69dc62dd81161104a63f5b85ead0c85" - integrity sha512-z1VgeZAyJx9txVPaCO168b2YyRbrpMwzP2AzC+bkNjIFhJHB496UVh/wRlW/Xh9cIyrJlsW9j1A/eYcVMFa8Jw== - dependencies: - invariant "^2.0.0" - query-string "^4.1.0" - warning "^2.0.0" - history@^4.9.0: version "4.10.1" resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz" @@ -11867,11 +11858,6 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" - integrity sha512-r8huvKK+m+VraiRipdZYc+U4XW43j6OFG/oIafe7GfDbRpCduRoX9JI/DRxqgtBSCeL+et6N6ibZoedHS2NyOQ== - hoist-non-react-statics@^2.3.1: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -12543,7 +12529,7 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" -invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -17428,7 +17414,7 @@ qs@6.13.0, qs@6.7.0, qs@^6.0.4, qs@^6.12.3, qs@^6.4.0, qs@~6.5.2: dependencies: side-channel "^1.0.6" -query-string@^4.1.0, query-string@^4.2.2: +query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= @@ -17765,19 +17751,6 @@ react-router@3: react-is "^16.13.0" warning "^3.0.0" -react-router@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.0.tgz#62b6279d589b70b34e265113e4c0a9261a02ed36" - integrity sha512-sXlLOg0TRCqnjCVskqBHGjzNjcJKUqXEKnDSuxMYJSPJNq9hROE9VsiIW2kfIq7Ev+20Iz0nxayekXyv0XNmsg== - dependencies: - create-react-class "^15.5.1" - history "^3.0.0" - hoist-non-react-statics "^1.2.0" - invariant "^2.2.1" - loose-envify "^1.2.0" - prop-types "^15.5.6" - warning "^3.0.0" - react-router@5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" @@ -21616,13 +21589,6 @@ walk@2.3.x: dependencies: foreachasync "^3.0.0" -warning@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/warning/-/warning-2.1.0.tgz#21220d9c63afc77a8c92111e011af705ce0c6901" - integrity sha512-O9pvum8nlCqIT5pRGo2WRQJPRG2bW/ZBeCzl7/8CWREjUW693juZpGup7zbRtuVcSKyGiRAIZLYsh3C0vq7FAg== - dependencies: - loose-envify "^1.0.0" - warning@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" From 2d297d9c7b904caa10a201d777cd63064650203e Mon Sep 17 00:00:00 2001 From: Isaac Gallup <142249919+IGallupSoCo@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:22:44 -0600 Subject: [PATCH 33/39] Upgrade body-parser package to 1.20.3 to close a DDoS vulnerability (#34115) * partial ugprade * next step * body-parser 1.19.0 * another increment * body-parser 1.20.0 * body-parser 1.20.3 --- src/platform/testing/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/yarn.lock b/yarn.lock index 748bdaee56ea..f2f5a95dbf4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6217,7 +6217,7 @@ body-parser@1.19.0: raw-body "2.4.0" type-is "~1.6.17" -body-parser@1.20.3, body-parser@^1.15.2: +body-parser@1.20.3, body-parser@^1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== From aeb88a1afb41de6184872fb63e8a14eff344cb20 Mon Sep 17 00:00:00 2001 From: Chris Kim <42885441+chriskim2311@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:37:59 -0800 Subject: [PATCH 34/39] VACMS-20283: Add bold note to reason question (#34117) --- .../discharge-wizard/components/questions/Reason.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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, From bc114be09d31ab6999f12a595e40064b4d3c6a7d Mon Sep 17 00:00:00 2001 From: nichelous-herndon <137448049+nichelous-herndon@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:19:18 -0600 Subject: [PATCH 35/39] 1957 datadog browser logs (#33700) * [#1957] Add Datadog init and helper function * [#1957] Add Datadog to signOutEventListener * [#1957] Add useDatadog hook and dd log in useVirtualAgentToken * [#1957] Fix useVirtualAgentToken tests * [#1957] Add DD logging to useWaitForCsrfToken and tests * add virtual agent local run command * add command * add feature toggle and test coverage for useWebChatFramework * add feature toggle and test * [#1957] Add conditional check on NODE_ENV before DD init * [#1957] Import logging into app entry, add client token for DD init * add source tag to datadog logging for chatbot * [#1957] Fix workflow lint warning * Added test cases for datadog * removed unneeded tests --------- Co-authored-by: Alex Person Co-authored-by: b-marrone --- src/applications/virtual-agent/README.md | 10 +++ src/applications/virtual-agent/app-entry.jsx | 1 + .../virtual-agent/components/WebChat.jsx | 7 +- .../event-listeners/signOutEventListener.js | 9 ++- .../virtual-agent/hooks/useDatadogLogging.js | 6 ++ .../hooks/useVirtualAgentToken.js | 27 +++++-- .../hooks/useWaitForCsrfToken.js | 9 ++- .../hooks/useWebChatFramework.js | 20 ++++- .../signOutEventListener.unit.spec.js | 12 ++- .../hooks/useVirtualAgentToken.unit.spec.js | 76 ++++++++++++++++++- .../hooks/useWaitForCsrfToken.unit.spec.js | 25 +++++- .../hooks/useWebChatFramework.unit.spec.js | 31 ++++++++ .../tests/utils/logging.unit.spec.js | 35 +++++++++ .../utils/validateParameters.unit.spec.js | 43 +++++++++++ .../virtual-agent/utils/logging.js | 31 ++++++++ .../virtual-agent/utils/validateParameters.js | 15 ++-- 16 files changed, 330 insertions(+), 27 deletions(-) create mode 100644 src/applications/virtual-agent/hooks/useDatadogLogging.js create mode 100644 src/applications/virtual-agent/tests/utils/logging.unit.spec.js create mode 100644 src/applications/virtual-agent/utils/logging.js 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); } From 866b5515f791269fe7ade8cbd8fd0ca4fd9555a3 Mon Sep 17 00:00:00 2001 From: Taras Kurilo Date: Thu, 16 Jan 2025 12:33:04 -0500 Subject: [PATCH 36/39] Tk/edm 433 format updates (#34119) * edm-433 name format and address format updated * edm-433 formating updates * added website url --- .../gi/containers/NationalExamDetails.jsx | 44 ++--- .../gi/containers/NationalExamsList.jsx | 13 +- .../NationalExamDetails.unit.spec.jsx | 5 +- .../gi/tests/utils/helpers.unit.spec.js | 165 ++++++++++++++++++ src/applications/gi/utils/helpers.js | 69 ++++++++ 5 files changed, 264 insertions(+), 32 deletions(-) diff --git a/src/applications/gi/containers/NationalExamDetails.jsx b/src/applications/gi/containers/NationalExamDetails.jsx index f6f4bf817482..a3cd0abb8b8b 100644 --- a/src/applications/gi/containers/NationalExamDetails.jsx +++ b/src/applications/gi/containers/NationalExamDetails.jsx @@ -2,22 +2,21 @@ import React, { useEffect, useState, useLayoutEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { - VaIcon, - VaLink, - VaAlert, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import moment from 'moment'; import { fetchNationalExamDetails } from '../actions'; +import { + formatNationalExamName, + formatAddress, + toTitleCase, +} from '../utils/helpers'; const NationalExamDetails = () => { const dispatch = useDispatch(); const { examId } = useParams(); const [isMobile, setIsMobile] = useState(false); - const { examDetails, loadingDetails, error } = useSelector( state => state.nationalExams, ); - useEffect( () => { window.scrollTo(0, 0); @@ -91,7 +90,7 @@ const NationalExamDetails = () => { if (error) { return (
- {

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

-
+
); } @@ -122,28 +121,30 @@ 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}, + {formatAddress(institution.physicalAddress?.city)},{' '} {institution.physicalAddress?.state}{' '} {institution.physicalAddress?.zip}

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

- @@ -177,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', { diff --git a/src/applications/gi/containers/NationalExamsList.jsx b/src/applications/gi/containers/NationalExamsList.jsx index efc9bb95719e..9b61c03e8903 100644 --- a/src/applications/gi/containers/NationalExamsList.jsx +++ b/src/applications/gi/containers/NationalExamsList.jsx @@ -4,10 +4,7 @@ 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'; @@ -66,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’re sorry. There’s a problem with our system. Try again later.

-
+
); } @@ -122,7 +119,7 @@ const NationalExamsList = () => {
    {currentExams.map(exam => (
  • - +

    {formatNationalExamName(exam.name)}

    @@ -137,7 +134,7 @@ const NationalExamsList = () => { )}`} onClick={handleRouteChange(exam.enrichedId)} /> -
    +
  • ))}
diff --git a/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx b/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx index 456f6520e551..16ad64c1f33e 100644 --- a/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx +++ b/src/applications/gi/tests/containers/NationalExamDetails.unit.spec.jsx @@ -129,7 +129,7 @@ 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,VA 12345'); + 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/"]', ); @@ -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(); }); diff --git a/src/applications/gi/tests/utils/helpers.unit.spec.js b/src/applications/gi/tests/utils/helpers.unit.spec.js index ec5293a313f3..ee6003b48257 100644 --- a/src/applications/gi/tests/utils/helpers.unit.spec.js +++ b/src/applications/gi/tests/utils/helpers.unit.spec.js @@ -32,6 +32,8 @@ import { capitalizeFirstLetter, getAbbreviationsAsArray, formatNationalExamName, + formatAddress, + toTitleCase, } from '../../utils/helpers'; describe('GIBCT helpers:', () => { @@ -776,4 +778,167 @@ describe('GIBCT helpers:', () => { 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 f45233171b0c..3074225bf28d 100644 --- a/src/applications/gi/utils/helpers.js +++ b/src/applications/gi/utils/helpers.js @@ -879,3 +879,72 @@ export const formatNationalExamName = name => { 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(' '); +}; From b4773bd2b854865587153ae47df2ba3082b2e988 Mon Sep 17 00:00:00 2001 From: Tony Wiliiams <3587066+vbahinwillit@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:18:30 -0600 Subject: [PATCH 37/39] update to add referral-appointments (#34125) Co-authored-by: Tony Williams --- src/applications/vaos/.eslintrc | 4 ++++ 1 file changed, 4 insertions(+) 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" } ] } From 87bdcc3b78298c801a74cff9952b14a352f50af1 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 16 Jan 2025 13:29:22 -0500 Subject: [PATCH 38/39] Update provider links to buttons (#34123) * Update provider links to buttons * Use va-button and fix tests * Try fix cypress tests --- .../ProviderSelect.jsx | 61 +++++++------ .../ProviderSortVariant.unit.spec.js | 56 ++++++------ .../index.unit.spec.js | 85 ++++++++++++++----- .../components/ContactInfoPage.unit.spec.js | 2 +- .../CommunityCarePreferencesPageObject.js | 5 +- 5 files changed, 129 insertions(+), 80 deletions(-) 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/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; From 68031f8c10e54bc4088230af1946c61c40818bc8 Mon Sep 17 00:00:00 2001 From: wafimohamed <158514667+wafimohamed@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:29:37 -0500 Subject: [PATCH 39/39] fixed issue with dispaly cards for each month enrollment (#34128) --- .../verify-your-enrollment/components/DGIBEnrollmentCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/verify-your-enrollment/components/DGIBEnrollmentCard.jsx b/src/applications/verify-your-enrollment/components/DGIBEnrollmentCard.jsx index e05819423739..0fc0af8f9c0e 100644 --- a/src/applications/verify-your-enrollment/components/DGIBEnrollmentCard.jsx +++ b/src/applications/verify-your-enrollment/components/DGIBEnrollmentCard.jsx @@ -47,7 +47,7 @@ const DGIBEnrollmentCard = ({ } return (
- {isVerificationEndDateValid(enrollment.verificationEndDate) && + {isVerificationEndDateValid(enrollment[0].verificationEndDate) && !enrollment[0].verificationMethod && ( <>