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 01/36] 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 02/36] 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 03/36] 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 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 && ( <>
From ffa2265237d5d87ec89a60c29b161353251e37a0 Mon Sep 17 00:00:00 2001 From: Jerek Shoemaker Date: Thu, 16 Jan 2025 13:36:44 -0600 Subject: [PATCH 09/36] [CST] Remove cstUseLighthouse5103 code (#34080) * Removing cstUseLighthouse5103 code * Updating tests to mock the LH 5103 endpoint instead of the EVSS one --- .../claims-status/actions/index.js | 49 ---- .../Default5103EvidenceNotice.jsx | 23 +- .../claims-status/containers/AskVAPage.jsx | 30 +- .../claims-status/selectors/index.js | 29 -- .../tests/actions/index.unit.spec.js | 210 ------------- .../tests/components/AskVAPage.unit.spec.jsx | 49 +--- .../tests/components/ClaimPage.unit.spec.jsx | 4 + .../Default5103EvidenceNotice.unit.spec.jsx | 107 +------ ...laim-letters-keyboard-only.cypress.spec.js | 13 +- .../feature-toggle-5103-update-disabled.json | 15 + ...feature-toggle-5103-update-enabled-v2.json | 12 - .../feature-toggle-5103-update-enabled.json | 12 - ...eature-toggle-claim-detail-v2-enabled.json | 12 - .../feature-toggle-claim-phases-enabled.json | 12 - .../lighthouse/feature-toggle-enabled.json | 19 -- .../e2e/page-objects/TrackClaimsPageV2.js | 2 +- .../tests/selectors/index.unit.spec.js | 276 ------------------ 17 files changed, 46 insertions(+), 828 deletions(-) create mode 100644 src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-disabled.json delete mode 100644 src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-enabled.json diff --git a/src/applications/claims-status/actions/index.js b/src/applications/claims-status/actions/index.js index 884a2c4a15b0..587e33934a0a 100644 --- a/src/applications/claims-status/actions/index.js +++ b/src/applications/claims-status/actions/index.js @@ -225,55 +225,6 @@ export const getClaim = (id, navigate) => { export const clearClaim = () => ({ type: CLEAR_CLAIM_DETAIL }); -export function submitRequest(id, cstClaimPhasesEnabled = false) { - return dispatch => { - dispatch({ - type: SUBMIT_DECISION_REQUEST, - }); - - if (canUseMocks()) { - dispatch({ type: SET_DECISION_REQUESTED }); - dispatch( - setNotification({ - title: 'Request received', - body: - 'Thank you. We have your claim request and will make a decision.', - }), - ); - return Promise.resolve(); - } - - return makeAuthRequest( - `/v0/evss_claims/${id}/request_decision`, - { method: 'POST' }, - dispatch, - () => { - dispatch({ type: SET_DECISION_REQUESTED }); - if (cstClaimPhasesEnabled) { - dispatch( - setNotification({ - title: 'We received your evidence waiver', - body: - 'Thank you. We’ll move your claim to the next step as soon as possible.', - }), - ); - } else { - dispatch( - setNotification({ - title: 'Request received', - body: - 'Thank you. We have your claim request and will make a decision.', - }), - ); - } - }, - error => { - dispatch({ type: SET_DECISION_REQUEST_ERROR, error }); - }, - ); - }; -} - export function submit5103(id, trackedItemId, cstClaimPhasesEnabled = false) { return dispatch => { dispatch({ diff --git a/src/applications/claims-status/components/claim-document-request-pages/Default5103EvidenceNotice.jsx b/src/applications/claims-status/components/claim-document-request-pages/Default5103EvidenceNotice.jsx index 6749213f0fe6..9ca90e623dfe 100644 --- a/src/applications/claims-status/components/claim-document-request-pages/Default5103EvidenceNotice.jsx +++ b/src/applications/claims-status/components/claim-document-request-pages/Default5103EvidenceNotice.jsx @@ -15,12 +15,8 @@ import { import { // START ligthouse_migration submit5103 as submit5103Action, - submitRequest as submitRequestAction, // END lighthouse_migration } from '../../actions'; -// START lighthouse_migration -import { cstUseLighthouse } from '../../selectors'; -// END lighthouse_migration import { setUpPage } from '../../utils/page'; import withRouter from '../../utils/withRouter'; @@ -33,8 +29,6 @@ function Default5103EvidenceNotice({ navigate, params, submit5103, - submitRequest, - useLighthouse5103, }) { const [addedEvidence, setAddedEvidence] = useState(false); const [checkboxErrorMessage, setCheckboxErrorMessage] = useState(undefined); @@ -58,11 +52,7 @@ function Default5103EvidenceNotice({ const submit = () => { if (addedEvidence) { - if (useLighthouse5103) { - submit5103(params.id, params.trackedItemId, true); - } else { - submitRequest(params.id, true); - } + submit5103(params.id, params.trackedItemId, true); } else { setCheckboxErrorMessage( `You must confirm you’re done adding evidence before submitting the evidence waiver`, @@ -138,6 +128,7 @@ function Default5103EvidenceNotice({ review stage as quickly as possible.

+ {' '} Note: You can add evidence to support your claim at any time. However, if you add evidence later, your claim will move back to this step, so we encourage you to add all your evidence now. @@ -177,17 +168,11 @@ function mapStateToProps(state) { decisionRequested: claimsState.claimAsk.decisionRequested, decisionRequestError: claimsState.claimAsk.decisionRequestError, loadingDecisionRequest: claimsState.claimAsk.loadingDecisionRequest, - // START lighthouse_migration - useLighthouse5103: cstUseLighthouse(state, '5103'), - // END lighthouse_migration }; } const mapDispatchToProps = { - // START lighthouse_migration submit5103: submit5103Action, - submitRequest: submitRequestAction, - // END lighthouse_migration }; export default withRouter( @@ -204,11 +189,7 @@ Default5103EvidenceNotice.propTypes = { loadingDecisionRequest: PropTypes.bool, navigate: PropTypes.func, params: PropTypes.object, - // START lighthouse_migration submit5103: PropTypes.func, - submitRequest: PropTypes.func, - useLighthouse5103: PropTypes.bool, - // END lighthouse_migration }; export { Default5103EvidenceNotice }; diff --git a/src/applications/claims-status/containers/AskVAPage.jsx b/src/applications/claims-status/containers/AskVAPage.jsx index 9c08481576a0..4706761117e5 100644 --- a/src/applications/claims-status/containers/AskVAPage.jsx +++ b/src/applications/claims-status/containers/AskVAPage.jsx @@ -1,23 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; + import { VaCheckbox } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { - // START ligthouse_migration - submit5103 as submit5103Action, - submitRequest as submitRequestAction, - // END lighthouse_migration getClaim as getClaimAction, + submit5103 as submit5103Action, } from '../actions'; -import NeedHelp from '../components/NeedHelp'; import ClaimsBreadcrumbs from '../components/ClaimsBreadcrumbs'; -// START lighthouse_migration -import { cstUseLighthouse } from '../selectors'; -// END lighthouse_migration +import NeedHelp from '../components/NeedHelp'; +import { setDocumentTitle } from '../utils/helpers'; import { setUpPage } from '../utils/page'; import withRouter from '../utils/withRouter'; -import { setDocumentTitle } from '../utils/helpers'; class AskVAPage extends React.Component { constructor() { @@ -54,11 +49,8 @@ class AskVAPage extends React.Component { decisionRequestError, params, submit5103, - submitRequest, - useLighthouse5103, } = this.props; - const submitFunc = useLighthouse5103 ? submit5103 : submitRequest; const submitDisabled = !this.state.submittedDocs || loadingDecisionRequest || @@ -127,7 +119,7 @@ class AskVAPage extends React.Component { submit class="button-primary vads-u-margin-top--1" text={buttonMsg} - onClick={() => submitFunc(params.id)} + onClick={() => submit5103(params.id)} /> {!loadingDecisionRequest ? ( toggleValues(state).loading; export const showClaimLettersFeature = state => toggleValues(state)[FEATURE_FLAG_NAMES.claimLettersAccess]; -// 'cst_use_lighthouse' -// endpoint - one of '5103', 'index', 'show' -export const cstUseLighthouse = (state, endpoint) => { - const flipperOverrideMode = sessionStorage.getItem('cstFlipperOverrideMode'); - if (flipperOverrideMode) { - switch (flipperOverrideMode) { - case 'featureToggle': - break; - case 'evss': - return false; - case 'lighthouse': - return true; - default: - break; - } - } - - // Returning true here because the feature toggle sometimes returns - // undefined and the feature toggle should always return true anyways - // Note: Checking for window.Cypress here because some of the Cypress - // tests are written for EVSS and will fail if this only returns true - - if (endpoint === 'show' && !window.Cypress) return true; - - return toggleValues(state)[ - FEATURE_FLAG_NAMES[`cstUseLighthouse#${endpoint}`] - ]; -}; - // 'cst_include_ddl_boa_letters' export const cstIncludeDdlBoaLetters = state => toggleValues(state)[FEATURE_FLAG_NAMES.cstIncludeDdlBoaLetters]; diff --git a/src/applications/claims-status/tests/actions/index.unit.spec.js b/src/applications/claims-status/tests/actions/index.unit.spec.js index 71ba09ab9f41..0a4180ea02ff 100644 --- a/src/applications/claims-status/tests/actions/index.unit.spec.js +++ b/src/applications/claims-status/tests/actions/index.unit.spec.js @@ -21,7 +21,6 @@ import { setFieldsDirty, setLastPage, setNotification, - submitRequest, updateField, submit5103, } from '../../actions'; @@ -445,215 +444,6 @@ describe('Actions', () => { global.window.dataLayer = oldDataLayer; }); }); - describe('submitRequest', () => { - context('when cstClaimPhasesEnabled is false', () => { - it('should submit request with canUseMocks true', done => { - const useMocksStub = sinon.stub(constants, 'canUseMocks').returns(true); - - const thunk = submitRequest(5); - const dispatch = sinon.spy(); - - thunk(dispatch) - .then(() => { - expect(dispatch.firstCall.args[0].type).to.equal( - SUBMIT_DECISION_REQUEST, - ); - expect(dispatch.secondCall.args[0].type).to.equal( - SET_DECISION_REQUESTED, - ); - }) - .then(() => useMocksStub.restore()) - .then(done, done); - }); - }); - - context('when cstClaimPhasesEnabled is true', () => { - it('should submit request with canUseMocks true', done => { - const useMocksStub = sinon.stub(constants, 'canUseMocks').returns(true); - - const thunk = submitRequest(5, true); - const dispatch = sinon.spy(); - - thunk(dispatch) - .then(() => { - expect(dispatch.firstCall.args[0].type).to.equal( - SUBMIT_DECISION_REQUEST, - ); - expect(dispatch.secondCall.args[0].type).to.equal( - SET_DECISION_REQUESTED, - ); - }) - .then(() => useMocksStub.restore()) - .then(done, done); - }); - }); - - context('', () => { - let expectedUrl; - const server = setupServer(); - - before(() => { - server.listen(); - }); - - beforeEach(() => { - server.events.on('request:start', req => { - expectedUrl = req.url.href; - }); - }); - - afterEach(() => { - server.resetHandlers(); - expectedUrl = undefined; - }); - - after(() => server.close()); - - context('when cstClaimPhasesEnabled is false', () => { - it('should submit request', done => { - const ID = 5; - server.use( - rest.post( - `https://dev-api.va.gov/v0/evss_claims/${ID}/request_decision`, - (req, res, ctx) => - res( - ctx.status(200), - ctx.json({ - // eslint-disable-next-line camelcase - job_id: ID, - }), - ), - ), - ); - - const thunk = submitRequest(ID); - const dispatchSpy = sinon.spy(); - const dispatch = action => { - dispatchSpy(action); - if (dispatchSpy.callCount === 3) { - expect(expectedUrl).to.contain('5/request_decision'); - expect(dispatchSpy.firstCall.args[0]).to.eql({ - type: SUBMIT_DECISION_REQUEST, - }); - expect(dispatchSpy.secondCall.args[0]).to.eql({ - type: SET_DECISION_REQUESTED, - }); - expect(dispatchSpy.thirdCall.args[0].type).to.eql( - SET_NOTIFICATION, - ); - expect(dispatchSpy.thirdCall.args[0].message.title).to.eql( - 'Request received', - ); - expect(dispatchSpy.thirdCall.args[0].message.body).to.eql( - 'Thank you. We have your claim request and will make a decision.', - ); - done(); - } - }; - - thunk(dispatch); - }); - it('should fail on error', done => { - const ID = 5; - server.use( - rest.post( - `https://dev-api.va.gov/v0/evss_claims/${ID}/request_decision`, - (req, res, ctx) => - res(ctx.status(400), ctx.json({ status: 400 })), - ), - ); - const thunk = submitRequest(ID); - const dispatchSpy = sinon.spy(); - const dispatch = action => { - dispatchSpy(action); - if (dispatchSpy.callCount === 2) { - expect(dispatchSpy.firstCall.args[0]).to.eql({ - type: SUBMIT_DECISION_REQUEST, - }); - expect(dispatchSpy.secondCall.args[0].type).to.eql( - SET_DECISION_REQUEST_ERROR, - ); - done(); - } - }; - - thunk(dispatch); - }); - }); - - context('when cstClaimPhasesEnabled is true', () => { - it('should submit request', done => { - const ID = 5; - server.use( - rest.post( - `https://dev-api.va.gov/v0/evss_claims/${ID}/request_decision`, - (req, res, ctx) => - res( - ctx.status(200), - ctx.json({ - // eslint-disable-next-line camelcase - job_id: ID, - }), - ), - ), - ); - - const thunk = submitRequest(ID, true); - const dispatchSpy = sinon.spy(); - const dispatch = action => { - dispatchSpy(action); - if (dispatchSpy.callCount === 3) { - expect(expectedUrl).to.contain('5/request_decision'); - expect(dispatchSpy.firstCall.args[0]).to.eql({ - type: SUBMIT_DECISION_REQUEST, - }); - expect(dispatchSpy.secondCall.args[0]).to.eql({ - type: SET_DECISION_REQUESTED, - }); - expect(dispatchSpy.thirdCall.args[0].type).to.eql( - SET_NOTIFICATION, - ); - expect(dispatchSpy.thirdCall.args[0].message.title).to.eql( - 'We received your evidence waiver', - ); - expect(dispatchSpy.thirdCall.args[0].message.body).to.eql( - 'Thank you. We’ll move your claim to the next step as soon as possible.', - ); - done(); - } - }; - - thunk(dispatch); - }); - it('should fail on error', done => { - const ID = 5; - server.use( - rest.post( - `https://dev-api.va.gov/v0/evss_claims/${ID}/request_decision`, - (req, res, ctx) => - res(ctx.status(400), ctx.json({ status: 400 })), - ), - ); - const thunk = submitRequest(ID); - const dispatchSpy = sinon.spy(); - const dispatch = action => { - dispatchSpy(action); - if (dispatchSpy.callCount === 2) { - expect(dispatchSpy.firstCall.args[0]).to.eql({ - type: SUBMIT_DECISION_REQUEST, - }); - expect(dispatchSpy.secondCall.args[0].type).to.eql( - SET_DECISION_REQUEST_ERROR, - ); - done(); - } - }; - - thunk(dispatch); - }); - }); - }); - }); describe('getStemClaims', () => { const server = setupServer(); diff --git a/src/applications/claims-status/tests/components/AskVAPage.unit.spec.jsx b/src/applications/claims-status/tests/components/AskVAPage.unit.spec.jsx index 4cc453f822e6..4e355e9a321b 100644 --- a/src/applications/claims-status/tests/components/AskVAPage.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/AskVAPage.unit.spec.jsx @@ -37,13 +37,11 @@ describe('', () => { it('should render enabled submit button when va-checkbox checked', () => { const router = getRouter(); - const submitRequest = sinon.spy(); const { container, rerender } = renderWithRouter( , ); @@ -60,7 +58,6 @@ describe('', () => { , ); @@ -73,14 +70,12 @@ describe('', () => { const router = { push: sinon.spy(), }; - const submitRequest = sinon.spy(); const { container } = renderWithRouter( , ); @@ -93,15 +88,10 @@ describe('', () => { it('should update claims and redirect after success', () => { const navigate = sinon.spy(); - const submitRequest = sinon.spy(); const getClaim = sinon.spy(); const tree = SkinDeep.shallowRender( - , + , ); tree.getMountedInstance().UNSAFE_componentWillReceiveProps({ decisionRequested: true, @@ -111,8 +101,7 @@ describe('', () => { expect(navigate.calledWith('../status')).to.be.true; }); - // START lighthouse_migration - context('cst_use_lighthouse_5103 feature toggle', () => { + context('5103 Submission', () => { const params = { id: 1 }; const props = { @@ -121,38 +110,8 @@ describe('', () => { router: getRouter(), }; - it('calls submitRequest when disabled', () => { - props.submitRequest = sinon.spy(); - props.submit5103 = sinon.spy(); - props.useLighthouse5103 = false; - - const { container, rerender } = renderWithRouter( - - - , - ); - // Check the checkbox - $('va-checkbox', container).__events.vaChange({ - detail: { checked: true }, - }); - - rerenderWithRouter( - rerender, - - - , - ); - // Click submit button - fireEvent.click($('.button-primary', container)); - - expect(props.submitRequest.called).to.be.true; - expect(props.submit5103.called).to.be.false; - }); - - it('calls submit5103 when enabled', () => { - props.submitRequest = sinon.spy(); + it('calls submit5103 ', () => { props.submit5103 = sinon.spy(); - props.useLighthouse5103 = true; const { container, rerender } = renderWithRouter( @@ -173,9 +132,7 @@ describe('', () => { // Click submit button fireEvent.click($('.button-primary', container)); - expect(props.submitRequest.called).to.be.false; expect(props.submit5103.called).to.be.true; }); }); - // END lighthouse_migration }); diff --git a/src/applications/claims-status/tests/components/ClaimPage.unit.spec.jsx b/src/applications/claims-status/tests/components/ClaimPage.unit.spec.jsx index beec70264e4b..596d1862648b 100644 --- a/src/applications/claims-status/tests/components/ClaimPage.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/ClaimPage.unit.spec.jsx @@ -8,6 +8,7 @@ import { renderWithRouter } from '../utils'; const params = { id: 1 }; const props = { + clearClaim: () => {}, params, }; @@ -24,13 +25,16 @@ describe('', () => { expect(props.getClaim.called).to.be.true; }); + it('calls clearClaim when it unmounts', () => { props.clearClaim = sinon.spy(); + const { unmount } = renderWithRouter(

, ); + unmount(); expect(props.clearClaim.called).to.be.true; }); diff --git a/src/applications/claims-status/tests/components/claim-document-request-pages/Default5103EvidenceNotice.unit.spec.jsx b/src/applications/claims-status/tests/components/claim-document-request-pages/Default5103EvidenceNotice.unit.spec.jsx index 9eafc5808a16..0fa68daab417 100644 --- a/src/applications/claims-status/tests/components/claim-document-request-pages/Default5103EvidenceNotice.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/claim-document-request-pages/Default5103EvidenceNotice.unit.spec.jsx @@ -80,6 +80,7 @@ describe('', () => { ); expect($('#default-5103-notice-page', container)).to.not.exist; }); + it('link has the correct href to upload additional evidence', () => { const { getByText } = renderWithRouter( ', () => { ); }); - context('when useLighthouse5103 false', () => { - const props = { - item: automated5103, - params: { id: claimId }, - useLighthouse5103: false, - }; - - context('when checkbox is checked and submit button clicked', () => { - it('should submitRequest notice and redirect to files tab', () => { - const submitRequest = sinon.spy(); - const submit5103 = sinon.spy(); - const navigate = sinon.spy(); - - const { container, rerender } = renderWithRouter( - , - ); - - expect($('#default-5103-notice-page', container)).to.exist; - expect($('va-checkbox', container)).to.exist; - expect($('va-button', container)).to.exist; - - // Check the checkbox - $('va-checkbox', container).__events.vaChange({ - detail: { checked: true }, - }); - - rerenderWithRouter( - rerender, - , - ); - - // Click submit button - fireEvent.click($('#submit', container)); - - expect(submitRequest.called).to.be.true; - expect(submit5103.called).to.be.false; - expect(navigate.calledWith('../files')).to.be.true; - }); - }); - context('when checkbox is not checked and submit button clicked', () => { - it('should not submit 5103 notice and error message displayed', () => { - const submitRequest = sinon.spy(); - const submit5103 = sinon.spy(); - const navigate = sinon.spy(); - - const { container } = renderWithRouter( - , - ); - expect($('#default-5103-notice-page', container)).to.exist; - expect($('va-checkbox', container)).to.exist; - expect($('va-button', container)).to.exist; - expect($('va-checkbox', container).getAttribute('error')).to.be.null; - - // Click submit button - fireEvent.click($('#submit', container)); - - expect($('va-checkbox', container).getAttribute('checked')).to.equal( - 'false', - ); - expect($('va-checkbox', container).getAttribute('required')).to.equal( - 'true', - ); - expect(submitRequest.called).to.be.false; - expect(submit5103.called).to.be.false; - expect(navigate.calledWith('../files')).to.be.false; - expect($('va-checkbox', container).getAttribute('error')).to.equal( - 'You must confirm you’re done adding evidence before submitting the evidence waiver', - ); - }); - }); - }); - - context('when useLighthouse5103 true', () => { + context('submit5103', () => { const props = { item: automated5103, params: { id: claimId }, @@ -194,15 +104,13 @@ describe('', () => { context('when checkbox is checked and submit button clicked', () => { it('should submit5103 notice and redirect to files tab', () => { - const submitRequest = sinon.spy(); - const submit5103 = sinon.spy(); const navigate = sinon.spy(); + const submit5103 = sinon.spy(); const { container, rerender } = renderWithRouter( ', () => { ', () => { // Click submit button fireEvent.click($('#submit', container)); - expect(submitRequest.called).to.be.false; expect(submit5103.called).to.be.true; expect(navigate.calledWith('../files')).to.be.true; }); }); + context('when checkbox is not checked and submit button clicked', () => { it('should not submit 5103 notice and error message displayed', () => { - const submitRequest = sinon.spy(); - const submit5103 = sinon.spy(); const navigate = sinon.spy(); + const submit5103 = sinon.spy(); const { container } = renderWithRouter( , ); + expect($('#default-5103-notice-page', container)).to.exist; expect($('va-checkbox', container)).to.exist; expect($('va-button', container)).to.exist; @@ -262,7 +168,6 @@ describe('', () => { expect($('va-checkbox', container).getAttribute('checked')).to.equal( 'false', ); - expect(submitRequest.called).to.be.false; expect(submit5103.called).to.be.false; expect(navigate.calledWith('../files')).to.be.false; expect($('va-checkbox', container).getAttribute('error')).to.equal( diff --git a/src/applications/claims-status/tests/e2e/08.claim-letters-keyboard-only.cypress.spec.js b/src/applications/claims-status/tests/e2e/08.claim-letters-keyboard-only.cypress.spec.js index da5bca2cb30b..10e6d06fd94d 100644 --- a/src/applications/claims-status/tests/e2e/08.claim-letters-keyboard-only.cypress.spec.js +++ b/src/applications/claims-status/tests/e2e/08.claim-letters-keyboard-only.cypress.spec.js @@ -1,4 +1,3 @@ -import featureToggleEnabled from './fixtures/mocks/claim-letters/feature-toggle-enabled.json'; import claimLetters from './fixtures/mocks/claim-letters/list.json'; describe('Claim Letters Page', () => { @@ -6,9 +5,15 @@ describe('Claim Letters Page', () => { cy.intercept('GET', '/v0/claim_letters', claimLetters.data).as( 'claimLetters', ); - cy.intercept('GET', '/v0/feature_toggles?*', featureToggleEnabled).as( - 'featureToggleEnabled', - ); + + cy.intercept('GET', '/v0/feature_toggles*', { + data: { + features: [ + { name: 'cst_include_ddl_boa_letters', value: true }, + { name: 'claim_letters_access', value: true }, + ], + }, + }); cy.login(); }); diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-disabled.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-disabled.json new file mode 100644 index 000000000000..3637dcd01648 --- /dev/null +++ b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-disabled.json @@ -0,0 +1,15 @@ +{ + "data": { + "type": "feature_toggles", + "features": [ + { + "name": "cst_claim_phases", + "value": true + }, + { + "name": "cst_5103_update_enabled", + "value": false + } + ] + } +} diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled-v2.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled-v2.json index 2f7f386eb1b2..46e07b419285 100644 --- a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled-v2.json +++ b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled-v2.json @@ -2,18 +2,6 @@ "data": { "type": "feature_toggles", "features": [ - { - "name": "cst_use_lighthouse_5103", - "value": false - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - }, { "name": "cst_claim_phases", "value": true diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled.json index 2f7f386eb1b2..46e07b419285 100644 --- a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled.json +++ b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-5103-update-enabled.json @@ -2,18 +2,6 @@ "data": { "type": "feature_toggles", "features": [ - { - "name": "cst_use_lighthouse_5103", - "value": false - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - }, { "name": "cst_claim_phases", "value": true diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-detail-v2-enabled.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-detail-v2-enabled.json index 35c474035f0c..11a3a0ef5ab9 100644 --- a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-detail-v2-enabled.json +++ b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-detail-v2-enabled.json @@ -2,18 +2,6 @@ "data": { "type": "feature_toggles", "features": [ - { - "name": "cst_use_lighthouse_5103", - "value": false - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - }, { "name": "cst_use_claim_details_v2", "value": true diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-phases-enabled.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-phases-enabled.json index ee2d10685ff2..85b6e0fca419 100644 --- a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-phases-enabled.json +++ b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-claim-phases-enabled.json @@ -2,18 +2,6 @@ "data": { "type": "feature_toggles", "features": [ - { - "name": "cst_use_lighthouse_5103", - "value": false - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - }, { "name": "cst_use_claim_details_v2", "value": true diff --git a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-enabled.json b/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-enabled.json deleted file mode 100644 index 5e1858427082..000000000000 --- a/src/applications/claims-status/tests/e2e/fixtures/mocks/lighthouse/feature-toggle-enabled.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": { - "type": "feature_toggles", - "features": [ - { - "name": "cst_use_lighthouse_5103", - "value": true - }, - { - "name": "cst_use_lighthouse_index", - "value": true - }, - { - "name": "cst_use_lighthouse_show", - "value": true - } - ] - } -} diff --git a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js index 5e85eafa7277..96d14c3f9c51 100644 --- a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js +++ b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js @@ -19,7 +19,7 @@ class TrackClaimsPageV2 { cst5103UpdateEnabledV2 = false, ) { if (submitForm) { - cy.intercept('POST', `/v0/evss_claims/189685/request_decision`, { + cy.intercept('POST', `/v0/benefits_claims/189685/submit5103`, { body: {}, }).as('askVA'); } diff --git a/src/applications/claims-status/tests/selectors/index.unit.spec.js b/src/applications/claims-status/tests/selectors/index.unit.spec.js index 6276ee452d7e..cc7c3a20193d 100644 --- a/src/applications/claims-status/tests/selectors/index.unit.spec.js +++ b/src/applications/claims-status/tests/selectors/index.unit.spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; -import FEATURE_FLAG_NAMES from '@department-of-veterans-affairs/platform-utilities/featureFlagNames'; import backendServices from '@department-of-veterans-affairs/platform-user/profile/backendServices'; import * as selectors from '../../selectors'; @@ -46,281 +45,6 @@ describe('selectors', () => { }); }); - describe('cstUseLighthouse', () => { - context('when endpoint is show', () => { - const endpoint = 'show'; - const cstUseLighthouseShow = - FEATURE_FLAG_NAMES[`cstUseLighthouse#${endpoint}`]; - context('when featureToggles are true', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: true, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: true, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return true when window.cypress false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - window.Cypress = false; - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - - it('should return true when window.cypress true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - window.Cypress = true; - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return true when window.cypress false', () => { - // sessionStorage.setItem('cstFlipperOverrideMode', ''); - window.Cypress = false; - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - - it('should return true when window.cypress true', () => { - // sessionStorage.setItem('cstFlipperOverrideMode', ''); - window.Cypress = true; - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - }); - - context('when featureToggles are false', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: false, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: false, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return true when window.cypress false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - window.Cypress = false; - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - - it('should return true when window.cypress true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - window.Cypress = true; - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return true when window.cypress false', () => { - // sessionStorage.setItem('cstFlipperOverrideMode', ''); - window.Cypress = false; - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - - it('should return true when window.cypress true', () => { - // sessionStorage.setItem('cstFlipperOverrideMode', ''); - window.Cypress = true; - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - }); - }); - - context('when endpoint is index', () => { - const endpoint = 'index'; - const cstUseLighthouseShow = - FEATURE_FLAG_NAMES[`cstUseLighthouse#${endpoint}`]; - context('when featureToggles are true', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: true, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: true, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return true ', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return true', () => { - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - }); - - context('when featureToggles are false', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: false, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: false, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return false', () => { - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - }); - }); - - context('when endpoint is 5103', () => { - const endpoint = '5103'; - const cstUseLighthouseShow = - FEATURE_FLAG_NAMES[`cstUseLighthouse#${endpoint}`]; - context('when featureToggles are true', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: true, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: true, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return true ', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return true', () => { - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - }); - - context('when featureToggles are false', () => { - const state = { - featureToggles: { - [cstUseLighthouseShow]: false, - // eslint-disable-next-line camelcase - cst_use_lighthouse_5103: false, - }, - }; - context('when cstFlipperOverrideMode is set to featureToggle', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'featureToggle'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to evss', () => { - it('should return false', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'evss'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - - context('when cstFlipperOverrideMode is set to lighthouse', () => { - it('should return true', () => { - sessionStorage.setItem('cstFlipperOverrideMode', 'lighthouse'); - - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.true; - }); - }); - - context('when cstFlipperOverrideMode is null', () => { - it('should return false', () => { - expect(selectors.cstUseLighthouse(state, endpoint)).to.be.false; - }); - }); - }); - }); - }); - describe('cstIncludeDdlBoaLetters', () => { const state = { featureToggles: { From dc613b26cfe9b1f4aafad079350cffee9a0b47ee Mon Sep 17 00:00:00 2001 From: Kyle Henson <145150351+khenson-oddball@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:58:55 -0700 Subject: [PATCH 10/36] Add veteran status confirmation integration (#34067) --- .../ProofOfVeteranStatus.jsx | 320 +++++++++++++----- .../ProofOfVeteranStatusNew.jsx | 280 +++++++++++---- .../ProofOfVeteranStatusNew.unit.spec.jsx | 129 ++++++- .../vet-verification-status/index.js | 29 ++ .../personalization/profile/mocks/server.js | 6 + .../MilitaryInformation.unit.spec.jsx | 23 +- .../PeriodOfServiceTypeText.unit.spec.jsx | 12 + .../proof-of-veteran-status.cypress.spec.js | 6 + 8 files changed, 660 insertions(+), 145 deletions(-) create mode 100644 src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js diff --git a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx index e5424c1625e0..0fe58bade12b 100644 --- a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx +++ b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatus.jsx @@ -6,6 +6,8 @@ import { generatePdf } from '~/platform/pdf'; import { focusElement } from '~/platform/utilities/ui'; import { captureError } from '~/platform/user/profile/vap-svc/util/analytics'; import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import { apiRequest } from '~/platform/utilities/api'; import { formatFullName } from '../../../common/helpers'; import { getServiceBranchDisplayName } from '../../helpers'; @@ -23,6 +25,8 @@ const ProofOfVeteranStatus = ({ mockUserAgent, }) => { const [errors, setErrors] = useState([]); + const [data, setData] = useState(null); + const [shouldFocusError, setShouldFocusError] = useState(false); const { first, middle, last, suffix } = userFullName; const userAgent = @@ -69,13 +73,45 @@ const ProofOfVeteranStatus = ({ }, }; + const { TOGGLE_NAMES, useToggleValue } = useFeatureToggle(); + const useLighthouseApi = useToggleValue( + TOGGLE_NAMES.veteranStatusCardUseLighthouseFrontend, + ); + + useEffect(() => { + let isMounted = true; + + const fetchVerificationStatus = async () => { + try { + const path = '/profile/vet_verification_status'; + const response = await apiRequest(path); + if (isMounted) { + setData(response.data); + } + } catch (error) { + if (isMounted) { + setErrors([ + "We're sorry. There's a problem with our system. We can't show your Veteran status card right now. Try again later.", + ]); + captureError(error, { eventName: 'vet-status-fetch-verification' }); + } + } + }; + fetchVerificationStatus(); + + return () => { + isMounted = false; + }; + }, []); + useEffect( () => { - if (errors?.length > 0) { + if (shouldFocusError && errors?.length > 0) { focusElement('.vet-status-pdf-download-error'); + setShouldFocusError(false); } }, - [errors], + [shouldFocusError, errors], ); const createPdf = async () => { @@ -93,6 +129,7 @@ const ProofOfVeteranStatus = ({ "We're sorry. Something went wrong on our end. Please try to download your Veteran status card later.", ]); captureError(error, { eventName: 'vet-status-pdf-download' }); + setShouldFocusError(true); } }; @@ -123,93 +160,216 @@ const ProofOfVeteranStatus = ({ ); }); + const contactInfoElements = data?.attributes?.message?.map(item => { + const contactNumber = `${CONTACTS.DS_LOGON.slice( + 0, + 3, + )}-${CONTACTS.DS_LOGON.slice(3, 6)}-${CONTACTS.DS_LOGON.slice(6)}`; + const startIndex = item.indexOf(contactNumber); + + if (startIndex === -1) { + return item; + } + + const before = item.slice(0, startIndex); + const telephone = item.slice( + startIndex, + startIndex + contactNumber.length + 11, + ); + const after = item.slice(startIndex + telephone.length); + + return ( + <> + {before} + ( + ){after} + + ); + }); + return ( <> -
-

- Proof of Veteran status -

-

- You can use your Veteran status card to get discounts offered to - Veterans at many restaurants, hotels, stores, and other businesses. -

-

- Note: - This card doesn’t entitle you to any VA benefits. -

- - {vetStatusEligibility.confirmed ? ( - <> -
- -
+ {useLighthouseApi ? ( +
+

+ Proof of Veteran status +

+

+ You can use your Veteran status card to get discounts offered to + Veterans at many restaurants, hotels, stores, and other businesses. +

+

+ Note: + This card doesn’t entitle you to any VA benefits. +

- {errors?.length > 0 ? ( -
- - {errors[0]} - + {data?.attributes?.veteranStatus === 'confirmed' ? ( + <> +
+
- ) : null} - -
-
-
- sample proof of veteran status card featuring name, date of birth, disability rating and period of service + + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} + +
+
+
+ sample proof of veteran status card featuring name, date of birth, disability rating and period of service +
-
-
- - You can use our mobile app to get proof of Veteran status. - To get started, download the{' '} - VA: Health and Benefits mobile app. - - } - /> -
- - ) : null} - - {!vetStatusEligibility.confirmed && - vetStatusEligibility.message.length > 0 ? ( - <> -
- - {componentizedMessage.map((message, i) => { - if (i === 0) { - return ( -

- {message} -

- ); +
+ + You can use our mobile app to get proof of Veteran status. + To get started, download the{' '} + VA: Health and Benefits mobile app. + } - return

{message}

; - })} + /> +
+ + ) : null} + + {data?.attributes?.veteranStatus !== 'confirmed' && + data?.attributes?.message.length > 0 ? ( + <> +
+ + {contactInfoElements.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} + + {errors?.length > 0 ? ( +
+ + {errors[0]}
- - ) : null} -
+ ) : null} +
+ ) : ( +
+

+ Proof of Veteran status +

+

+ You can use your Veteran status card to get discounts offered to + Veterans at many restaurants, hotels, stores, and other businesses. +

+

+ Note: + This card doesn’t entitle you to any VA benefits. +

+ + {vetStatusEligibility.confirmed ? ( + <> +
+ +
+ + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} + +
+
+
+ sample proof of veteran status card featuring name, date of birth, disability rating and period of service +
+
+
+
+ + You can use our mobile app to get proof of Veteran status. + To get started, download the{' '} + VA: Health and Benefits mobile app. + + } + /> +
+ + ) : null} + + {!vetStatusEligibility.confirmed && + vetStatusEligibility.message.length > 0 ? ( + <> +
+ + {componentizedMessage.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} +
+ )} ); }; diff --git a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx index 81df2cad97f7..d2533e9c4ba9 100644 --- a/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx +++ b/src/applications/personalization/profile/components/proof-of-veteran-status/ProofOfVeteranStatusNew.jsx @@ -6,6 +6,8 @@ import { generatePdf } from '~/platform/pdf'; import { focusElement } from '~/platform/utilities/ui'; import { captureError } from '~/platform/user/profile/vap-svc/util/analytics'; import { CONTACTS } from '@department-of-veterans-affairs/component-library/contacts'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import { apiRequest } from '~/platform/utilities/api'; import { formatFullName } from '../../../common/helpers'; import { getServiceBranchDisplayName } from '../../helpers'; import ProofOfVeteranStatusCard from './ProofOfVeteranStatusCard/ProofOfVeteranStatusCard'; @@ -24,6 +26,8 @@ const ProofOfVeteranStatusNew = ({ mockUserAgent, }) => { const [errors, setErrors] = useState([]); + const [data, setData] = useState(null); + const [shouldFocusError, setShouldFocusError] = useState(false); const { first, middle, last, suffix } = userFullName; const userAgent = @@ -59,6 +63,8 @@ const ProofOfVeteranStatusNew = ({ serviceHistory.length && formattedFullName ); + const hasConfirmationData = !!(data && data.attributes); + const pdfData = { title: `Veteran status card for ${formattedFullName}`, details: { @@ -86,13 +92,45 @@ const ProofOfVeteranStatusNew = ({ }, }; + const { TOGGLE_NAMES, useToggleValue } = useFeatureToggle(); + const useLighthouseApi = useToggleValue( + TOGGLE_NAMES.veteranStatusCardUseLighthouseFrontend, + ); + + useEffect(() => { + let isMounted = true; + + const fetchVerificationStatus = async () => { + try { + const path = '/profile/vet_verification_status'; + const response = await apiRequest(path); + if (isMounted) { + setData(response.data); + } + } catch (error) { + if (isMounted) { + setErrors([ + "We're sorry. There's a problem with our system. We can't show your Veteran status card right now. Try again later.", + ]); + captureError(error, { eventName: 'vet-status-fetch-verification' }); + } + } + }; + fetchVerificationStatus(); + + return () => { + isMounted = false; + }; + }, []); + useEffect( () => { - if (errors?.length > 0) { + if (shouldFocusError && errors?.length > 0) { focusElement('.vet-status-pdf-download-error'); + setShouldFocusError(false); } }, - [errors], + [shouldFocusError, errors], ); const createPdf = async () => { @@ -140,6 +178,33 @@ const ProofOfVeteranStatusNew = ({ ); }); + const contactInfoElements = data?.attributes?.message?.map(item => { + const contactNumber = `${CONTACTS.DS_LOGON.slice( + 0, + 3, + )}-${CONTACTS.DS_LOGON.slice(3, 6)}-${CONTACTS.DS_LOGON.slice(6)}`; + const startIndex = item.indexOf(contactNumber); + + if (startIndex === -1) { + return item; + } + + const before = item.slice(0, startIndex); + const telephone = item.slice( + startIndex, + startIndex + contactNumber.length + 11, + ); + const after = item.slice(startIndex + telephone.length); + + return ( + <> + {before} + ( + ){after} + + ); + }); + return ( <>
@@ -152,74 +217,165 @@ const ProofOfVeteranStatusNew = ({ {userHasRequiredCardData ? ( <> - {vetStatusEligibility.confirmed ? ( + {!useLighthouseApi ? ( <> - {errors?.length > 0 ? ( -
- - {errors[0]} - -
+ {vetStatusEligibility.confirmed ? ( + <> + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} +
+
+ +
+
+
+ +
+
+ + You can use our mobile app to get proof of Veteran + status. To get started, download the{' '} + VA: Health and Benefits mobile + app. + + } + /> +
+ + ) : null} + {!vetStatusEligibility.confirmed && + vetStatusEligibility.message.length > 0 ? ( + <> +
+ + {componentizedMessage.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ ) : null} -
-
- -
-
-
- -
-
- - You can use our mobile app to get proof of Veteran - status. To get started, download the{' '} - VA: Health and Benefits mobile app. - - } - /> -
) : null} - {!vetStatusEligibility.confirmed && - vetStatusEligibility.message.length > 0 ? ( + {useLighthouseApi && hasConfirmationData ? ( <> -
- - {componentizedMessage.map((message, i) => { - if (i === 0) { - return ( -

- {message} -

- ); - } - return

{message}

; - })} -
-
+ {data?.attributes?.veteranStatus === 'confirmed' ? ( + <> + {errors?.length > 0 ? ( +
+ + {errors[0]} + +
+ ) : null} +
+
+ +
+
+
+ +
+
+ + You can use our mobile app to get proof of Veteran + status. To get started, download the{' '} + VA: Health and Benefits mobile + app. + + } + /> +
+ + ) : null} + + {data?.attributes?.veteranStatus !== 'confirmed' && + data?.attributes?.message.length > 0 ? ( + <> +
+ + {contactInfoElements.map((message, i) => { + if (i === 0) { + return ( +

+ {message} +

+ ); + } + return

{message}

; + })} +
+
+ + ) : null} ) : null} + + {useLighthouseApi && !hasConfirmationData ? ( + +

+ We’re sorry. There’s a problem with our system. We can’t show + your Veteran status card right now. Try again later. +

+
+ ) : null} ) : ( { }); }); + describe('should fetch verification status on render', () => { + let apiRequestStub; + const initialState = createBasicInitialState( + [eligibleServiceHistoryItem], + confirmedEligibility, + true, + ); + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + + it('displays the card successfully', async () => { + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { veteranStatus: 'confirmed' }, + }, + }; + + apiRequestStub.resolves(mockData); + + const view = renderWithProfileReducers(, { + initialState, + }); + + sinon.assert.calledWith( + apiRequestStub, + '/profile/vet_verification_status', + ); + await waitFor(() => { + expect( + view.queryByText( + /Get proof of Veteran status on your mobile device/i, + ), + ).to.exist; + expect( + view.queryByText( + /We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now./, + ), + ).to.not.exist; + }); + }); + + it('displays the returned not confirmed message', async () => { + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'not confirmed', + notConfirmedReason: 'PERSON_NOT_FOUND', + message: problematicEligibility.message, + }, + }, + }; + + apiRequestStub.resolves(mockData); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.queryByText( + /Get proof of Veteran Status on your mobile device/i, + ), + ).to.not.exist; + expect( + view.queryByText( + /We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now./, + ), + ).to.exist; + }); + }); + + it('handles empty API response', async () => { + const mockData = { + data: {}, + }; + apiRequestStub.resolves(mockData); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.queryByText( + 'We’re sorry. There’s a problem with our system. We can’t show your Veteran status card right now. Try again later.', + ), + ).to.exist; + }); + }); + + it('handles API error', async () => { + apiRequestStub.rejects(new Error('API Error')); + const view = renderWithProfileReducers(, { + initialState, + }); + + await waitFor(() => { + expect( + view.getByText( + 'We’re sorry. There’s a problem with our system. We can’t show your Veteran status card right now. Try again later.', + ), + ).to.exist; + }); + }); + }); + describe('when eligible', () => { const initialState = createBasicInitialState( [ diff --git a/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js b/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js new file mode 100644 index 000000000000..ba62fadf4807 --- /dev/null +++ b/src/applications/personalization/profile/mocks/endpoints/vet-verification-status/index.js @@ -0,0 +1,29 @@ +const confirmed = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'confirmed', + }, + }, +}; + +const notConfirmed = { + data: { + id: null, + type: 'veteran_status_confirmations', + attributes: { + veteranStatus: 'not confirmed', + notConfirmedReason: 'PERSON_NOT_FOUND', + message: [ + 'We’re sorry. There’s a problem with your discharge status records. We can’t provide a Veteran status card for you right now.', + 'To fix the problem with your records, call the Defense Manpower Data Center at 800-538-9552 (TTY: 711). They’re open Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.', + ], + }, + }, +}; + +module.exports = { + confirmed, + notConfirmed, +}; diff --git a/src/applications/personalization/profile/mocks/server.js b/src/applications/personalization/profile/mocks/server.js index 9177f471ca1d..fcb081e41bc3 100644 --- a/src/applications/personalization/profile/mocks/server.js +++ b/src/applications/personalization/profile/mocks/server.js @@ -22,6 +22,7 @@ const mockDisabilityCompensations = require('./endpoints/disability-compensation const directDeposits = require('./endpoints/direct-deposits'); const bankAccounts = require('./endpoints/bank-accounts'); const serviceHistory = require('./endpoints/service-history'); +const vetVerificationStatus = require('./endpoints/vet-verification-status'); const fullName = require('./endpoints/full-name'); const { baseUserTransitionAvailabilities, @@ -107,6 +108,7 @@ const responses = { profileShowPrivacyPolicy: true, veteranOnboardingContactInfoFlow: true, veteranStatusCardUseLighthouse: true, + veteranStatusCardUseLighthouseFrontend: true, }), ), secondsOfDelay, @@ -236,6 +238,10 @@ const responses = { // .status(200) // .json(serviceHistory.generateServiceHistoryError('403')); }, + 'GET /v0/profile/vet_verification_status': (_req, res) => { + return res.status(200).json(vetVerificationStatus.confirmed); + // return res.status(200).json(vetVerificationStatus.notConfirmed); + }, 'GET /v0/disability_compensation_form/rating_info': (_req, res) => { // return res.status(200).json(ratingInfo.success.serviceConnected0); return res.status(200).json(ratingInfo.success.serviceConnected40); diff --git a/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx b/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx index 3ac7fdcd0b6d..fcf3bb8ebf91 100644 --- a/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx +++ b/src/applications/personalization/profile/tests/components/military-information/MilitaryInformation.unit.spec.jsx @@ -1,8 +1,8 @@ import React from 'react'; import { expect } from 'chai'; - +import * as api from '~/platform/utilities/api'; +import sinon from 'sinon'; import { renderWithProfileReducers } from '../../unit-test-helpers'; - import MilitaryInformation from '../../../components/military-information/MilitaryInformation'; function createBasicInitialState(toggles = {}) { @@ -61,6 +61,16 @@ function createBasicInitialState(toggles = {}) { describe('MilitaryInformation', () => { let initialState; let view; + let apiRequestStub; + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + describe('when military history exists', () => { it('should render data for each entry of military history', () => { initialState = createBasicInitialState(); @@ -91,6 +101,15 @@ describe('MilitaryInformation', () => { initialState = createBasicInitialState(); initialState.vaProfile.militaryInformation.serviceHistory.serviceHistory[0].branchOfService = null; initialState.vaProfile.militaryInformation.serviceHistory.serviceHistory[1].branchOfService = undefined; + const mockData = { + data: { + id: '', + type: 'veteran_status_confirmations', + attributes: { veteranStatus: 'confirmed' }, + }, + }; + + apiRequestStub.resolves(mockData); view = renderWithProfileReducers(, { initialState, }); diff --git a/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx b/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx index 2cd5b272046a..3b6a2c289116 100644 --- a/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx +++ b/src/applications/personalization/profile/tests/components/military-information/PeriodOfServiceTypeText.unit.spec.jsx @@ -1,5 +1,7 @@ import React from 'react'; import { expect } from 'chai'; +import * as api from '~/platform/utilities/api'; +import sinon from 'sinon'; import { renderWithProfileReducers } from '../../unit-test-helpers'; import MilitaryInformation from '../../../components/military-information/MilitaryInformation'; @@ -58,6 +60,16 @@ function createBasicInitialState(toggles = {}) { } describe('MilitaryInformation - Period of Service Type Text', () => { + let apiRequestStub; + + beforeEach(() => { + apiRequestStub = sinon.stub(api, 'apiRequest'); + }); + + afterEach(() => { + apiRequestStub.restore(); + }); + describe('when military history exists', () => { it('should render periodOfServiceTypeText when present and when periodOfServiceTypeCode is A or V', () => { const initialState = createBasicInitialState(); diff --git a/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js b/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js index 5f007816d5c0..08cb01bf1704 100644 --- a/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js +++ b/src/applications/personalization/profile/tests/e2e/proof-of-veteran-status/proof-of-veteran-status.cypress.spec.js @@ -8,6 +8,10 @@ import { dishonorableDischarge, unknownDischarge, } from '../../../mocks/endpoints/service-history'; +import { + confirmed, + notConfirmed, +} from '../../../mocks/endpoints/vet-verification-status'; import MilitaryInformation from '../military-information/MilitaryInformation'; describe('Proof of Veteran status', () => { @@ -16,6 +20,7 @@ describe('Proof of Veteran status', () => { cy.intercept('GET', '/v0/user', loa3User72); cy.intercept('GET', '/v0/profile/full_name', fullName.success); cy.intercept('GET', '/v0/profile/service_history', airForce); + cy.intercept('GET', '/v0/profile/vet_verification_status', confirmed); }); it('Should display the Proof of Veteran Status component', () => { @@ -31,6 +36,7 @@ const login = ({ dischargeCode }) => { cy.intercept('GET', '/v0/user', loa3User72); cy.intercept('GET', '/v0/profile/full_name', fullName.success); cy.intercept('GET', '/v0/profile/service_history', dischargeCode); + cy.intercept('GET', '/v0/profile/vet_verification_status', notConfirmed); }; describe('Veteran is not eligible', () => { From cc6bacea9bdee619c8347cb858d02b0c01ee4e8e Mon Sep 17 00:00:00 2001 From: Ian Magenta <59981318+ianmagenta@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:50:07 -0800 Subject: [PATCH 11/36] [VI-1007] Added Missing buttonContent Assignment (#34134) * added missing buttonContent assignment * fixed unit test ci --------- Co-authored-by: Caitlin <78328496+CaitHawk@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 2 +- src/applications/verify/components/UnifiedVerify.jsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 88c5d6b6196c..e27444d75b09 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -266,7 +266,7 @@ jobs: name: Unit Tests needs: [fetch-allow-lists, tests-prep] timeout-minutes: 30 - runs-on: ubuntu-16-cores-latest + runs-on: ubuntu-16-cores-22.04 outputs: app_folders: ${{ steps.get-changed-apps.outputs.folders }} changed-files: ${{ steps.get-changed-apps.outputs.changed_files }} diff --git a/src/applications/verify/components/UnifiedVerify.jsx b/src/applications/verify/components/UnifiedVerify.jsx index 19c590dff3e4..fbdf69f82322 100644 --- a/src/applications/verify/components/UnifiedVerify.jsx +++ b/src/applications/verify/components/UnifiedVerify.jsx @@ -19,10 +19,12 @@ const Verify = () => { let buttonContent; if (isAuthenticated) { - <> - - - ; + buttonContent = ( + <> + + + + ); } else if (isAuthenticatedOAuth) { // Use the loginServiceName to determine which button to show if (loginServiceName === 'idme') { From f59cfff1d74395ad9da8cd1186e9f0f356dfb996 Mon Sep 17 00:00:00 2001 From: Jami Gibbs Date: Thu, 16 Jan 2025 16:01:59 -0600 Subject: [PATCH 12/36] pin the 16 core runner to ubuntu 22.04 (#34139) From f5b716e4f64e9f5e05009314409941d124d895f3 Mon Sep 17 00:00:00 2001 From: Taras Kurilo Date: Thu, 16 Jan 2025 17:29:14 -0500 Subject: [PATCH 13/36] EDM-451 456 496 SOB Updates (#34133) * edm-451 456 496 sob updates * edm-456 update text --- .../components/UserInfoSection.jsx | 6 ++++- .../containers/StatusPage.jsx | 26 ++++++++++++++----- .../sass/post-911-gib-status.scss | 16 +++++++----- .../components/UserInfoSection.unit.spec.jsx | 25 ++++++++++++++++++ 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/applications/post-911-gib-status/components/UserInfoSection.jsx b/src/applications/post-911-gib-status/components/UserInfoSection.jsx index e30b9e3507d1..21e4d247f3c7 100644 --- a/src/applications/post-911-gib-status/components/UserInfoSection.jsx +++ b/src/applications/post-911-gib-status/components/UserInfoSection.jsx @@ -101,7 +101,11 @@ function UserInfoSection({ enrollmentData = {}, showCurrentAsOfAlert }) { You can print your statement and use it as a replacement for a Certificate of Eligibility (COE) to show that you qualify for - benefits. + benefits. This statement only includes entitlement earned through + your own military service. If you recently transferred entitlement, + it may not be reflected here.

+

+ The Supreme Court’s Rudisill decision may increase your months of + entitlement if you have two or more qualifying periods of active + duty. +

+
); printButton = ( -
+

How can I see my Post-9/11 GI Bill benefit payments?

-
+
If you've received education benefit payments through this program,{' '} @@ -66,10 +77,11 @@ class StatusPage extends React.Component {
-

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

+ Call 888-GI-BILL-1 ( + ). We're here from Monday through Friday, 8:00 a.m to 7:00 p.m + ET. If you have hearing loss, call{' '} + .

diff --git a/src/applications/post-911-gib-status/sass/post-911-gib-status.scss b/src/applications/post-911-gib-status/sass/post-911-gib-status.scss index 21206b6383ce..f28651b3da1a 100644 --- a/src/applications/post-911-gib-status/sass/post-911-gib-status.scss +++ b/src/applications/post-911-gib-status/sass/post-911-gib-status.scss @@ -5,8 +5,8 @@ margin-bottom: 5em; h3 { - margin: 1em 0 .5em 0; - padding: 0 0 .25em 0; + margin-bottom: 8.5px; + padding: 0; } hr { @@ -16,18 +16,20 @@ margin-top: 1em; margin-bottom: 1em; } + h2 { + margin-top: 0; + } .section { margin-bottom: 2em; } .section-line { - margin-bottom: .25em; + margin-bottom: 0.25em; + } + #benefit-level { + margin-top: 2em; } - - /*.usa-alert { - margin: 1em 0 1em 0; - }*/ .not-qualified h5 { margin-top: 1em; diff --git a/src/applications/post-911-gib-status/tests/components/UserInfoSection.unit.spec.jsx b/src/applications/post-911-gib-status/tests/components/UserInfoSection.unit.spec.jsx index 6f111e5aa446..fb00ccb41e83 100644 --- a/src/applications/post-911-gib-status/tests/components/UserInfoSection.unit.spec.jsx +++ b/src/applications/post-911-gib-status/tests/components/UserInfoSection.unit.spec.jsx @@ -136,4 +136,29 @@ describe('', () => { expect(benefitEndDate.text()).to.contain('Since you’re on active duty'); }); }); + describe('date of birth InfoPair', () => { + it('should display the formatted date of birth if present', () => { + const tree = SkinDeep.shallowRender(); + const dobInfoPair = tree + .everySubTree('InfoPair') + .find(pair => pair.props.label === 'Date of birth'); + expect(dobInfoPair).to.exist; + expect(dobInfoPair.props.value).to.equal('November 12, 1995'); + }); + + it('should display "Unavailable" if dateOfBirth is missing', () => { + // Create a copy of props but remove dateOfBirth + const noDobProps = _.merge({}, props, { + enrollmentData: { + dateOfBirth: null, + }, + }); + const tree = SkinDeep.shallowRender(); + const dobInfoPair = tree + .everySubTree('InfoPair') + .find(pair => pair.props.label === 'Date of birth'); + expect(dobInfoPair).to.exist; + expect(dobInfoPair.props.value).to.equal('Unavailable'); + }); + }); }); From 32fcd0b48636fb4ba300f01282e84450ed7442d3 Mon Sep 17 00:00:00 2001 From: Jami Gibbs Date: Thu, 16 Jan 2025 16:54:58 -0600 Subject: [PATCH 14/36] Disable no import linting error (#34143) * disable no import linting error * disable esline error --- .../vaos/referral-appointments/ChooseDateAndTime.jsx | 2 ++ .../vaos/referral-appointments/components/RequestsList.jsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/applications/vaos/referral-appointments/ChooseDateAndTime.jsx b/src/applications/vaos/referral-appointments/ChooseDateAndTime.jsx index 24b172a4758b..10f49b1b9bff 100644 --- a/src/applications/vaos/referral-appointments/ChooseDateAndTime.jsx +++ b/src/applications/vaos/referral-appointments/ChooseDateAndTime.jsx @@ -3,8 +3,10 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { useLocation } from 'react-router-dom'; import ReferralLayout from './components/ReferralLayout'; +// eslint-disable-next-line import/no-restricted-paths import { getUpcomingAppointmentListInfo } from '../appointment-list/redux/selectors'; import { setFormCurrentPage, fetchProviderDetails } from './redux/actions'; +// eslint-disable-next-line import/no-restricted-paths import { fetchFutureAppointments } from '../appointment-list/redux/actions'; import { getProviderInfo } from './redux/selectors'; import { FETCH_STATUS } from '../utils/constants'; diff --git a/src/applications/vaos/referral-appointments/components/RequestsList.jsx b/src/applications/vaos/referral-appointments/components/RequestsList.jsx index d0dac5e7c89a..a88b485a1b71 100644 --- a/src/applications/vaos/referral-appointments/components/RequestsList.jsx +++ b/src/applications/vaos/referral-appointments/components/RequestsList.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import RequestAppointmentLayout from '../../components/RequestAppointmentLayout'; import { APPOINTMENT_STATUS } from '../../utils/constants'; import InfoAlert from '../../components/InfoAlert'; +// eslint-disable-next-line import/no-restricted-paths import ScheduleAppointmentLink from '../../appointment-list/components/ScheduleAppointmentLink'; const RequestList = ({ appointments, requestsError }) => { From e00c7c5765c50003d5a70f5c189bbd01da57a26a Mon Sep 17 00:00:00 2001 From: Simi Adebowale <47654119+simiadebowale@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:04:33 -0600 Subject: [PATCH 15/36] Add required indicator to DateTimeSelectPage heading and corresponding test (#34132) --- .../components/DateTimeSelectPage/index.jsx | 7 ++++- .../DateTimeSelectPage/index.unit.spec.js | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/applications/vaos/new-appointment/components/DateTimeSelectPage/index.jsx b/src/applications/vaos/new-appointment/components/DateTimeSelectPage/index.jsx index 41a46b1243a4..57d8c9334324 100644 --- a/src/applications/vaos/new-appointment/components/DateTimeSelectPage/index.jsx +++ b/src/applications/vaos/new-appointment/components/DateTimeSelectPage/index.jsx @@ -178,7 +178,12 @@ export default function DateTimeSelectPage() { return (
-

{pageTitle}

+

+ {pageTitle} + + (*Required) + +

{!loadingSlots && ( { }); expect(screen.history.push.called).to.be.false; }); + it('should show required text next to page heading', async () => { + const preferredDate = moment(); + const slot308Date = moment().add(6, 'days'); + + setDateTimeSelectMockFetches({ + typeOfCareId: 'outpatientMentalHealth', + slotDatesByClinicId: { + '308': [slot308Date], + }, + }); + + const store = createTestStore(initialState); + + await setTypeOfCare(store, /mental health/i); + await setVAFacility(store, '983', 'outpatientMentalHealth'); + await setClinic(store, '983_308'); + await setPreferredDate(store, preferredDate); + + const screen = renderWithStoreAndRouter( + , + { + store, + }, + ); + + expect(await screen.findByText(/Required/i)).to.exist; + }); }); From 2d7bdb8b5fa312fbf0a584d18dbf1a2c7480620e Mon Sep 17 00:00:00 2001 From: Colin <143013011+cosu419@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:15:13 -0800 Subject: [PATCH 16/36] Appoint a rep - submission method (#34050) * Added feature toggle for appoint v2 features * Moving page depends logic into schema files to clean up form config * Made use of feature toggle for v2 features * Adjusted feature toggle + env checks. Added constant for local testing without vets-api * Created rep submission method screen * Updated routing for to include submission method * Added to submissionmethod page conditional * Added unit tests for submission method * Added return statement * Disabled feature toggle for e2e tests * comment * Corrected error state copy * Added unit test for error state * File + component naming * Updated pageDepends conditional * Make use of new pagedepends in representativeSubmissionMethod --- .../ContactAccreditedRepresentative.jsx | 9 + .../RepresentativeSubmissionMethod.jsx | 95 +++++++++++ .../representative-appoint/config/form.js | 32 ++-- .../constants/enableV2FeaturesLocally.js | 3 + .../representative-appoint/containers/App.jsx | 26 ++- .../hooks/useV2FeatureVisibility.js | 31 ++++ .../representative-appoint/pages/index.js | 2 + .../representativeSubmissionMethod.js | 43 +++++ .../selectAccreditedOrganization.js | 20 ++- .../tests/e2e/navigation/2122.cypress.spec.js | 4 + .../e2e/navigation/2122a.cypress.spec.js | 4 + ...presentativeSubmissionMethod.unit.spec.jsx | 159 ++++++++++++++++++ .../feature-toggles/featureFlagNames.json | 1 + 13 files changed, 396 insertions(+), 33 deletions(-) create mode 100644 src/applications/representative-appoint/components/RepresentativeSubmissionMethod.jsx create mode 100644 src/applications/representative-appoint/constants/enableV2FeaturesLocally.js create mode 100644 src/applications/representative-appoint/hooks/useV2FeatureVisibility.js create mode 100644 src/applications/representative-appoint/pages/representative/representativeSubmissionMethod.js create mode 100644 src/applications/representative-appoint/tests/pages/representative/representativeSubmissionMethod.unit.spec.jsx diff --git a/src/applications/representative-appoint/components/ContactAccreditedRepresentative.jsx b/src/applications/representative-appoint/components/ContactAccreditedRepresentative.jsx index cf3d6507433e..4602b79a158f 100644 --- a/src/applications/representative-appoint/components/ContactAccreditedRepresentative.jsx +++ b/src/applications/representative-appoint/components/ContactAccreditedRepresentative.jsx @@ -4,10 +4,12 @@ import FormNavButtons from 'platform/forms-system/src/js/components/FormNavButto import PropTypes from 'prop-types'; import { useReviewPage } from '../hooks/useReviewPage'; import { getEntityAddressAsObject } from '../utilities/helpers'; +import useV2FeatureToggle from '../hooks/useV2FeatureVisibility'; import AddressEmailPhone from './AddressEmailPhone'; const ContactAccreditedRepresentative = props => { + const v2IsEnabled = useV2FeatureToggle(); const { formData, goBack, goForward, goToPath } = props; const rep = props?.formData?.['view:selectedRepresentative']; const repAttributes = rep?.attributes; @@ -26,6 +28,9 @@ const ContactAccreditedRepresentative = props => { ) && representative.attributes?.accreditedOrganizations?.data?.length > 1; + // will need to update this when we can determine submission methods + const submissionMethodRequired = orgSelectionRequired && v2IsEnabled; + const handleGoBack = () => { if (isReviewPage) { goToPath('/representative-select?review=true'); @@ -36,6 +41,10 @@ const ContactAccreditedRepresentative = props => { const handleGoForward = () => { if (isReviewPage) { + if (submissionMethodRequired) { + goToPath('/representative-submission-method?review=true'); + return; + } if (orgSelectionRequired) { goToPath('/representative-organization?review=true'); } else { diff --git a/src/applications/representative-appoint/components/RepresentativeSubmissionMethod.jsx b/src/applications/representative-appoint/components/RepresentativeSubmissionMethod.jsx new file mode 100644 index 000000000000..8fd82cb1e45b --- /dev/null +++ b/src/applications/representative-appoint/components/RepresentativeSubmissionMethod.jsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { VaRadio } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import FormNavButtons from 'platform/forms-system/src/js/components/FormNavButtons'; +import { scrollToFirstError } from 'platform/utilities/ui'; +import { useReviewPage } from '../hooks/useReviewPage'; + +const RepresentativeSubmissionMethod = props => { + const { formData, setFormData, goBack, goForward, goToPath } = props; + const [error, setError] = useState(null); + + const isReviewPage = useReviewPage(); + + const handleGoBack = () => { + if (isReviewPage) { + goToPath('representative-contact?review=true'); + } else { + goBack(formData); + } + }; + const handleGoForward = () => { + if (!formData?.representativeSubmissionMethod) { + setError('Choose how to submit your request by selecting an option'); + scrollToFirstError({ focusOnAlertRole: true }); + } else if (isReviewPage) { + goToPath('/representative-organization?review=true'); + } else { + goForward(formData); + } + }; + + const handleRadioSelect = e => { + setError(null); + setFormData({ + ...formData, + representativeSubmissionMethod: e.detail.value, + }); + }; + + return ( + <> + + + + + + + + ); +}; + +RepresentativeSubmissionMethod.propTypes = { + formData: PropTypes.object, + goBack: PropTypes.func, + goForward: PropTypes.func, + goToPath: PropTypes.func, + setFormData: PropTypes.func, +}; + +function mapStateToProps(state) { + return { + formData: state.form.data, + }; +} + +export { RepresentativeSubmissionMethod }; // Named export for testing + +export default connect( + mapStateToProps, + null, +)(RepresentativeSubmissionMethod); diff --git a/src/applications/representative-appoint/config/form.js b/src/applications/representative-appoint/config/form.js index 0add5f0207a0..d74bb684f16a 100644 --- a/src/applications/representative-appoint/config/form.js +++ b/src/applications/representative-appoint/config/form.js @@ -1,7 +1,6 @@ import commonDefinitions from 'vets-json-schema/dist/definitions.json'; -// import environment from '@department-of-veterans-affairs/platform-utilities/environment'; -// import profileContactInfo from 'platform/forms-system/src/js/definitions/profileContactInfo'; import FormFooter from 'platform/forms/components/FormFooter'; + import GetFormHelp from '../components/GetFormHelp'; import configService from '../utilities/configService'; import manifest from '../manifest.json'; @@ -33,6 +32,7 @@ import { replaceAccreditedRepresentative, selectedAccreditedOrganizationId, contactAccreditedRepresentative, + representativeSubmissionMethod, } from '../pages'; // import initialData from '../tests/fixtures/data/test-data.json'; @@ -41,6 +41,7 @@ import SelectAccreditedRepresentative from '../components/SelectAccreditedRepres import SelectedAccreditedRepresentativeReview from '../components/SelectAccreditedRepresentativeReview'; import ContactAccreditedRepresentative from '../components/ContactAccreditedRepresentative'; import SelectOrganization from '../components/SelectOrganization'; +import RepresentativeSubmissionMethod from '../components/RepresentativeSubmissionMethod'; import SubmissionError from '../components/SubmissionError'; @@ -136,29 +137,26 @@ const formConfig = { uiSchema: contactAccreditedRepresentative.uiSchema, schema: contactAccreditedRepresentative.schema, }, + RepresentativeSubmissionMethod: { + title: 'Representative Submission Method', + path: 'representative-submission-method', + CustomPage: RepresentativeSubmissionMethod, + depends: formData => + representativeSubmissionMethod.pageDepends(formData), + uiSchema: representativeSubmissionMethod.uiSchema, + schema: representativeSubmissionMethod.schema, + }, selectAccreditedOrganization: { path: 'representative-organization', title: 'Organization Select', hideOnReview: true, CustomPage: SelectOrganization, depends: formData => - !!formData['view:selectedRepresentative'] && - ['representative', 'veteran_service_officer'].includes( - formData['view:selectedRepresentative'].attributes - ?.individualType, - ) && - formData['view:selectedRepresentative'].attributes - ?.accreditedOrganizations?.data?.length > 1, + selectedAccreditedOrganizationId.pageDepends(formData), uiSchema: selectedAccreditedOrganizationId.uiSchema, - schema: { - type: 'object', - properties: { - selectedAccreditedOrganizationId: { - type: 'string', - }, - }, - }, + schema: selectedAccreditedOrganizationId.schema, }, + replaceAccreditedRepresentative: { title: 'Representative Replace', path: 'representative-replace', diff --git a/src/applications/representative-appoint/constants/enableV2FeaturesLocally.js b/src/applications/representative-appoint/constants/enableV2FeaturesLocally.js new file mode 100644 index 000000000000..c5c1800af150 --- /dev/null +++ b/src/applications/representative-appoint/constants/enableV2FeaturesLocally.js @@ -0,0 +1,3 @@ +// toggle this eg when testing PRs locally + +export const enableV2FeaturesForLocalTesting = true; diff --git a/src/applications/representative-appoint/containers/App.jsx b/src/applications/representative-appoint/containers/App.jsx index 0b397ca4d7f4..4d3cd330117d 100644 --- a/src/applications/representative-appoint/containers/App.jsx +++ b/src/applications/representative-appoint/containers/App.jsx @@ -15,6 +15,8 @@ import formConfig from '../config/form'; import configService from '../utilities/configService'; import { getFormSubtitle } from '../utilities/helpers'; +import useV2FeatureToggle from '../hooks/useV2FeatureVisibility'; + function App({ loggedIn, location, children, formData, setFormData }) { const subTitle = getFormSubtitle(formData); @@ -25,6 +27,7 @@ function App({ loggedIn, location, children, formData, setFormData }) { } = useFeatureToggle(); const appIsEnabled = useToggleValue(appToggleKey); + const v2FeatureToggle = useV2FeatureToggle(); const isProduction = window.Cypress || environment.isProduction(); const isAppToggleLoading = useToggleLoadingValue(appToggleKey); @@ -40,6 +43,7 @@ function App({ loggedIn, location, children, formData, setFormData }) { [pathname], ); + // dynamically updates the form subtitle to 21-22 or 21-22A useEffect( () => { configService.setFormConfig({ subTitle }); @@ -50,26 +54,18 @@ function App({ loggedIn, location, children, formData, setFormData }) { useEffect( () => { - const defaultViewFields = { + const updatedFormData = { + ...formData, + v2IsEnabled: v2FeatureToggle, 'view:isLoggedIn': loggedIn, + 'view:representativeQueryInput': '', + 'view:representativeSearchResults': [], }; - setFormData({ - ...formData, - ...defaultViewFields, - }); + setFormData(updatedFormData); }, - [loggedIn], + [v2FeatureToggle, loggedIn], ); - // resetting user query between sessions - useEffect(() => { - setFormData({ - ...formData, - 'view:representativeQueryInput': '', - 'view:representativeSearchResults': [], - }); - }, []); - if (isAppToggleLoading) { return (
diff --git a/src/applications/representative-appoint/hooks/useV2FeatureVisibility.js b/src/applications/representative-appoint/hooks/useV2FeatureVisibility.js new file mode 100644 index 000000000000..e58ffe290db5 --- /dev/null +++ b/src/applications/representative-appoint/hooks/useV2FeatureVisibility.js @@ -0,0 +1,31 @@ +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import environment from 'platform/utilities/environment'; +import { enableV2FeaturesForLocalTesting } from '../constants/enableV2FeaturesLocally'; + +const useV2FeatureToggle = () => { + const { + TOGGLE_NAMES: { appointARepresentativeEnableV2Features: appToggleKey }, + useToggleLoadingValue, + useToggleValue, + } = useFeatureToggle(); + + const appointV2FeaturesEnabled = useToggleValue(appToggleKey); + const toggleIsLoading = useToggleLoadingValue(appToggleKey); + + if (toggleIsLoading) { + return false; + } + + // can remove this after verifying the toggle in staging + if (environment.isProduction() || window.Cypress) { + return false; + } + + if (environment.isLocalhost()) { + return enableV2FeaturesForLocalTesting; + } + + return appointV2FeaturesEnabled; +}; + +export default useV2FeatureToggle; diff --git a/src/applications/representative-appoint/pages/index.js b/src/applications/representative-appoint/pages/index.js index ae5338060abf..dcbc97651470 100644 --- a/src/applications/representative-appoint/pages/index.js +++ b/src/applications/representative-appoint/pages/index.js @@ -20,6 +20,7 @@ import * as selectAccreditedRepresentative from './representative/selectAccredit import * as replaceAccreditedRepresentative from './representative/replaceAccreditedRepresentative'; import * as selectedAccreditedOrganizationId from './representative/selectAccreditedOrganization'; import * as contactAccreditedRepresentative from './representative/contactAccreditedRepresentative'; +import * as representativeSubmissionMethod from './representative/representativeSubmissionMethod'; export { authorizeMedical, @@ -44,4 +45,5 @@ export { replaceAccreditedRepresentative, selectedAccreditedOrganizationId, contactAccreditedRepresentative, + representativeSubmissionMethod, }; diff --git a/src/applications/representative-appoint/pages/representative/representativeSubmissionMethod.js b/src/applications/representative-appoint/pages/representative/representativeSubmissionMethod.js new file mode 100644 index 000000000000..7c4b6ac43c69 --- /dev/null +++ b/src/applications/representative-appoint/pages/representative/representativeSubmissionMethod.js @@ -0,0 +1,43 @@ +import RepresentativeSubmissionMethod from '../../components/RepresentativeSubmissionMethod'; + +export const uiSchema = { + representativeSubmissionMethod: { + 'ui:title': "Select how you'd like to submit your request", + 'ui:widget': RepresentativeSubmissionMethod, + 'ui:options': { + hideLabelText: true, + hideOnReview: true, + }, + 'ui:required': () => true, + }, +}; + +export const schema = { + type: 'object', + properties: { + representativeSubmissionMethod: { + type: 'string', + }, + }, +}; + +export const pageDepends = formData => { + const { v2IsEnabled } = formData; + // temporarily hardcoding these values + const repHasMultipleOrganizations = + !!formData['view:selectedRepresentative'] && + ['representative', 'veteran_service_officer'].includes( + formData['view:selectedRepresentative'].attributes?.individualType, + ) && + formData['view:selectedRepresentative'].attributes?.accreditedOrganizations + ?.data?.length > 1; + const userCanSubmitDigitally = true; + const representativeAcceptsDigitalSubmission = true; + + return ( + v2IsEnabled && + repHasMultipleOrganizations && + userCanSubmitDigitally && + representativeAcceptsDigitalSubmission + ); +}; diff --git a/src/applications/representative-appoint/pages/representative/selectAccreditedOrganization.js b/src/applications/representative-appoint/pages/representative/selectAccreditedOrganization.js index 21ddac36f596..47ef06e7bd84 100644 --- a/src/applications/representative-appoint/pages/representative/selectAccreditedOrganization.js +++ b/src/applications/representative-appoint/pages/representative/selectAccreditedOrganization.js @@ -12,4 +12,22 @@ export const uiSchema = { }, }; -export const schema = {}; +export const schema = { + type: 'object', + properties: { + selectedAccreditedOrganizationId: { + type: 'string', + }, + }, +}; + +export const pageDepends = formData => { + return ( + !!formData['view:selectedRepresentative'] && + ['representative', 'veteran_service_officer'].includes( + formData['view:selectedRepresentative'].attributes?.individualType, + ) && + formData['view:selectedRepresentative'].attributes?.accreditedOrganizations + ?.data?.length > 1 + ); +}; diff --git a/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js b/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js index 420d24a7644a..4312fd9fe670 100644 --- a/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js +++ b/src/applications/representative-appoint/tests/e2e/navigation/2122.cypress.spec.js @@ -17,6 +17,10 @@ describe('Authenticated', () => { data: { features: [ { name: 'appoint_a_representative_enable_frontend', value: true }, + { + name: 'appoint_a_representative_enable_v2_features', + value: false, + }, ], }, }); diff --git a/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js b/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js index 1c6df30fbb0c..c1ec9e80484d 100644 --- a/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js +++ b/src/applications/representative-appoint/tests/e2e/navigation/2122a.cypress.spec.js @@ -18,6 +18,10 @@ describe('Unauthenticated', () => { data: { features: [ { name: 'appoint_a_representative_enable_frontend', value: true }, + { + name: 'appoint_a_representative_enable_v2_features', + value: false, + }, ], }, }); diff --git a/src/applications/representative-appoint/tests/pages/representative/representativeSubmissionMethod.unit.spec.jsx b/src/applications/representative-appoint/tests/pages/representative/representativeSubmissionMethod.unit.spec.jsx new file mode 100644 index 000000000000..8ec8f1784f96 --- /dev/null +++ b/src/applications/representative-appoint/tests/pages/representative/representativeSubmissionMethod.unit.spec.jsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { expect } from 'chai'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import sinon from 'sinon'; +import { RepresentativeSubmissionMethod } from '../../../components/RepresentativeSubmissionMethod'; +import * as reviewPageHook from '../../../hooks/useReviewPage'; + +describe('', () => { + const getProps = () => { + return { + props: { + formData: {}, + goBack: sinon.spy(), + goForward: sinon.spy(), + goToPath: sinon.spy(), + }, + mockStore: { + getState: () => ({}), + subscribe: () => {}, + }, + }; + }; + + const renderContainer = (props, mockStore) => { + return render( + + + , + ); + }; + + it('should render component', () => { + const { props, mockStore } = getProps(); + + const { container } = render( + + + , + ); + expect(container).to.exist; + }); + + it('displays an error', async () => { + const { props, mockStore } = getProps(); + + const { container } = renderContainer(props, mockStore); + + const radioSelector = container.querySelector('va-radio'); + + const continueButton = container.querySelector('.usa-button-primary'); + + fireEvent.click(continueButton); + + await waitFor(() => { + expect(radioSelector).to.have.attr( + 'error', + 'Choose how to submit your request by selecting an option', + ); + }); + }); + + context('non-review mode', () => { + it('should call goBack with formData when handleGoBack is triggered and isReviewPage is false', () => { + const { props, mockStore } = getProps(); + + const useReviewPageStub = sinon + .stub(reviewPageHook, 'call') + .returns(false); + + const { getByText } = renderContainer(props, mockStore); + + fireEvent.click(getByText('Back')); + + expect(props.goBack.calledOnce).to.be.true; + expect(props.goBack.calledWith(props.formData)).to.be.true; + + useReviewPageStub.restore(); + }); + + it('should call goForward with formData when handleGoForward is triggered and isReviewPage is false', () => { + const { props, mockStore } = getProps(); + + const useReviewPageStub = sinon + .stub(reviewPageHook, 'useReviewPage') + .returns(false); + + props.formData.representativeSubmissionMethod = 'mail'; + + const { getByText } = render( + + + , + ); + + fireEvent.click(getByText('Continue')); + + expect(props.goForward.calledOnce).to.be.true; + expect(props.goForward.calledWith(props.formData)).to.be.true; + expect(props.goToPath.called).to.be.false; + + useReviewPageStub.restore(); + }); + }); + + context('review mode', () => { + beforeEach(function() { + Object.defineProperty(window, 'location', { + value: { search: '?review=true' }, + writable: true, + }); + }); + + it('should call goToPath with the correct path when handleGoBack is triggered and isReviewPage is true', () => { + const useReviewPageStub = sinon + .stub(reviewPageHook, 'useReviewPage') + .returns(true); + + const { props, mockStore } = getProps(); + + const { getByText } = renderContainer(props, mockStore); + + fireEvent.click(getByText('Back')); + + expect(props.goBack.called).to.be.false; + expect(props.goToPath.calledOnce).to.be.true; + expect(props.goToPath.calledWith('representative-contact?review=true')).to + .be.true; + + useReviewPageStub.restore(); + }); + + it('should call goToPath with /representative-organization?review=true when handleGoForward is triggered, isReviewPage is true, and isReplacingRep is true', () => { + const { props, mockStore } = getProps(); + + const useReviewPageStub = sinon + .stub(reviewPageHook, 'useReviewPage') + .returns(true); + + props.formData.representativeSubmissionMethod = 'mail'; + + const { getByText } = render( + + + , + ); + + fireEvent.click(getByText('Continue')); + + expect(props.goForward.called).to.be.false; + expect(props.goToPath.calledOnce).to.be.true; + expect( + props.goToPath.calledWith('/representative-organization?review=true'), + ).to.be.true; + + useReviewPageStub.restore(); + }); + }); +}); diff --git a/src/platform/utilities/feature-toggles/featureFlagNames.json b/src/platform/utilities/feature-toggles/featureFlagNames.json index fc321f66f4e1..327eeed7d3fa 100644 --- a/src/platform/utilities/feature-toggles/featureFlagNames.json +++ b/src/platform/utilities/feature-toggles/featureFlagNames.json @@ -5,6 +5,7 @@ "aedpPrefill": "aedp_prefill", "allClaimsAddDisabilitiesEnhancement": "all_claims_add_disabilities_enhancement", "appointARepresentativeEnableFrontend": "appoint_a_representative_enable_frontend", + "appointARepresentativeEnableV2Features": "appoint_a_representative_enable_v2_features", "askVaDashboardFeature": "ask_va_dashboard_feature", "askVaFormFeature": "ask_va_form_feature", "askVaIntroductionPageFeature": "ask_va_introduction_page_feature", From d3abcb7b709ec1f21e1266558697627e93b0f13d Mon Sep 17 00:00:00 2001 From: Brandon Cooper Date: Fri, 17 Jan 2025 09:16:07 -0500 Subject: [PATCH 17/36] fix typo (#34140) --- .../caregivers/components/FormFields/FacilitySearch.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/caregivers/components/FormFields/FacilitySearch.jsx b/src/applications/caregivers/components/FormFields/FacilitySearch.jsx index 025a203c168a..d29e1483ec55 100644 --- a/src/applications/caregivers/components/FormFields/FacilitySearch.jsx +++ b/src/applications/caregivers/components/FormFields/FacilitySearch.jsx @@ -301,7 +301,7 @@ const FacilitySearch = props => {

You’ll need to find and select the VA medical center or clinic where - the Veteran receives or plans to recieve care. + the Veteran receives or plans to receive care.

The VA medical center or clinic may be in a different city, state, or From 96272b8316e06e31ac056e3ea44f8f11f30ad9c8 Mon Sep 17 00:00:00 2001 From: Peri-Ann McLaren <141954992+pmclaren19@users.noreply.github.com> Date: Fri, 17 Jan 2025 07:36:14 -0700 Subject: [PATCH 18/36] remove request by from breadcrumbs, tab title and document request page header (#34138) --- .../claim-document-request-pages/DefaultPage.jsx | 2 +- .../components/DocumentRequestPage.unit.spec.jsx | 6 +++--- .../DefaultPage.unit.spec.jsx | 15 +++++++++------ .../tests/e2e/page-objects/TrackClaimsPage.js | 2 +- .../tests/e2e/page-objects/TrackClaimsPageV2.js | 2 +- .../tests/utils/helpers.unit.spec.js | 4 ++-- src/applications/claims-status/utils/helpers.js | 3 ++- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/applications/claims-status/components/claim-document-request-pages/DefaultPage.jsx b/src/applications/claims-status/components/claim-document-request-pages/DefaultPage.jsx index da9f6e9ae1a4..e22cc22c2632 100644 --- a/src/applications/claims-status/components/claim-document-request-pages/DefaultPage.jsx +++ b/src/applications/claims-status/components/claim-document-request-pages/DefaultPage.jsx @@ -21,7 +21,7 @@ export default function DefaultPage({ }) { return (

-

Request for {item.displayName}

+

{item.displayName}

{item.status === 'NEEDED_FROM_YOU' ? ( ) : null} diff --git a/src/applications/claims-status/tests/components/DocumentRequestPage.unit.spec.jsx b/src/applications/claims-status/tests/components/DocumentRequestPage.unit.spec.jsx index a777678891e4..0b8a3734d2a0 100644 --- a/src/applications/claims-status/tests/components/DocumentRequestPage.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/DocumentRequestPage.unit.spec.jsx @@ -125,10 +125,10 @@ describe('', () => { `../document-request/${trackedItem.id}`, ); expect(breadcrumbs.breadcrumbList[3].label).to.equal( - `Request for ${trackedItem.displayName}`, + trackedItem.displayName, ); expect(document.title).to.equal( - `Request for ${trackedItem.displayName} | Veterans Affairs`, + `${trackedItem.displayName} | Veterans Affairs`, ); }); }); @@ -465,7 +465,7 @@ describe('', () => { , ); - expect(document.title).to.equal('Request for Testing | Veterans Affairs'); + expect(document.title).to.equal('Testing | Veterans Affairs'); expect(resetUploads.called).to.be.true; }); diff --git a/src/applications/claims-status/tests/components/claim-document-request-pages/DefaultPage.unit.spec.jsx b/src/applications/claims-status/tests/components/claim-document-request-pages/DefaultPage.unit.spec.jsx index bc17cec4197b..ae721b033752 100644 --- a/src/applications/claims-status/tests/components/claim-document-request-pages/DefaultPage.unit.spec.jsx +++ b/src/applications/claims-status/tests/components/claim-document-request-pages/DefaultPage.unit.spec.jsx @@ -1,6 +1,5 @@ import React from 'react'; import { expect } from 'chai'; -import moment from 'moment-timezone'; import { $ } from '@department-of-veterans-affairs/platform-forms-system/ui'; @@ -41,6 +40,12 @@ describe('', () => { documents: '[]', date: '2024-03-07', }; + const today = new Date(); + const past = new Date(item.suspenseDate); + const monthsDue = + today.getMonth() - + past.getMonth() + + 12 * (today.getFullYear() - past.getFullYear()); const { getByText, container } = renderWithRouter( , ); @@ -49,12 +54,10 @@ describe('', () => { expect($('.due-date-header', container)).to.exist; const formattedClaimDate = formatDate(item.suspenseDate); getByText( - `Needed from you by ${formattedClaimDate} - Due ${moment( - item.suspenseDate, - ).fromNow()}`, + `Needed from you by ${formattedClaimDate} - Due ${monthsDue} months ago`, ); expect($('.optional-upload', container)).to.not.exist; - getByText('Request for Submit buddy statement(s)'); + getByText('Submit buddy statement(s)'); getByText(scrubDescription(item.description)); expect($('va-additional-info', container)).to.exist; expect($('va-file-input', container)).to.exist; @@ -85,7 +88,7 @@ describe('', () => { getByText( '- We’ve asked others to send this to us, but you may upload it if you have it.', ); - getByText('Request for Submit buddy statement(s)'); + getByText('Submit buddy statement(s)'); getByText(scrubDescription(item.description)); expect($('va-additional-info', container)).to.exist; expect($('va-file-input', container)).to.exist; diff --git a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPage.js b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPage.js index 42c2f0cf3d85..229e4b5e6438 100644 --- a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPage.js +++ b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPage.js @@ -336,7 +336,7 @@ class TrackClaimsPage { } else { cy.get('.usa-breadcrumb__list > li:nth-child(4) a').should( 'contain', - 'Request for Submit Buddy Statement(s)', + 'Submit Buddy Statement(s)', ); } } diff --git a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js index 96d14c3f9c51..307789b936fd 100644 --- a/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js +++ b/src/applications/claims-status/tests/e2e/page-objects/TrackClaimsPageV2.js @@ -660,7 +660,7 @@ class TrackClaimsPageV2 { } else { cy.get('.usa-breadcrumb__list > li:nth-child(4) a').should( 'contain', - 'Request for Submit Buddy Statement(s)', + 'Submit Buddy Statement(s)', ); } } diff --git a/src/applications/claims-status/tests/utils/helpers.unit.spec.js b/src/applications/claims-status/tests/utils/helpers.unit.spec.js index d12c3c3b7588..07ec9782084b 100644 --- a/src/applications/claims-status/tests/utils/helpers.unit.spec.js +++ b/src/applications/claims-status/tests/utils/helpers.unit.spec.js @@ -1196,11 +1196,11 @@ describe('Disability benefits helpers: ', () => { 'Review evidence list (5103 notice)', ); }); - it('should display Request for Submit buddy statement(s)', () => { + it('should display Submit buddy statement(s)', () => { const displayName = 'Submit buddy statement(s)'; const documentRequestPageTitle = setDocumentRequestPageTitle(displayName); - expect(documentRequestPageTitle).to.equal(`Request for ${displayName}`); + expect(documentRequestPageTitle).to.equal(displayName); }); }); diff --git a/src/applications/claims-status/utils/helpers.js b/src/applications/claims-status/utils/helpers.js index 9ed672c0eaf2..b9138949573e 100644 --- a/src/applications/claims-status/utils/helpers.js +++ b/src/applications/claims-status/utils/helpers.js @@ -1190,10 +1190,11 @@ export const generateClaimTitle = (claim, placement, tab) => { }; // Use this function to set the Document Request Page Title, Page Tab and Page Breadcrumb Title +// It is also used to set the Document Request Page breadcrumb text export function setDocumentRequestPageTitle(displayName) { return isAutomated5103Notice(displayName) ? 'Review evidence list (5103 notice)' - : `Request for ${displayName}`; + : displayName; } // Used to set page title for the CST Tabs From fff065779f9296088747f20637c3de89024949a2 Mon Sep 17 00:00:00 2001 From: Kevin Suarez Date: Fri, 17 Jan 2025 09:56:23 -0500 Subject: [PATCH 19/36] 100539 Only hit AC-API if user is verified (#34082) --- .../mhv-landing-page/containers/LandingPageContainer.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/applications/mhv-landing-page/containers/LandingPageContainer.jsx b/src/applications/mhv-landing-page/containers/LandingPageContainer.jsx index b20605be4f21..c38f6212eb96 100644 --- a/src/applications/mhv-landing-page/containers/LandingPageContainer.jsx +++ b/src/applications/mhv-landing-page/containers/LandingPageContainer.jsx @@ -12,6 +12,7 @@ import { } from '../utilities/data'; import { isAuthenticatedWithSSOe, + isLOA3, isVAPatient, selectProfile, signInServiceEnabled, @@ -32,6 +33,7 @@ const LandingPageContainer = () => { const profile = useSelector(selectProfile); const ssoe = useSelector(isAuthenticatedWithSSOe); const useSiS = useSelector(signInServiceEnabled); + const userVerified = useSelector(isLOA3); const registered = useSelector(isVAPatient); const unreadMessageAriaLabel = resolveUnreadMessageAriaLabel( unreadMessageCount, @@ -77,7 +79,7 @@ const LandingPageContainer = () => { useEffect( () => { - if (!profile.loading) { + if (!profile.loading && userVerified) { if (userHasMhvAccount) { dispatch({ type: fetchAccountStatusSuccess, @@ -95,7 +97,7 @@ const LandingPageContainer = () => { } } }, - [userHasMhvAccount, profile.loading, dispatch], + [userHasMhvAccount, profile.loading, userVerified, dispatch], ); if (loading) From 6af03351eafe53b2b382a973c9d2bd9c83a912e4 Mon Sep 17 00:00:00 2001 From: Robert Hasselle <123402053+rhasselle-oddball@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:21:59 -0600 Subject: [PATCH 20/36] 0966 new confirmation page followups (#34101) * 0966 followup changes for confirmation page --- .../21-0966/containers/ConfirmationPage.jsx | 239 +++++++++++++++--- .../containers/ConfirmationPage.unit.spec.jsx | 63 ++++- 2 files changed, 257 insertions(+), 45 deletions(-) diff --git a/src/applications/simple-forms/21-0966/containers/ConfirmationPage.jsx b/src/applications/simple-forms/21-0966/containers/ConfirmationPage.jsx index 1889373e7dba..a9594862dc86 100644 --- a/src/applications/simple-forms/21-0966/containers/ConfirmationPage.jsx +++ b/src/applications/simple-forms/21-0966/containers/ConfirmationPage.jsx @@ -28,6 +28,9 @@ import { } from '../definitions/constants'; import { useNewConfirmationPage } from '../config/features'; +const { COMPENSATION, PENSION } = veteranBenefits; +const { SURVIVOR } = survivingDependentBenefits; + const NextSteps = ({ formData }) => (

What are my next steps?

@@ -37,13 +40,15 @@ const NextSteps = ({ formData }) => (

{confirmationPageNextStepsParagraph(formData)}

) : ( -

- You should complete and file your claims as soon as possible. If you - complete and file your claim before the intent to file expires and we - approve your claim, you may be able to get retroactive payments. - Retroactive payments are payments for the time between when we processed - your intent to file and when we approved your claim. -

+ <> +

You should complete and file your claims as soon as possible.

+

+ If you complete and file your claim before the intent to file expires + and we approve your claim, you may be able to get retroactive + payments. Retroactive payments are payments for the time between when + we processed your intent to file and when we approved your claim. +

+ )}
); @@ -51,7 +56,7 @@ const NextSteps = ({ formData }) => ( const ActionLinksToCompleteClaims = ({ formData }) => ( <> {(hasActiveCompensationITF({ formData }) || - formData.benefitSelection[veteranBenefits.COMPENSATION]) && ( + formData.benefitSelection[COMPENSATION]) && (
(
)} {(hasActivePensionITF({ formData }) || - formData.benefitSelection[veteranBenefits.PENSION]) && ( + formData.benefitSelection[PENSION]) && (
( />
)} - {formData.benefitSelection[survivingDependentBenefits.SURVIVOR] && ( + {formData.benefitSelection[SURVIVOR] && (
( ); +const CompensationAction = () => ( +
+ +
+); + +const PensionAction = () => ( +
+ +
+); + +const SurvivorAction = () => ( +
+ +
+); + +const PensionExistingAction = ({ formData }) => ( +
+

+ Our records show that you already have an intent to file for pension + claims. Your intent to file for pension claims expires on{' '} + {format( + new Date(formData['view:activePensionITF'].expirationDate), + 'MMMM d, yyyy', + )} + . +

+ +
+); + +const CompensationExistingAction = ({ formData }) => ( +
+

+ Our records show that you already have an intent to file for disability + compensation. Your intent to file for disability compensation expires on{' '} + {format( + new Date(formData['view:activeCompensationITF'].expirationDate), + 'MMMM d, yyyy', + )} + . +

+ +
+); + +const newActionMap = { + [COMPENSATION]: CompensationAction, + [PENSION]: PensionAction, + [SURVIVOR]: SurvivorAction, +}; + +const existingActionMap = { + [COMPENSATION]: CompensationExistingAction, + [PENSION]: PensionExistingAction, +}; + +/** + * Returns a data only object of string constants like this: + * { + * actionsNew: ["compensation", "pension"], + * actionsExisting: ["compensation", "pension"] + * } + * + * This makes it easier to test the logic in isolation without rendering + */ +export const getNextStepsActionsPlaceholders = formData => { + const actionsExisting = []; + const actionsNew = []; + + const isCompensationApplicable = + hasActiveCompensationITF({ formData }) || + formData.benefitSelection?.[COMPENSATION]; + + const isPensionApplicable = + hasActivePensionITF({ formData }) || formData.benefitSelection?.[PENSION]; + + const isSurvivorApplicable = formData.benefitSelection?.[SURVIVOR]; + const hasCompensationExisting = hasActiveCompensationITF({ formData }); + const hasPensionExisting = hasActivePensionITF({ formData }); + + if (hasCompensationExisting) { + actionsExisting.push(COMPENSATION); + } else if (isCompensationApplicable) { + actionsNew.push(COMPENSATION); + } + + if (hasPensionExisting) { + actionsExisting.push(PENSION); + } else if (isPensionApplicable) { + actionsNew.push(PENSION); + } + + if (isSurvivorApplicable) { + actionsNew.push(SURVIVOR); + } + + return { actionsNew, actionsExisting }; +}; + +const NextStepsV2Actions = ({ formData }) => { + let { actionsNew, actionsExisting } = getNextStepsActionsPlaceholders( + formData, + ); + + // convert arrays of strings to arrays of JSX + // e.g. ["compensation", "pension"] => [, ] + actionsNew = actionsNew.map(action => { + const Component = newActionMap[action]; + return ; + }); + actionsExisting = actionsExisting.map(action => { + const Component = existingActionMap[action]; + return ; + }); + + return ( +
+ {actionsNew} + {actionsExisting} +
+ ); +}; + +const NextStepsV2 = ({ formData }) => { + return ( +
+

What are my next steps?

+ {confirmationPageNextStepsParagraph(formData) ? ( + <> +

You should complete and file your claims as soon as possible.

+

{confirmationPageNextStepsParagraph(formData)}

+ + ) : ( + <> +

You should complete and file your claims as soon as possible.

+

+ If you complete and file your claim before the intent to file + expires and we approve your claim, you may be able to get + retroactive payments. Retroactive payments are payments for the time + between when we processed your intent to file and when we approved + your claim. +

+ + )} + +
+ ); +}; + const AlreadySubmittedHeader = ({ formData }) => ( <> {!confirmationPageFormBypassed(formData) && ( <> {hasActiveCompensationITF({ formData }) && - formData.benefitSelection[veteranBenefits.PENSION] && ( + formData.benefitSelection[PENSION] && (

You’ve already submitted an intent to file for disability @@ -108,7 +277,7 @@ const AlreadySubmittedHeader = ({ formData }) => (

)} {hasActivePensionITF({ formData }) && - formData.benefitSelection[veteranBenefits.COMPENSATION] && ( + formData.benefitSelection[COMPENSATION] && (

You’ve already submitted an intent to file for pension claims @@ -142,14 +311,12 @@ export const ConfirmationPage = props => { const currentFormData = { ...data, benefitSelection: { - [veteranBenefits.COMPENSATION]: - data.benefitSelection?.[veteranBenefits.COMPENSATION] || + [COMPENSATION]: + data.benefitSelection?.[COMPENSATION] || data.benefitSelectionCompensation, - [veteranBenefits.PENSION]: - data.benefitSelection?.[veteranBenefits.PENSION] || - data.benefitSelectionPension, - [survivingDependentBenefits.SURVIVOR]: - data.benefitSelection?.[survivingDependentBenefits.SURVIVOR], + [PENSION]: + data.benefitSelection?.[PENSION] || data.benefitSelectionPension, + [SURVIVOR]: data.benefitSelection?.[SURVIVOR], }, }; @@ -183,6 +350,7 @@ export const ConfirmationPage = props => { expirationDate: submissionResponse?.expirationDate, confirmationNumber, })} + actions={<>} /> {!confirmationPageFormBypassed(formData) && ( <> @@ -191,34 +359,23 @@ export const ConfirmationPage = props => { /> - After we review your form, we’ll confirm next steps. Then you’ll have 1 year to file your claim.

- ) : ( - <> -

- After we process your request, we’ll confirm next steps. - If we don’t already have a form showing that you’re - authorized as a signer, we’ll contact the Veteran or - family member who intends to file the claim. -

-

- If we need information after reviewing your form, we’ll - contact you -

- - ) - } - /> + } + item1Actions={<>} + /> + )} )} - - - + diff --git a/src/applications/simple-forms/21-0966/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/simple-forms/21-0966/tests/containers/ConfirmationPage.unit.spec.jsx index 6e4e7d8efd4d..551045cd7731 100644 --- a/src/applications/simple-forms/21-0966/tests/containers/ConfirmationPage.unit.spec.jsx +++ b/src/applications/simple-forms/21-0966/tests/containers/ConfirmationPage.unit.spec.jsx @@ -7,7 +7,9 @@ import thunk from 'redux-thunk'; import { expect } from 'chai'; import formConfig from '../../config/form'; import * as features from '../../config/features'; -import ConfirmationPage from '../../containers/ConfirmationPage'; +import ConfirmationPage, { + getNextStepsActionsPlaceholders, +} from '../../containers/ConfirmationPage'; const veteranData = { benefitSelection: { @@ -167,7 +169,6 @@ describe('Confirmation page V2', () => { getByText(/Jack/); getByText(/Your form submission was successful on/); getByText(/You have until/); - getByText(/After we process your request/); expect( container.querySelector( 'va-link-action[text="Complete your pension claim"]', @@ -219,7 +220,6 @@ describe('Confirmation page V2', () => { getByText(/Alternate/); getByText(/Your form submission was successful on/); getByText(/You have until/); - getByText(/After we process your request/); expect( container.querySelector( 'va-link-action[text="Complete your pension for survivors claim"]', @@ -261,7 +261,6 @@ describe('Confirmation page V2', () => { getByText(/Jack/); getByText(/Your form submission was successful on/); getByText(/You have until/); - getByText(/After we process your request/); expect( container.querySelector( 'va-link-action[text="Complete your pension claim"]', @@ -294,6 +293,60 @@ describe('Confirmation page V2', () => { ), ).to.exist; }); + + it('should return correct getNextStepsActionsPlaceholders for a veteran with new benefits', () => { + const formData = { + benefitSelection: { + compensation: true, + pension: true, + }, + }; + const placeholders = getNextStepsActionsPlaceholders(formData); + expect(placeholders.actionsNew).to.deep.equal(['compensation', 'pension']); + expect(placeholders.actionsExisting).to.deep.equal([]); + }); + + it('should return correct getNextStepsActionsPlaceholders for a veteran with mixed benefits', () => { + const formData = { + benefitSelection: { + pension: true, + }, + 'view:activeCompensationITF': responseExisting.compensationIntent, + }; + const placeholders = getNextStepsActionsPlaceholders(formData); + expect(placeholders.actionsNew).to.deep.equal(['pension']); + expect(placeholders.actionsExisting).to.deep.equal(['compensation']); + }); + + it('should return correct getNextStepsActionsPlaceholders for a veteran with existing benefits', () => { + const formData = { + benefitSelection: {}, + 'view:activePensionITF': responseExisting.pensionIntent, + 'view:activeCompensationITF': responseExisting.compensationIntent, + }; + const placeholders = getNextStepsActionsPlaceholders(formData); + expect(placeholders.actionsNew).to.deep.equal([]); + expect(placeholders.actionsExisting).to.deep.equal([ + 'compensation', + 'pension', + ]); + }); + + it('should return correct getNextStepsActionsPlaceholders for a survivor', () => { + const formData = { + benefitSelection: { + survivor: true, + }, + 'view:activePensionITF': responseExisting.pensionIntent, + 'view:activeCompensationITF': responseExisting.compensationIntent, + }; + const placeholders = getNextStepsActionsPlaceholders(formData); + expect(placeholders.actionsNew).to.deep.equal(['survivor']); + expect(placeholders.actionsExisting).to.deep.equal([ + 'compensation', + 'pension', + ]); + }); }); describe('Confirmation page V1', () => { @@ -379,4 +432,6 @@ describe('Confirmation page V1', () => { 'va-link-action[text="Complete your pension for survivors claim"]', ); }); + + // confirmation v1 tests end }); From 1913280804d9d45609d3036551006a4680d51acb Mon Sep 17 00:00:00 2001 From: jvcAdHoc <144135615+jvcAdHoc@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:42:39 -0500 Subject: [PATCH 21/36] [99786] Appoint a Rep add submission types to search results (#34105) * add submission types to search results * fix spec nesting --- .../components/SearchResult.jsx | 16 +- .../SelectAccreditedRepresentative.jsx | 1 + .../config/prefillTransformer.js | 4 + .../components/SearchResult.unit.spec.jsx | 145 ++++++++++++++++++ .../config/prefillTransformer.unit.spec.js | 36 +++++ .../getFormNumberFromEntity.unit.spec.jsx | 38 +++++ .../utilities/helpers.js | 8 + 7 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/applications/representative-appoint/tests/utilities/getFormNumberFromEntity.unit.spec.jsx diff --git a/src/applications/representative-appoint/components/SearchResult.jsx b/src/applications/representative-appoint/components/SearchResult.jsx index cd154a3b3e25..b1376b4bff99 100644 --- a/src/applications/representative-appoint/components/SearchResult.jsx +++ b/src/applications/representative-appoint/components/SearchResult.jsx @@ -4,12 +4,15 @@ import { connect } from 'react-redux'; import { setData } from '~/platform/forms-system/src/js/actions'; import { VaButton } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { parsePhoneNumber } from '../utilities/parsePhoneNumber'; +import { getFormNumberFromEntity } from '../utilities/helpers'; +import useV2FeatureToggle from '../hooks/useV2FeatureVisibility'; const SearchResult = ({ representative, query, handleSelectRepresentative, loadingPOA, + userIsDigitalSubmitEligible, }) => { const { id } = representative.data; const { @@ -26,6 +29,9 @@ const SearchResult = ({ accreditedOrganizations, } = representative.data.attributes; + const formNumber = getFormNumberFromEntity(representative.data); + const v2IsEnabled = useV2FeatureToggle(); + const representativeName = name || fullName; const { contact, extension } = parsePhoneNumber(phone); @@ -43,6 +49,13 @@ const SearchResult = ({ (stateCode ? ` ${stateCode}` : '') + (zipCode ? ` ${zipCode}` : ''); + const submissionTypeContent = v2IsEnabled && + userIsDigitalSubmitEligible && ( +

+ Accepts VA Form {formNumber} online, by mail, and in person +

+ ); + return (
@@ -82,7 +95,7 @@ const SearchResult = ({
)} - + {submissionTypeContent}
{addressExists && (
@@ -181,6 +194,7 @@ SearchResult.propTypes = { router: PropTypes.object, routes: PropTypes.array, stateCode: PropTypes.string, + userIsDigitalSubmitEligible: PropTypes.bool, zipCode: PropTypes.string, }; diff --git a/src/applications/representative-appoint/components/SelectAccreditedRepresentative.jsx b/src/applications/representative-appoint/components/SelectAccreditedRepresentative.jsx index 2be2f28e02fb..22227efaded2 100644 --- a/src/applications/representative-appoint/components/SelectAccreditedRepresentative.jsx +++ b/src/applications/representative-appoint/components/SelectAccreditedRepresentative.jsx @@ -204,6 +204,7 @@ const SelectAccreditedRepresentative = props => { currentSelectedRep={currentSelectedRep.current} goToPath={goToPath} handleSelectRepresentative={handleSelectRepresentative} + userIsDigitalSubmitEligible={formData?.userIsDigitalSubmitEligible} /> ))}

diff --git a/src/applications/representative-appoint/config/prefillTransformer.js b/src/applications/representative-appoint/config/prefillTransformer.js index 671d21c25482..42cd9065d3ef 100644 --- a/src/applications/representative-appoint/config/prefillTransformer.js +++ b/src/applications/representative-appoint/config/prefillTransformer.js @@ -70,5 +70,9 @@ export default function prefillTransformer(formData) { newFormData['Branch of Service'] = undefined; } + newFormData.userIsDigitalSubmitEligible = + formData?.identityValidation?.hasIcn && + formData?.identityValidation?.hasParticipantId; + return newFormData; } diff --git a/src/applications/representative-appoint/tests/components/SearchResult.unit.spec.jsx b/src/applications/representative-appoint/tests/components/SearchResult.unit.spec.jsx index 7b947bca0dd1..11a73ca494d5 100644 --- a/src/applications/representative-appoint/tests/components/SearchResult.unit.spec.jsx +++ b/src/applications/representative-appoint/tests/components/SearchResult.unit.spec.jsx @@ -1,7 +1,9 @@ import React from 'react'; import { render } from '@testing-library/react'; import { expect } from 'chai'; +import sinon from 'sinon'; import { SearchResult } from '../../components/SearchResult'; +import * as useV2FeatureToggle from '../../hooks/useV2FeatureVisibility'; describe('SearchResult Component', () => { it('evaluates addressExists correctly', () => { @@ -16,6 +18,10 @@ describe('SearchResult Component', () => { }, }; + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(false); + const { container } = render( { const addressAnchor = container.querySelector('.address-anchor'); expect(addressAnchor).to.exist; expect(addressAnchor.textContent).to.contain('123 Main St'); + + useV2FeatureVisibilityStub.restore(); }); it('evaluates addressExists correctly when only city, stateCode, and zipCode exist', () => { @@ -41,6 +49,10 @@ describe('SearchResult Component', () => { }, }; + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(false); + const { container } = render( { const addressAnchor = container.querySelector('.address-anchor'); expect(addressAnchor).to.exist; expect(addressAnchor.textContent).to.contain('Anytown, CT'); + + useV2FeatureVisibilityStub.restore(); }); it('includes the representative name in the select button text', () => { @@ -69,6 +83,10 @@ describe('SearchResult Component', () => { }, }; + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(false); + const { container } = render( { ); expect(selectButton).to.exist; expect(selectButton.getAttribute('text')).to.contain('Robert Smith'); + + useV2FeatureVisibilityStub.restore(); + }); + + context('when v2 is enabled', () => { + context('when the user is userIsDigitalSubmitEligible', () => { + it('displays submission methods', () => { + const representative = { + data: { + id: 1, + type: 'individual', + attributes: { + addressLine1: '123 Main St', + city: '', + stateCode: '', + zipCode: '', + fullName: 'Robert Smith', + individualType: 'representative', + }, + }, + }; + + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(true); + + const { container } = render( + {}} + loadingPOA={false} + userIsDigitalSubmitEligible + />, + ); + + const submissionMethods = container.querySelector( + '[data-testid="submission-methods"]', + ); + + expect(submissionMethods).to.exist; + + useV2FeatureVisibilityStub.restore(); + }); + }); + + context('when the user is not userIsDigitalSubmitEligible', () => { + it('does not display submission methods', () => { + const representative = { + data: { + id: 1, + type: 'individual', + attributes: { + addressLine1: '123 Main St', + city: '', + stateCode: '', + zipCode: '', + fullName: 'Robert Smith', + individualType: 'representative', + }, + }, + }; + + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(true); + + const { container } = render( + {}} + loadingPOA={false} + userIsDigitalSubmitEligible={false} + />, + ); + + const submissionMethods = container.querySelector( + '[data-testid="submission-methods"]', + ); + + expect(submissionMethods).not.to.exist; + + useV2FeatureVisibilityStub.restore(); + }); + }); + }); + + context('when v2 is not enabled', () => { + it('does not display submission methods', () => { + const representative = { + data: { + id: 1, + type: 'individual', + attributes: { + addressLine1: '123 Main St', + city: '', + stateCode: '', + zipCode: '', + fullName: 'Robert Smith', + individualType: 'representative', + }, + }, + }; + + const useV2FeatureVisibilityStub = sinon + .stub(useV2FeatureToggle, 'default') + .returns(false); + + const { container } = render( + {}} + loadingPOA={false} + userIsDigitalSubmitEligible + />, + ); + + const submissionMethods = container.querySelector( + '[data-testid="submission-methods"]', + ); + + expect(submissionMethods).not.to.exist; + + useV2FeatureVisibilityStub.restore(); + }); }); }); diff --git a/src/applications/representative-appoint/tests/config/prefillTransformer.unit.spec.js b/src/applications/representative-appoint/tests/config/prefillTransformer.unit.spec.js index 7c5fec8c6afd..cf6e6938568f 100644 --- a/src/applications/representative-appoint/tests/config/prefillTransformer.unit.spec.js +++ b/src/applications/representative-appoint/tests/config/prefillTransformer.unit.spec.js @@ -120,4 +120,40 @@ describe('prefillTransformer', () => { expect(result.veteranSocialSecurityNumber).to.be.undefined; }); }); + + context('when the user does not have an ICN', () => { + it('sets userIsDigitalSubmitEligible to false', () => { + const data = { + ...prefill, + identityValidation: { hasIcn: false, hasParticipantId: true }, + }; + + const result = prefillTransformer(data); + + expect(result.userIsDigitalSubmitEligible).to.be.false; + }); + }); + + context('when the user does not have a participant id', () => { + it('sets userIsDigitalSubmitEligible to false', () => { + const data = { + ...prefill, + identityValidation: { hasIcn: true, hasParticipantId: false }, + }; + + const result = prefillTransformer(data); + + expect(result.userIsDigitalSubmitEligible).to.be.false; + }); + }); + + context('when the user has an ICN and a participant id', () => { + it('sets userIsDigitalSubmitEligible to true', () => { + const data = { ...prefill }; + + const result = prefillTransformer(data); + + expect(result.userIsDigitalSubmitEligible).to.be.true; + }); + }); }); diff --git a/src/applications/representative-appoint/tests/utilities/getFormNumberFromEntity.unit.spec.jsx b/src/applications/representative-appoint/tests/utilities/getFormNumberFromEntity.unit.spec.jsx new file mode 100644 index 000000000000..3def6d21c030 --- /dev/null +++ b/src/applications/representative-appoint/tests/utilities/getFormNumberFromEntity.unit.spec.jsx @@ -0,0 +1,38 @@ +import { expect } from 'chai'; + +import { getFormNumberFromEntity } from '../../utilities/helpers'; + +describe('getFormNumberFromEntity', () => { + it('should return "21-22" when entity type is organization', () => { + const mockFormData = { type: 'organization' }; + const result = getFormNumberFromEntity(mockFormData); + expect(result).to.equal('21-22'); + }); + + it('should return "21-22a" when individual type is attorney', () => { + const mockFormData = { + type: 'individual', + attributes: { individualType: 'attorney' }, + }; + const result = getFormNumberFromEntity(mockFormData); + expect(result).to.equal('21-22a'); + }); + + it('should return "21-22a" when individual type is claimsAgent', () => { + const mockFormData = { + type: 'individual', + attributes: { individualType: 'claimsAgent' }, + }; + const result = getFormNumberFromEntity(mockFormData); + expect(result).to.equal('21-22a'); + }); + + it('should return "21-22" when individual type is representative', () => { + const mockFormData = { + type: 'individual', + attributes: { individualType: 'representative' }, + }; + const result = getFormNumberFromEntity(mockFormData); + expect(result).to.equal('21-22'); + }); +}); diff --git a/src/applications/representative-appoint/utilities/helpers.js b/src/applications/representative-appoint/utilities/helpers.js index 49e210e37346..a777f757f624 100644 --- a/src/applications/representative-appoint/utilities/helpers.js +++ b/src/applications/representative-appoint/utilities/helpers.js @@ -115,6 +115,14 @@ export const getRepType = entity => { return 'VSO Representative'; }; +export const getFormNumberFromEntity = entity => { + const repType = getRepType(entity); + + return ['Organization', 'VSO Representative'].includes(repType) + ? '21-22' + : '21-22a'; +}; + export const getFormNumber = formData => { const entity = formData['view:selectedRepresentative']; const entityType = entity?.type; From 0d621157a643a4d7760be1e65ecf085bc40d79dc Mon Sep 17 00:00:00 2001 From: Hemesh Patel <49699643+hemeshvpatel@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:52:59 -0600 Subject: [PATCH 22/36] fix flow back issue, remove unused code (#34131) --- .../categoryAndTopic/selectCategory.js | 18 -------- .../categoryAndTopic/selectSubtopic.js | 18 -------- .../chapters/categoryAndTopic/selectTopic.js | 18 -------- .../yourQuestion/whoIsYourQuestionAbout.js | 22 ---------- src/applications/ask-va/config/form.js | 36 ++++++++++----- .../config/schema-helpers/formFlowHelper.js | 1 - .../whoIsYourQuestionAbout.unit.spec.js | 44 ------------------- 7 files changed, 24 insertions(+), 133 deletions(-) delete mode 100644 src/applications/ask-va/config/chapters/categoryAndTopic/selectCategory.js delete mode 100644 src/applications/ask-va/config/chapters/categoryAndTopic/selectSubtopic.js delete mode 100644 src/applications/ask-va/config/chapters/categoryAndTopic/selectTopic.js delete mode 100644 src/applications/ask-va/config/chapters/yourQuestion/whoIsYourQuestionAbout.js delete mode 100644 src/applications/ask-va/tests/config/chapters/yourQuestion/whoIsYourQuestionAbout.unit.spec.js diff --git a/src/applications/ask-va/config/chapters/categoryAndTopic/selectCategory.js b/src/applications/ask-va/config/chapters/categoryAndTopic/selectCategory.js deleted file mode 100644 index 1c90ad77c548..000000000000 --- a/src/applications/ask-va/config/chapters/categoryAndTopic/selectCategory.js +++ /dev/null @@ -1,18 +0,0 @@ -import { CHAPTER_1 } from '../../../constants'; - -const selectCategoryPage = { - uiSchema: { - selectCategory: { 'ui:title': CHAPTER_1.PAGE_1.QUESTION_1 }, - }, - schema: { - type: 'object', - required: ['selectCategory'], - properties: { - selectCategory: { - type: 'string', - }, - }, - }, -}; - -export default selectCategoryPage; diff --git a/src/applications/ask-va/config/chapters/categoryAndTopic/selectSubtopic.js b/src/applications/ask-va/config/chapters/categoryAndTopic/selectSubtopic.js deleted file mode 100644 index a0b9e8581f91..000000000000 --- a/src/applications/ask-va/config/chapters/categoryAndTopic/selectSubtopic.js +++ /dev/null @@ -1,18 +0,0 @@ -import { CHAPTER_1 } from '../../../constants'; - -const selectSubtopicPage = { - uiSchema: { - selectTopic: { 'ui:title': CHAPTER_1.PAGE_3.QUESTION_1 }, - }, - schema: { - type: 'object', - required: ['selectSubtopic'], - properties: { - selectSubtopic: { - type: 'string', - }, - }, - }, -}; - -export default selectSubtopicPage; diff --git a/src/applications/ask-va/config/chapters/categoryAndTopic/selectTopic.js b/src/applications/ask-va/config/chapters/categoryAndTopic/selectTopic.js deleted file mode 100644 index 2a3c5942452e..000000000000 --- a/src/applications/ask-va/config/chapters/categoryAndTopic/selectTopic.js +++ /dev/null @@ -1,18 +0,0 @@ -import { CHAPTER_1 } from '../../../constants'; - -const selectTopicPage = { - uiSchema: { - selectTopic: { 'ui:title': CHAPTER_1.PAGE_2.QUESTION_1 }, - }, - schema: { - type: 'object', - required: ['selectTopic'], - properties: { - selectTopic: { - type: 'string', - }, - }, - }, -}; - -export default selectTopicPage; diff --git a/src/applications/ask-va/config/chapters/yourQuestion/whoIsYourQuestionAbout.js b/src/applications/ask-va/config/chapters/yourQuestion/whoIsYourQuestionAbout.js deleted file mode 100644 index 3e7386348c39..000000000000 --- a/src/applications/ask-va/config/chapters/yourQuestion/whoIsYourQuestionAbout.js +++ /dev/null @@ -1,22 +0,0 @@ -import PageFieldSummary from '../../../components/PageFieldSummary'; -import { CHAPTER_2 } from '../../../constants'; - -const whoIsYourQuestionAboutPage = { - uiSchema: { - 'ui:objectViewField': PageFieldSummary, - whoIsYourQuestionAbout: { - 'ui:title': CHAPTER_2.PAGE_1.TITLE, - }, - }, - schema: { - type: 'object', - required: ['whoIsYourQuestionAbout'], - properties: { - whoIsYourQuestionAbout: { - type: 'string', - }, - }, - }, -}; - -export default whoIsYourQuestionAboutPage; diff --git a/src/applications/ask-va/config/form.js b/src/applications/ask-va/config/form.js index a65a9fb83a7f..d66abc76240a 100644 --- a/src/applications/ask-va/config/form.js +++ b/src/applications/ask-va/config/form.js @@ -11,12 +11,8 @@ import ConfirmationPage from '../containers/ConfirmationPage'; import IntroductionPage from '../containers/IntroductionPage'; // Category and Topic pages -import selectCategoryPage from './chapters/categoryAndTopic/selectCategory'; -import selectSubtopicPage from './chapters/categoryAndTopic/selectSubtopic'; -import selectTopicPage from './chapters/categoryAndTopic/selectTopic'; // Your Question -import whoIsYourQuestionAboutPage from './chapters/yourQuestion/whoIsYourQuestionAbout'; import yourQuestionPage from './chapters/yourQuestion/yourQuestion'; // Your Personal Information - Authenticated @@ -118,28 +114,40 @@ const formConfig = { title: CHAPTER_1.PAGE_1.TITLE, CustomPage: CategorySelectPage, CustomPageReview: CustomPageReviewField, - uiSchema: selectCategoryPage.uiSchema, - schema: selectCategoryPage.schema, editModeOnReviewPage: false, + schema: { + // This does still need to be here or it'll throw an error + type: 'object', + properties: {}, // The properties can be empty + }, + uiSchema: {}, }, selectTopic: { path: CHAPTER_1.PAGE_2.PATH, title: CHAPTER_1.PAGE_2.TITLE, CustomPage: TopicSelectPage, CustomPageReview: CustomPageReviewField, - uiSchema: selectTopicPage.uiSchema, - schema: selectTopicPage.schema, editModeOnReviewPage: false, + schema: { + // This does still need to be here or it'll throw an error + type: 'object', + properties: {}, // The properties can be empty + }, + uiSchema: {}, }, selectSubtopic: { path: CHAPTER_1.PAGE_3.PATH, title: CHAPTER_1.PAGE_3.TITLE, CustomPage: SubTopicSelectPage, CustomPageReview: CustomPageReviewField, - uiSchema: selectSubtopicPage.uiSchema, - schema: selectSubtopicPage.schema, depends: form => requiredForSubtopicPage.includes(form.selectTopic), editModeOnReviewPage: false, + schema: { + // This does still need to be here or it'll throw an error + type: 'object', + properties: {}, // The properties can be empty + }, + uiSchema: {}, }, }, }, @@ -153,11 +161,15 @@ const formConfig = { title: CHAPTER_2.PAGE_1.TITLE, CustomPage: WhoIsYourQuestionAboutCustomPage, CustomPageReview: CustomPageReviewField, - uiSchema: whoIsYourQuestionAboutPage.uiSchema, - schema: whoIsYourQuestionAboutPage.schema, depends: formData => { return whoIsYourQuestionAboutCondition(formData); }, + schema: { + // This does still need to be here or it'll throw an error + type: 'object', + properties: {}, // The properties can be empty + }, + uiSchema: {}, }, relationshipToVeteran: { editModeOnReviewPage: false, diff --git a/src/applications/ask-va/config/schema-helpers/formFlowHelper.js b/src/applications/ask-va/config/schema-helpers/formFlowHelper.js index 0fa03979a42b..6d5be28cf4fa 100644 --- a/src/applications/ask-va/config/schema-helpers/formFlowHelper.js +++ b/src/applications/ask-va/config/schema-helpers/formFlowHelper.js @@ -488,7 +488,6 @@ export const aboutSomeoneElseRelationshipVeteranPages = flowPages( const aboutSomeoneElseRelationshipFamilyMember = [ 'isQuestionAboutVeteranOrSomeoneElse', - 'aboutTheVeteran', // Needed for list, should not render ]; export const aboutSomeoneElseRelationshipFamilyMemberPages = flowPages( ch3Pages, diff --git a/src/applications/ask-va/tests/config/chapters/yourQuestion/whoIsYourQuestionAbout.unit.spec.js b/src/applications/ask-va/tests/config/chapters/yourQuestion/whoIsYourQuestionAbout.unit.spec.js deleted file mode 100644 index 3ed0d6418999..000000000000 --- a/src/applications/ask-va/tests/config/chapters/yourQuestion/whoIsYourQuestionAbout.unit.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import { $$ } from '@department-of-veterans-affairs/platform-forms-system/ui'; -import { DefinitionTester } from '@department-of-veterans-affairs/platform-testing/schemaform-utils'; -import { render } from '@testing-library/react'; -import { expect } from 'chai'; -import React from 'react'; -import { Provider } from 'react-redux'; - -import formConfig from '../../../../config/form'; -import { getData } from '../../../fixtures/data/mock-form-data'; - -const { - schema, - uiSchema, -} = formConfig.chapters.yourQuestionPart1.pages.whoIsYourQuestionAbout; - -describe('whoIsYourQuestionAboutPage', () => { - it('should render', () => { - const { container, getByLabelText } = render( - - - , - , - ); - - const radioLabels = $$('.form-radio-buttons > label', container); - const radioLabelList = [ - 'Myself', - 'Someone else', - "It's a general question", - ]; - - expect(getByLabelText(/Who is your question about/)).to.exist; - - radioLabels.forEach( - radio => expect(radioLabelList.includes(radio.textContent)).to.be.true, - ); - }); -}); From c0e9012a734e52b3cd314fada0f4b5d864f32ec4 Mon Sep 17 00:00:00 2001 From: Nicholas Hibbits <172406954+nicholashibbits@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:23:25 -0800 Subject: [PATCH 23/36] Edm 493 521 finalize lacs UI (#34130) * update capitalizeFirstLetter function, update word casing where needed * change UI for result details based on lac type, fix element spacing * refactor capitalizeFirstLetter function * scroll to top of each lac page when mounted * Fix table width issue and add pagination * make FAQs single select, change React bindings to web-components * change react bindings to web components * focus correctly on faqs | cleanup markup styles --------- Co-authored-by: Nick Hibbits <172406954+nicholashibbits1@users.noreply.github.com> --- .../LicenseCertificationAdminInfo.jsx | 52 ++++++++------ .../LicenseCertificationTestInfo.jsx | 61 +++++++++++++++- .../LicenseCertificationSearchPage.jsx | 71 +++++++++++-------- .../LicenseCertificationSearchResult.jsx | 19 +++-- .../LicenseCertificationSearchResults.jsx | 31 ++++---- src/applications/gi/sass/gi.scss | 2 + src/applications/gi/utils/helpers.js | 24 +++++-- 7 files changed, 177 insertions(+), 83 deletions(-) diff --git a/src/applications/gi/components/LicenseCertificationAdminInfo.jsx b/src/applications/gi/components/LicenseCertificationAdminInfo.jsx index 5f3bfe86708f..f849cc0df481 100644 --- a/src/applications/gi/components/LicenseCertificationAdminInfo.jsx +++ b/src/applications/gi/components/LicenseCertificationAdminInfo.jsx @@ -1,39 +1,51 @@ -import { - VaIcon, - VaLink, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import React from 'react'; +import { capitalizeFirstLetter } from '../utils/helpers'; -function LicenseCertificationAdminInfo({ institution }) { +function LicenseCertificationAdminInfo({ institution, type }) { const { name, mailingAddress } = institution; + return (

-

Admin Info

+

+ Admin info +

- -

{name}

+

{capitalizeFirstLetter(name)}

-

The following is the headquarters address.

+ {type === 'Certification' ? ( +

+ Certification tests are available to be taken nationally, search for a + testing site near you. +

+ ) : ( + <> +

The following is the headquarters address.

-

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

+

+ {capitalizeFirstLetter(mailingAddress.address1)} +
+ {capitalizeFirstLetter(mailingAddress.city)}, {mailingAddress.state}{' '} + {mailingAddress.zip} +
+

+ + )}

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

- +

+ +

); } diff --git a/src/applications/gi/components/LicenseCertificationTestInfo.jsx b/src/applications/gi/components/LicenseCertificationTestInfo.jsx index ac57b73ecc75..8b5940652472 100644 --- a/src/applications/gi/components/LicenseCertificationTestInfo.jsx +++ b/src/applications/gi/components/LicenseCertificationTestInfo.jsx @@ -1,17 +1,64 @@ -import React from 'react'; +import React, { useLayoutEffect, useState } from 'react'; +import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { formatCurrency } from '../utils/helpers'; function LcTestInfo({ tests }) { + const [currentPage, setCurrentPage] = useState(1); + + useLayoutEffect( + // eslint-disable-next-line consistent-return + () => { + const observer = new MutationObserver(() => { + const vaTableInner = document.querySelector( + '.table-wrapper va-table-inner', + ); + if (vaTableInner?.shadowRoot) { + const { shadowRoot } = vaTableInner; + const usaTable = shadowRoot.querySelector('.usa-table'); + if (usaTable) { + usaTable.style.width = '100%'; + } + } + }); + const vaTable = document.querySelector('.table-wrapper va-table'); + if (vaTable) { + observer.observe(vaTable, { + attributes: true, + childList: true, + subtree: true, + }); + } + return () => observer.disconnect(); + }, + [tests], + ); + + const itemsPerPage = 10; + + const totalPages = Math.ceil(tests.length / itemsPerPage); + const currentResults = + tests.length > itemsPerPage + ? tests.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ) + : tests; + + const handlePageChange = page => { + setCurrentPage(page); + }; + return ( <> +

Test info

Test Name Fees - {tests && - tests.map((test, index) => { + {currentResults && + currentResults.map((test, index) => { return ( {test.name} @@ -20,6 +67,14 @@ function LcTestInfo({ tests }) { ); })} + {tests.length > itemsPerPage && ( + handlePageChange(e.detail.page)} + page={currentPage} + pages={totalPages} + maxPageListLength={itemsPerPage} + /> + )}
); diff --git a/src/applications/gi/containers/LicenseCertificationSearchPage.jsx b/src/applications/gi/containers/LicenseCertificationSearchPage.jsx index d02ff59eebab..3d375f46da47 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchPage.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchPage.jsx @@ -2,13 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { - VaAccordion, - VaAccordionItem, - VaLink, - VaLoadingIndicator, - VaModal, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { VaModal } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import LicenseCertificationSearchForm from '../components/LicenseCertificationSearchForm'; import { handleLcResultsSearch, updateQueryParam } from '../utils/helpers'; import { fetchLicenseCertificationResults } from '../actions'; @@ -39,7 +33,7 @@ const faqs = [ 'How do I get reimbursed for the licenses, certifications, and prep courses?', answer: ( <> - @@ -52,7 +46,7 @@ const faqs = [



- @@ -94,12 +88,9 @@ function LicenseCertificationSearchPage({ message: '', }); - const handleUpdateQueryParam = () => updateQueryParam(history, location); - - const handleReset = callback => { - history.replace('/lc-search'); - callback?.(); - }; + useEffect(() => { + window.scrollTo(0, 0); + }, []); useEffect( () => { @@ -110,6 +101,27 @@ function LicenseCertificationSearchPage({ [hasFetchedOnce, dispatchFetchLicenseCertificationResults], ); + const handleFaqClick = index => { + const element = document.getElementById(`faq-${index}`); + element?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + }; + + const handleKeyDown = (event, index) => { + if (event.key === 'Enter' || event.key === ' ') { + handleFaqClick(index); + } + }; + + const handleUpdateQueryParam = () => updateQueryParam(history, location); + + const handleReset = callback => { + history.replace('/lc-search'); + callback?.(); + }; + const handleShowModal = (changedField, message, callback) => { return setModal({ visible: true, @@ -136,12 +148,7 @@ function LicenseCertificationSearchPage({ return (
- {fetchingLc && ( - - )} + {fetchingLc && } {!fetchingLc && hasFetchedOnce && lcResults.length !== 0 && ( @@ -175,16 +182,24 @@ function LicenseCertificationSearchPage({ />
-

FAQs

- +

+ FAQs +

+ {faqs.map((faq, index) => { return ( - + handleFaqClick(index)} + onKeyDown={e => handleKeyDown(e, index)} + > {faq.answer} - + ); })} -
+
{ modal.callback(); @@ -203,7 +217,6 @@ function LicenseCertificationSearchPage({ primaryButtonText="Continue to change" onSecondaryButtonClick={toggleModal} secondaryButtonText="Go Back" - // status={status} visible={modal.visible} >

{modal.message}

@@ -219,14 +232,12 @@ LicenseCertificationSearchPage.propTypes = { fetchingLc: PropTypes.bool.isRequired, hasFetchedOnce: PropTypes.bool.isRequired, lcResults: PropTypes.array, - // error: Proptypes // verify error Proptypes }; const mapStateToProps = state => ({ lcResults: state.licenseCertificationSearch.lcResults, fetchingLc: state.licenseCertificationSearch.fetchingLc, hasFetchedOnce: state.licenseCertificationSearch.hasFetchedOnce, - // error: // create error state in redux store }); const mapDispatchToProps = { diff --git a/src/applications/gi/containers/LicenseCertificationSearchResult.jsx b/src/applications/gi/containers/LicenseCertificationSearchResult.jsx index 8ba551bdbace..7a7e144f5120 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchResult.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchResult.jsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { useParams } from 'react-router-dom'; -import { VaLoadingIndicator } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { fetchLcResult } from '../actions'; import LicenseCertificationAdminInfo from '../components/LicenseCertificationAdminInfo'; import LicenseCertificationTestInfo from '../components/LicenseCertificationTestInfo'; @@ -16,6 +15,8 @@ function LicenseCertificationSearchResult({ const { id } = useParams(); useEffect(() => { + window.scrollTo(0, 0); + if (!hasFetchedResult) { dispatchFetchLcResult(id); } @@ -25,22 +26,20 @@ function LicenseCertificationSearchResult({ return (
- {fetchingLcResult && ( - - )} + {fetchingLcResult && } {!fetchingLcResult && - institution && - tests && ( // better check for empty resultInfo + institution && + tests && (

{lacNm}

{eduLacTypeNm}

- +
diff --git a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx index c6be2e45a5b9..65f9c9adcd1c 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx @@ -2,13 +2,7 @@ import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ADDRESS_DATA from 'platform/forms/address/data'; -import { - VaCard, - VaLink, - VaLinkAction, - VaLoadingIndicator, - VaPagination, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { useHistory, useLocation } from 'react-router-dom'; import { fetchLicenseCertificationResults } from '../actions'; import { @@ -62,8 +56,13 @@ function LicenseCertificationSearchResults({ [lcResults], ); + useEffect(() => { + window.scrollTo(0, 0); + }, []); + const handlePageChange = page => { setCurrentPage(page); + window.scroll({ top: 0, bottom: 0, behavior: 'smooth' }); // troubleshoot scrollTo functions in platform to align with standards }; const handleRouteChange = id => event => { @@ -83,7 +82,7 @@ function LicenseCertificationSearchResults({ return (
{fetchingLc && ( - @@ -97,7 +96,7 @@ function LicenseCertificationSearchResults({ Search Results

-
+

Showing{' '} @@ -108,9 +107,9 @@ function LicenseCertificationSearchResults({ itemsPerPage, )} of ${filteredResults.length} results for:`}

-

- License/Certification Name: {' '} + License/Certification name: {' '} {`"${nameParam}"`}

@@ -140,7 +139,7 @@ function LicenseCertificationSearchResults({ {currentResults.map((result, index) => { return (
  • - +

    {result.lacNm}

    {result.eduLacTypeNm} @@ -150,7 +149,7 @@ function LicenseCertificationSearchResults({ {ADDRESS_DATA.states[result.state]}

    )} - - +

  • ); })} @@ -170,7 +169,7 @@ function LicenseCertificationSearchResults({

    )}
    - {filteredResults.length > 0 && ( + {filteredResults.length > itemsPerPage && ( { }; export function capitalizeFirstLetter(string) { - if (string) { - return string.charAt(0).toUpperCase() + string.slice(1); - } + if (!string) return null; + + const exceptions = ['NW', 'SW', 'NE', 'SE', 'of', 'and']; + + return string + .split(' ') + .map(word => { + if (exceptions.includes(word)) { + return word; + } - return null; + if (word === 'OF') { + return 'of'; + } + if (word === 'AND') { + return 'and'; + } + + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(' '); } export const mappedStates = Object.entries(ADDRESS_DATA.states).map(state => { From 4f280529e4021fa0c3ea04f40f57cc6f2bbf8aa1 Mon Sep 17 00:00:00 2001 From: Taras Kurilo Date: Fri, 17 Jan 2025 12:47:25 -0500 Subject: [PATCH 24/36] add feature flag for programs route (#34137) --- src/applications/gi/routes.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/applications/gi/routes.jsx b/src/applications/gi/routes.jsx index a53d7ba850ec..8827a194b90e 100644 --- a/src/applications/gi/routes.jsx +++ b/src/applications/gi/routes.jsx @@ -21,6 +21,9 @@ const BuildRoutes = () => { const lcToggleValue = useToggleValue( TOGGLE_NAMES.giComparisonToolLceToggleFlag, ); + const toggleGiProgramsFlag = useToggleValue( + TOGGLE_NAMES.giComparisonToolProgramsToggleFlag, + ); return ( <> @@ -31,10 +34,12 @@ const BuildRoutes = () => { from="/profile/:facilityCode" to="/institution/:facilityCode" /> - } - /> + {toggleGiProgramsFlag && ( + } + /> + )} } From a5d10517651a4313484a1d1c5d2807ac250812b3 Mon Sep 17 00:00:00 2001 From: Oren Mittman Date: Fri, 17 Jan 2025 12:50:16 -0500 Subject: [PATCH 25/36] 4: [ART] Tweak var names in return URL fn (#34036) --- .../utilities/constants.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/applications/accredited-representative-portal/utilities/constants.js b/src/applications/accredited-representative-portal/utilities/constants.js index 9ea188b06b1b..b9da0acc651d 100644 --- a/src/applications/accredited-representative-portal/utilities/constants.js +++ b/src/applications/accredited-representative-portal/utilities/constants.js @@ -13,7 +13,7 @@ import { const USIP_PATH = '/sign-in'; const USIP_BASE_URL = environment.BASE_URL; -const baseReturnUrl = +const BASE_RETURN_URL = externalApplicationsConfig[USIP_APPLICATIONS.ARP].externalRedirectUrl; export const getSignInUrl = ({ returnUrl } = {}) => { @@ -22,8 +22,12 @@ export const getSignInUrl = ({ returnUrl } = {}) => { url.searchParams.set(USIP_QUERY_PARAMS.OAuth, true); if (returnUrl) { - const returnUrlPrefix = new URL(returnUrl).href.replace(baseReturnUrl, ''); - url.searchParams.set(USIP_QUERY_PARAMS.to, returnUrlPrefix); + const returnUrlSuffix = new URL(returnUrl).href.replace( + BASE_RETURN_URL, + '', + ); + + url.searchParams.set(USIP_QUERY_PARAMS.to, returnUrlSuffix); } return url; From b4e43a96fd260f3c54f58dc059b5a5d1e7fd8881 Mon Sep 17 00:00:00 2001 From: Jennifer Quispe Date: Fri, 17 Jan 2025 13:32:21 -0500 Subject: [PATCH 26/36] 4: Art/98327/poa-request-details (#34031) * 98327 - details cherry pick * 98327 - request details * 98327 - pr feedback * 98327 - update mock json * 98327 - removing tests --- .../components/POARequestCard.jsx | 83 ++- .../containers/POARequestDetailsPage.jsx | 483 ++++++++++------ .../containers/POARequestSearchPage.jsx | 7 +- .../sass/POARequestCard.scss | 2 +- .../sass/POARequestDetails.scss | 71 ++- .../components/POARequestCard.unit.spec.jsx | 16 - .../utilities/mockApi.js | 26 +- .../utilities/mocks/poaRequests.json | 540 ++++++++++++------ .../utilities/poaRequests.js | 40 ++ 9 files changed, 815 insertions(+), 453 deletions(-) delete mode 100644 src/applications/accredited-representative-portal/tests/unit/components/POARequestCard.unit.spec.jsx create mode 100644 src/applications/accredited-representative-portal/utilities/poaRequests.js diff --git a/src/applications/accredited-representative-portal/components/POARequestCard.jsx b/src/applications/accredited-representative-portal/components/POARequestCard.jsx index 50c2fd7c2cf3..65b7fe19f2bc 100644 --- a/src/applications/accredited-representative-portal/components/POARequestCard.jsx +++ b/src/applications/accredited-representative-portal/components/POARequestCard.jsx @@ -1,28 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { differenceInDays } from 'date-fns'; - import { - formatDateParsedZoneLong, - timeFromNow, -} from 'platform/utilities/date/index'; - -const expiresSoon = expDate => { - const EXPIRES_SOON_THRESHOLD_DURATION = 7 * 24 * 60 * 60 * 1000; - const now = new Date(); - const expiresAt = new Date(expDate); - const daysLeft = timeFromNow(expiresAt, now); - if ( - differenceInDays(expiresAt, now) > 0 && - differenceInDays(expiresAt, now) < EXPIRES_SOON_THRESHOLD_DURATION - ) { - return `(in ${daysLeft})`; - } - return null; -}; + expiresSoon, + formatStatus, + resolutionDate, +} from '../utilities/poaRequests'; const POARequestCard = ({ poaRequest, id }) => { + const lastName = poaRequest?.power_of_attorney_form?.claimant?.name?.last; + const firstName = poaRequest?.power_of_attorney_form?.claimant?.name?.first; + const city = poaRequest?.power_of_attorney_form?.claimant?.address.city; + const state = + poaRequest?.power_of_attorney_form?.claimant?.address.state_code; + const zipCode = + poaRequest?.power_of_attorney_form?.claimant?.address.zip_code; + const poaStatus = + poaRequest.resolution?.decision_type || poaRequest.resolution?.type; return (
  • @@ -30,62 +24,59 @@ const POARequestCard = ({ poaRequest, id }) => { data-testid={`poa-request-card-${id}-status`} className="usa-label poa-request__card-field poa-request__card-field--status" > - {poaRequest.status} + {formatStatus(poaStatus)} - + View details for

    - {`${poaRequest.claimant.lastName}, ${ - poaRequest.claimant.firstName - }`} + {`${lastName}, ${firstName}`}

    - - {poaRequest.claimantAddress.city} - + {city} {', '} - - {poaRequest.claimantAddress.state} - + {state} {', '} - - {poaRequest.claimantAddress.zip} - + {zipCode}

    - {poaRequest.status === 'Declined' && ( + {poaStatus === 'declination' && ( <> POA request declined on: - - {formatDateParsedZoneLong(poaRequest.acceptedOrDeclinedAt)} - + {resolutionDate(poaRequest.resolution?.created_at, id)} )} - {poaRequest.status === 'Accepted' && ( + {poaStatus === 'acceptance' && ( <> POA request accepted on: - - {formatDateParsedZoneLong(poaRequest.acceptedOrDeclinedAt)} + {resolutionDate(poaRequest.resolution?.created_at, id)} + + )} + + {poaStatus === 'expiration' && ( + <> + + POA request expired on: + {resolutionDate(poaRequest.resolution?.created_at, id)} )} - {poaRequest.status === 'Pending' && ( + {!poaRequest.resolution && ( <> - {expiresSoon(poaRequest.expiresAt) && ( + {expiresSoon(poaRequest.expires_at) && ( { POA request expires on: - - {formatDateParsedZoneLong(poaRequest.expiresAt)} - + {resolutionDate(poaRequest.expires_at, id)} - {expiresSoon(poaRequest.expiresAt)} + {expiresSoon(poaRequest.expires_at)} )} @@ -113,6 +102,8 @@ const POARequestCard = ({ poaRequest, id }) => { POARequestCard.propTypes = { cssClass: PropTypes.string, + id: PropTypes.string, + poaRequest: PropTypes.object, }; export default POARequestCard; diff --git a/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx b/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx index 7d4f3b42392b..8303c3439437 100644 --- a/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx +++ b/src/applications/accredited-representative-portal/containers/POARequestDetailsPage.jsx @@ -1,11 +1,14 @@ import React, { useState } from 'react'; -import { Link, useLoaderData, Form, redirect } from 'react-router-dom'; -import { formatDateShort } from 'platform/utilities/date/index'; +import { useLoaderData, Form, redirect } from 'react-router-dom'; import { VaRadio, VaRadioOption, } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; - +import { + expiresSoon, + formatStatus, + resolutionDate, +} from '../utilities/poaRequests'; import api from '../utilities/api'; const DECISION_TYPES = { @@ -48,24 +51,54 @@ const DECISION_OPTIONS = { ...DECLINATION_OPTIONS, }; -const checkAuthorizations = ( - isTreatmentDisclosureAuthorized, - isAddressChangingAuthorized, - status, -) => { - const authorizations = []; - if (isTreatmentDisclosureAuthorized === status) { - authorizations.push('Health'); - } +const Authorized = () => { + return ( + + + Authorized + + ); +}; + +const NoAccess = () => { + return ( + + + No Access + + ); +}; - if (isAddressChangingAuthorized === status) { - authorizations.push('Address'); +const AccessToSome = () => { + return ( + + + Access to some + + ); +}; +const checkAuthorizations = x => { + if (x) { + return ; } - return authorizations.length > 0 ? authorizations.join(', ') : 'None'; + return ; +}; +const checkLimitations = (limitations, limit) => { + const checkLimitation = limitations.includes(limit); + return checkAuthorizations(checkLimitation); }; const POARequestDetailsPage = () => { - const poaRequest = useLoaderData().attributes; + const poaRequest = useLoaderData(); const [error, setError] = useState(false); const handleChange = e => { e.preventDefault(); @@ -76,194 +109,278 @@ const POARequestDetailsPage = () => { setError(true); } }; - return ( -

    -

    - POA request: - {poaRequest?.claimant?.firstName} {poaRequest?.claimant?.lastName} -

    -