diff --git a/.github/ISSUE_TEMPLATE/Post_RC_Code_Change.md b/.github/ISSUE_TEMPLATE/Post_RC_Code_Change.md index 0bb1da5594c..e97bbdd76d1 100644 --- a/.github/ISSUE_TEMPLATE/Post_RC_Code_Change.md +++ b/.github/ISSUE_TEMPLATE/Post_RC_Code_Change.md @@ -3,7 +3,7 @@ name: Post RC Code Change about: Template for Adding Code After RC is Cut requiring a new RC build title: Post RC Code Change Template labels: release -assignees: TKDickson, bischoffa +assignees: TKDickson, SarahHuber_AdHoc --- diff --git a/.github/ISSUE_TEMPLATE/release_ticket.md b/.github/ISSUE_TEMPLATE/release_ticket.md index faa9246b6d8..8a467d4dbda 100644 --- a/.github/ISSUE_TEMPLATE/release_ticket.md +++ b/.github/ISSUE_TEMPLATE/release_ticket.md @@ -3,7 +3,7 @@ name: Release Review Template about: Template for requesting a production release for VA mobile app title: "{{ env.releaseDate }} Release Sign-Off: {{ env.versionNumber }}" labels: release -assignees: timwright12, chrisj-usds, dumathane, rachelhanster, kellylein, DonMcCaugheyUSDS, TKDickson +assignees: timwright12, chrisj-usds, dumathane, rachelhanster, SarahHuber_AdHoc, DonMcCaugheyUSDS, TKDickson --- diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml index c7b675e39a0..d5121a34e41 100644 --- a/.github/workflows/e2e_android.yml +++ b/.github/workflows/e2e_android.yml @@ -82,7 +82,6 @@ jobs: message: 'Starting E2E Android tests. Please see :thread: for results. This process may take a while.' find_detox_tests_to_run: - if: github.event_name == 'pull_request' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: ./.github/workflows/e2e_detox_mapping.yml output_detox_tests_to_run: @@ -99,8 +98,8 @@ jobs: - name: 'Get Matrix Value' id: matrix_value run: | - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ inputs.tests_to_run}}" != "" ]]; then - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ inputs.tests_to_run}}" != "" ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then e2eNames=$(gh api repos/department-of-veterans-affairs/va-mobile-app/contents/VAMobile/e2e/tests | jq --compact-output 'del(.[] | select(.name == "utils.ts")) | [.[].name]') echo "matrix=$e2eNames" >> "$GITHUB_OUTPUT" echo "individual_matrix=" >> "$GITHUB_OUTPUT" @@ -116,7 +115,7 @@ jobs: if [[ "${{ needs.find_detox_tests_to_run.outputs.test_run }}" != "" ]]; then echo "${{needs.find_detox_tests_to_run.outputs.test_matrix}}" if [[ "${{needs.find_detox_tests_to_run.outputs.test_matrix}}" == "[]" ]]; then - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then e2eNames=$(gh api repos/department-of-veterans-affairs/va-mobile-app/contents/VAMobile/e2e/tests | jq --compact-output 'del(.[] | select(.name == "utils.ts")) | [.[].name]') echo "matrix=$e2eNames" >> "$GITHUB_OUTPUT" echo "individual_matrix=" >> "$GITHUB_OUTPUT" @@ -242,7 +241,7 @@ jobs: if: failure() || success() uses: actions/upload-artifact@v4 with: - name: ${{matrix.testsuite}}-e2e-junit + name: e2e-junit-${{matrix.testsuite}} path: VAMobile/e2e/test_reports/e2e-junit.xml - name: Upload artifacts on failure @@ -349,16 +348,10 @@ jobs: matrix_send_test_results_to_testrail: if: (!cancelled()) && github.event.inputs.run_testRail == 'true' - needs: [matrix-e2e-android, output_detox_tests_to_run] - strategy: - fail-fast: false - max-parallel: 1 - matrix: - testsuite: ${{ fromJSON(needs.output_detox_tests_to_run.outputs.output1) }} + needs: [matrix-e2e-android, output_detox_tests_to_run] name: Update testRail Results uses: ./.github/workflows/update_testrail_results.yml with: - test_names: "${{matrix.testsuite}}" testRail_name: ${{ inputs.testRail_name }} test_OS_name: "Android" secrets: inherit diff --git a/.github/workflows/e2e_ios.yml b/.github/workflows/e2e_ios.yml index 00822a7b1e3..7197fe38d91 100644 --- a/.github/workflows/e2e_ios.yml +++ b/.github/workflows/e2e_ios.yml @@ -73,7 +73,6 @@ jobs: message: 'Starting E2E iOS tests. Please see :thread: for results. This process may take a while.' find_detox_tests_to_run: - if: github.event_name == 'pull_request' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: ./.github/workflows/e2e_detox_mapping.yml output_detox_tests_to_run: @@ -90,8 +89,8 @@ jobs: - name: 'Get Matrix Value' id: matrix_value run: | - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ inputs.tests_to_run}}" != "" ]]; then - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ inputs.tests_to_run}}" != "" ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then e2eNames=$(gh api repos/department-of-veterans-affairs/va-mobile-app/contents/VAMobile/e2e/tests | jq --compact-output 'del(.[] | select(.name == "utils.ts")) | [.[].name]') echo "matrix=$e2eNames" >> "$GITHUB_OUTPUT" echo "individual_matrix=" >> "$GITHUB_OUTPUT" @@ -107,7 +106,7 @@ jobs: if [[ "${{ needs.find_detox_tests_to_run.outputs.test_run }}" != "" ]]; then echo "${{needs.find_detox_tests_to_run.outputs.test_matrix}}" if [[ "${{needs.find_detox_tests_to_run.outputs.test_matrix}}" == "[]" ]]; then - if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]]; then + if [[ "${{ inputs.run_full_test }}" == "true" ]] || [[ ${{ github.event_name }} == 'schedule' ]] || [[ "${{ github.event.pull_request.user.login }}" == "dependabot[bot]" ]]; then e2eNames=$(gh api repos/department-of-veterans-affairs/va-mobile-app/contents/VAMobile/e2e/tests | jq --compact-output 'del(.[] | select(.name == "utils.ts")) | [.[].name]') echo "matrix=$e2eNames" >> "$GITHUB_OUTPUT" echo "individual_matrix=" >> "$GITHUB_OUTPUT" @@ -139,7 +138,6 @@ jobs: IOS_PROJ_FILE: 'VAMobile.xcodeproj' # Xcode scheme to build IOS_SCHEME: 'VAMobileRelease' - strategy: fail-fast: false matrix: @@ -220,8 +218,8 @@ jobs: if: failure() || success() uses: actions/upload-artifact@v4 with: - name: ${{matrix.testsuite}}-e2e-junit - path: VAMobile/e2e/test_reports/e2e-junit.xml + name: e2e-junit-${{matrix.testsuite}} + path: VAMobile/e2e/test_reports/e2e-junit.xml - name: Upload artifacts on failure if: failure() || steps.run_e2e_tests.outcome == 'failure' @@ -328,16 +326,10 @@ jobs: matrix_send_test_results_to_testrail: if: (!cancelled()) && github.event.inputs.run_testRail == 'true' - needs: [matrix-e2e-ios, output_detox_tests_to_run] - strategy: - fail-fast: false - max-parallel: 1 - matrix: - testsuite: ${{ fromJSON(needs.output_detox_tests_to_run.outputs.output1) }} + needs: [matrix-e2e-ios, output_detox_tests_to_run] name: Update testRail Results uses: ./.github/workflows/update_testrail_results.yml with: - test_names: "${{matrix.testsuite}}" testRail_name: ${{ inputs.testRail_name }} test_OS_name: "iOS" secrets: inherit diff --git a/.github/workflows/release_branch_issue.yml b/.github/workflows/release_branch_issue.yml index 40db9d7f871..75d64ec5994 100644 --- a/.github/workflows/release_branch_issue.yml +++ b/.github/workflows/release_branch_issue.yml @@ -94,7 +94,7 @@ jobs: run: | declare -A GITHUB_TO_SLACK_MAP GITHUB_TO_SLACK_MAP["TKDickson"]="U02PJLJ0H6H" - GITHUB_TO_SLACK_MAP["kellylein"]="UJHA49K6X" + GITHUB_TO_SLACK_MAP["SarahHuber_AdHoc"]="U07U9EDGAFP" GITHUB_TO_SLACK_MAP["dumathane"]="U02RC1BRZBP" GITHUB_TO_SLACK_MAP["timwright12"]="U01DBDAJZ18" GITHUB_TO_SLACK_MAP["ala_yna"]="UQC180926" @@ -111,7 +111,7 @@ jobs: env: SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }} SLACK_TKDickson: $TKDickson - SLACK_kellylein: $kellylein + SLACK_SarahHuberAdHoc: $SarahHuber_AdHoc SLACK_dumathane: $dumathane SLACK_timwright12: $timwright12 SLACK_alayna: $ala_yna @@ -133,7 +133,7 @@ jobs: - *Tickets Tagged Appropriately:* ${{ needs.release_ticket.outputs.qaDueDate }}\n\n\ *Contacts:*\n\ - *Release Testing:* <@${SLACK_TKDickson}>\n\ - - *Release Manager:* <@${SLACK_kellylein}>\n\ + - *Release Manager:* <@${SLACK_SarahHuberAdHoc}>\n\ - *Release Ticket Validation:* <@${SLACK_dumathane}>\n\ - *Engineering:* <@${SLACK_dumathane}>, <@${SLACK_timwright12}>\n\ - *Mobile Product Approvers:* H&B and Global - <@${SLACK_alayna}> Global - <@${SLACK_ajsarkar28}>\n\ @@ -148,4 +148,4 @@ jobs: -H 'Authorization: Bearer '"$SLACK_API_TOKEN" \ -H 'Content-type: application/json' \ -d @- \ - https://slack.com/api/chat.postMessage + https://slack.com/api/chat.postMessage \ No newline at end of file diff --git a/.github/workflows/update_testrail_results.yml b/.github/workflows/update_testrail_results.yml index a6bf3b822a5..e2b4ebf3cd6 100644 --- a/.github/workflows/update_testrail_results.yml +++ b/.github/workflows/update_testrail_results.yml @@ -10,9 +10,6 @@ on: description: "TestRail api key" required: true inputs: - test_names: - type: string - default: '' testRail_name: type: string default: '' @@ -39,7 +36,7 @@ jobs: - name: Download junit file uses: actions/download-artifact@v4 with: - name: ${{inputs.test_names}}-e2e-junit + pattern: e2e-junit-* - name: 'Find run ID in testRail' id: run-id-selection run: | @@ -86,15 +83,19 @@ jobs: python-version: '3.x' - name: TestRail CLI upload results if: always() - run: | + run: | pip install trcli - trcli -y \ - -h https://dsvavsp.testrail.io/ \ - --project "VA Mobile App" \ - --project-id 29 \ - -u ${{secrets.TEST_RAIL_USER}} \ - -k ${{secrets.TEST_RAIL_KEY}} \ - parse_junit \ - --run-id ${{steps.run-id-selection.outputs.TEST_RUN_ID}} \ - --section-id ${{steps.section-id-selection.outputs.SECTION_RUN_ID}} \ - -f "/home/runner/work/va-mobile-app/va-mobile-app/e2e-junit.xml" + for dir in /home/runner/work/va-mobile-app/va-mobile-app/e2e-junit-*/; do + echo "$dir" + trcli -y \ + -h https://dsvavsp.testrail.io/ \ + --project "VA Mobile App" \ + --project-id 29 \ + -u ${{secrets.TEST_RAIL_USER}} \ + -k ${{secrets.TEST_RAIL_KEY}} \ + parse_junit \ + --run-id ${{steps.run-id-selection.outputs.TEST_RUN_ID}} \ + --section-id ${{steps.section-id-selection.outputs.SECTION_RUN_ID}} \ + -f "${dir}e2e-junit.xml" + done + diff --git a/VAMobile/android/Gemfile.lock b/VAMobile/android/Gemfile.lock index ee304c14cb0..f715530f744 100644 --- a/VAMobile/android/Gemfile.lock +++ b/VAMobile/android/Gemfile.lock @@ -10,7 +10,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.998.0) + aws-partitions (1.1001.0) aws-sdk-core (3.211.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -127,7 +127,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-firebaseappdistribution_v1alpha (0.2.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-iamcredentials_v1 (0.21.0) + google-apis-iamcredentials_v1 (0.22.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.16.0) google-apis-core (>= 0.15.0, < 2.a) @@ -160,7 +160,7 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.4) + json (2.7.6) jwt (2.9.3) base64 mini_magick (4.13.2) @@ -205,7 +205,7 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.26.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/VAMobile/documentation/design/About/designers.md b/VAMobile/documentation/design/About/designers.md index 0c0adf3737f..8f8db755633 100644 --- a/VAMobile/documentation/design/About/designers.md +++ b/VAMobile/documentation/design/About/designers.md @@ -48,12 +48,12 @@ Once you’ve loaded the library, you should be able to access everything in it ## Figma -VA mobile design teams at Ad Hoc use Figma to view, share, and collaborate on our work. Only designers actively working on products at VA can be added to Figma. Once you have been added, you may access the libraries in the cloud. +VA mobile app design teams use Figma to view, share, and collaborate on our work. Currently, only designers at Ad Hoc can be added as Editors to the Mobile App team's Figma account. If you're working on an external Experience Team and need access to our files, you can follow the steps below to be added as a Viewer. ### Get added to Figma 1. Go to [figma.com](https://www.figma.com/) and [create a Figma account](https://help.figma.com/hc/en-us/articles/360039811114-Create-a-Figma-account) -2. In the [#va-mobile-app-shared-systems](https://dsva.slack.com/archives/C05HF9ULKJ4) channel in Slack, ping a Figma admin (currently Jen Ecker and Jessica Woodin) requesting to be added. +2. In the [#va-mobile-app-shared-systems](https://dsva.slack.com/archives/C05HF9ULKJ4) channel in Slack, ping a Figma admin (currently Kelly Lein, Jessica Woodin, and Holly Collier) requesting to be added. 3. Receive the invite via email and accept the invitation. 4. Boom, you’re in! @@ -65,4 +65,15 @@ Figma contains a few important features that help teams work together: * You receive library updates automatically (a big advantage of using Figma) * You can see what everyone else is working on in the VA workspace * Developers can inspect any element on a page -* You can [create a branch](https://department-of-veterans-affairs.github.io/va-mobile-app/docs/UX/How-We-Work/figma-branching) of your file at any time \ No newline at end of file +* You can [create a branch](https://department-of-veterans-affairs.github.io/va-mobile-app/docs/UX/How-We-Work/figma-branching) of your file at any time + +### Designers on Experience Teams +Currently, designers on Experience Teams can only be added as Viewers in the **VA Mobile App's Figma account**. In order to use the Mobile App libraries in the **VA.gov Platform team's Figma account**, designers should follow the steps below. + +1. In the [VA Mobile App team's Figma account](https://www.figma.com/files/827597988283174959/team/1114266503868297401), open the [Component Library](https://www.figma.com/design/Zzt8z60hCtdEzXx2GFWghH/%F0%9F%93%90-Component-Library---Design-System---VA-Mobile?m=auto&t=h1T1ozCx1hqbFSDa-7) and/or [Flagship Library](https://www.figma.com/design/QVLPB3eOunmKrgQOuOt0SU/Flagship-Library---%F0%9F%93%90-Resource---VA-Mobile?m=auto&t=h1T1ozCx1hqbFSDa-7). +2. Find the component you need. +3. Copy the component. +4. In the [VA.gov Platform team's Figma account](https://www.figma.com/files/team/1278375444205744118/all-projects), open your working file. +5. Paste the component into your working file. + +If you have questions or need assistance, reach out in the [#va-mobile-app-shared-systems](https://dsva.slack.com/archives/C05HF9ULKJ4) channel. \ No newline at end of file diff --git a/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/a11y-research-session-help.md b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/a11y-research-session-help.md new file mode 100644 index 00000000000..f8aee15ce93 --- /dev/null +++ b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/a11y-research-session-help.md @@ -0,0 +1,61 @@ +--- +title: Accessible Research Session Help +--- + +# Helpful tips for running an accessible research session + +*Last update: November 4, 2024* + +## Locating the VA: Health and Benefits app +With the recent update to iOS 18, users can now customize the appearance of their app icons without an additional shortcut or appearance customization app. There are now options available for light and dark mode for the app icons or to completely customize the color of the icons themselves. Prior to the update, we would have asked a sighted participant to locate the app icon with the white "VA" and blue background, however, a participant's app icon may no longer visually appear this way. On Android devices, it is also common for a user to customize the appearance of their app icons, font styles, etc. using shortcuts or apperance customization apps. + +Instead of relying on the app icon's appearance to describe and verify that the participant has the VA: Health and Benefits app installed, you should consider building a few extra minutes into your research study plan to verify that the participant has the correct app by asking them to open the app and visually look at the splash / login screen in their screen share to verify. + +If Perigean will be verifying this for you, notify your recruiter that they will need to request that the Veteran open the app and visually verify that they have the correct app installed. You should also consider supplying screenshots of the login screen to the recruiter in both dark and light modes, so that they have it to make the visual comparison themselves. + +For screen reader participants who are using VoiceOver or TalkBack, no matter which color option or icon style they may have chosen to use on their device, both screen readers will still announce the app as "VA". + + +## iOS + +### How to enable screen sharing in Zoom when using VoiceOver +1. In Zoom, navigate/swipe to the "share" (button) and double-tap +2. Double-tap on the first option: "screen" (button) +3. Double-tap on "start broadcast" (button) + - The participant will then hear a countdown (3, 2, 1) and the screen will begin broadcasting. + +### Screen Curtain +When VoiceOver is active and the participant has begun sharing their screen with you, you may notice that the screen is black and that you cannot see anything that is being shared. This is called **screen curtain**. If a participant has screen curtain enabled, you will not be able to see anything on the participants' screen unless they disable it. + +To disable screen curtain, instruct the participant to tap the screen four times using three fingers. This is called a three-finger quadruple tap. +- **Note:** If you instruct a user to turn off screen curtain during a session, offer to help them re-enable it before ending the session. Provide these instructions (the same instructions used to disable it) while they are still sharing their screen so that you can verify that screen curtain is re-enabled. + +## Android + +### How to enable screen sharing in Zoom when using TalkBack +1. By default, Zoom will typically default to "active speaker" mode. To display the toolbar, the participant can double-tap the screen with one finger. +2. Once the toolbar is displayed in Zoom, the particpant should navigate/swipe to the "share" (button) and double-tap + - _Share should be option 6 of 11 in the Zoom toolbar._ +3. The participant should navigate through the list until they get to "screen" (this is usually the 6th option in the list) and double-tap. +4. The participant will then read out the disclaimer for sharing their screen. Instruct the participant to navigate to "start now" (button) and double-tap. +5. After sharing the screen, Zoom will usually take the user out of Zoom and back to their home screen (or the last screen they were on prior to joining the meeting). Instruct them to return to / open the Zoom app to be able to access the chat area and access the link to the build. + - **Note:** After the participant opens the Zoom app again, it will typically default them back to the share option (although it will now announce as "stop share"). + - _Chat should be option 4 of 11 in the Zoom toolbar._ + +### Screen Curtain / Screen Shade +Some Android devices do have the ability to enable screen curtain on their device. If a participant has shared their screen with you during a session and their screen is black, dimmed, or is not changing, it is possible that their device support screen curtain and that it is enabled. Unfortunately, there is not a simple gesture availablet to disable screen curtain. + +To disable screen curtain on an Android device, the participant should: +1. Triple-tap the screen once to open the TalkBack menu. + - **Note:** On some devices, the gesture to open the TalkBack menu may not be available or may not work. In this case, you should instruct the user to navigate to their TalkBack settings within their device settings (Settings > Accessibility > TalkBack). +2. Instruct the participant to locate an option for "screen curtain", "screen shade", "show screen", etc. + - **Note:** Depending on the participant's Android device manufacturer (Google, Samsung, etc.), this feature may have a different name or a different way of announcing / listing the feature. The participant should listen for an option that might impact the visibility of the device screen. + - Alternatively, they might also be ale to activate their voice assistant (Google Assistant, Bixby, etc.) and instruct the voice assistant to show their screen, disable screen shade/curtain, etc. +3. This step will depend on the participant's Android device, but after they have located the option for the screen curtain / shade, they should follow any necessary steps to show their screen / disable screen curtain. This could be a simple double-tap gesture to show the screen or could take them to their TalkBack settings where they may need to toggle off the screen curtain. + - **Note:** It is recommended that you ask the participant to talk you through the steps that they are taking to disable the screen shade / curtain and that you (or an observer) make a quick note so that you can help them reactivate it at the end of the session. + +## Additional Resources +- [VoiceOver Gestures (iOS)](https://support.apple.com/guide/iphone/use-voiceover-gestures-iph3e2e2281/ios) +- [TalkBack Gestures (Android)](https://support.google.com/accessibility/android/answer/6151827?hl=en) +- [Inclusive Research for Screen Reader Users by Angela Fowler and Jamie Klenetsky Fay (Google Doc)](https://docs.google.com/document/d/1KvXZqzTm_Go1ZjzCmo8lNqe6Y2QOB9-9bt-RWsojGI0/edit?usp=sharing) + - **Note:** This guide is based on running inclusive research sessions with a computer and not on mobile devices. Not all of the information in the guide will apply to mobile-based research sessions. \ No newline at end of file diff --git a/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/index.md b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/index.md index 0eab83a2e11..f09b022f204 100644 --- a/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/index.md +++ b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/Accessibility/index.md @@ -8,27 +8,23 @@ We take a proactive, accessibility-first approach to everything we build. Access We also believe in accessibility beyond compliance. We don’t just meet the bare minimum of accessibility recommendations. We go above and beyond these recommendations to ensure we’re creating a product that is truly accessible to all of our users. - ## Accessibility principles - ### Based on VA standards The [VA design system](https://design.va.gov/) does not have comprehensive accessibility guidelines. However, it does [provide accessibility samples and guidelines](https://department-of-veterans-affairs.github.io/va-mobile-app/docs/QA/QualityAssuranceProcess/Accessibility#:~:text=provide%20accessibility%20samples%20and%20guidelines) when it comes to design, content, and components. We base our design system on this existing system with modifications for native mobile apps. - ### Content takes center stage All content throughout the app is clear and direct to ensure that users understand the information presented to them. A more thorough explanation of our best practices for accessible content can be found on the [content documentation page](/docs/Flagship%20design%20library/Content/content-style-guide). - ### Go beyond guideline standards -The VA Mobile app targets WCAG 2.2 Level AA and Level AAA success criteria and section 508 guidelines. For more information, see the following resources: +The VA Mobile app targets WCAG 2.2 Level AA and Level AAA success criteria and section 508 guidelines. We also reference MCAG (Mobile Content Accessiblity Guidelines), when applicable. For more information, see the following resources: - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) - [508 guidelines](https://www.access-board.gov/ict/#508-chapter-1-application-and-administration) - +- [MCAG (Mobile Content Accessibility Guidelines)](https://getevinced.github.io/mcag/) ### Comprehensive disability support @@ -40,7 +36,6 @@ We aim to provide a usable experience for all Veterans and include disability su - Individuals with hearing impairments - Individuals with mental or cognitive disabilities - ### Assistive technology support Our app and components are tested for accessibility with automated and manual techniques. Users should expect to be able to access our app using modern assistive technologies. These include native and third-party tools like: @@ -51,21 +46,18 @@ Our app and components are tested for accessibility with automated and manual te - Bluetooth mouse and keyboard - Tools for readability - ### Beyond disabilities We believe that accessibility also goes beyond disabilities and we strive to provide access to our app for users with limited connectivity and device performance. - ### The Section 508 law Section 508 is a federal law that requires all U.S. government agencies to make their electronic and information technology and data accessible to everyone. By law, agencies must provide people with disabilities the same level of access as those without disabilities. - ## Resources -* [Accessibility checklist for UX designers](/docs/QA/QualityAssuranceProcess/Accessibility/a11y-checklist-ux-designers) -* [Accessibility checklist for content designers](/docs/QA/QualityAssuranceProcess/Accessibility/a11y-checklist-content-designers) -* [Haptic feedback](/docs/Flagship%20design%20library/Patterns/haptics) -* [VA.gov mobile accessibility testing plan](/docs/QA/QualityAssuranceProcess/Accessibility/testing-plan) -* [Native mobile app accessibility resources & articles](/docs/QA/QualityAssuranceProcess/Accessibility/resources) +- [Accessibility checklist for UX designers](/docs/QA/QualityAssuranceProcess/Accessibility/a11y-checklist-ux-designers) +- [Accessibility checklist for content designers](/docs/QA/QualityAssuranceProcess/Accessibility/a11y-checklist-content-designers) +- [Haptic feedback](/docs/Flagship%20design%20library/Patterns/haptics) +- [VA.gov mobile accessibility testing plan](/docs/QA/QualityAssuranceProcess/Accessibility/testing-plan) +- [Native mobile app accessibility resources & articles](/docs/QA/QualityAssuranceProcess/Accessibility/resources) diff --git a/VAMobile/documentation/docs/QA/QualityAssuranceProcess/a11y-research-session-help.md b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/a11y-research-session-help.md new file mode 100644 index 00000000000..91fe358801c --- /dev/null +++ b/VAMobile/documentation/docs/QA/QualityAssuranceProcess/a11y-research-session-help.md @@ -0,0 +1,68 @@ +--- +title: Accessible Research Session Help +description: Helpful tips for running an accessible research session +--- + +## Locating the VA: Health and Benefits app + +With the recent update to iOS 18, users can now customize the appearance of their app icons without an additional shortcut or appearance customization app. There are options for light and dark mode for the app icons or to completely customize the color of the icons themselves. Prior to the update, we would have asked a sighted participant to locate the app icon with the white "VA" and blue background, however, a participant's app icon may no longer visually appear this way. On Android devices, it is also common for a user to customize their icons, font styles, etc. using shortcuts or apperance customization apps. + +Instead of relying on the app icon's appearance to describe and verify that the participant has the VA: Health and Benefits app installed, you should consider building a few extra minutes into your research study plan to verify that the participant has the app by asking them to open the app and visually looking at the splash / login screen in their screen share. + +If a third party company will be verifying this for you, notify your recruiter that they will need to request that the Veteran open the app and visually verify that they have the correct app installed. You should also consider supplying screenshots of the login screen to the recruiter in both dark and light modes, so that they have it to make the visual comparison themselves. + +For screen reader participants who are using VoiceOver or TalkBack, no matter what color options or icon style they may have chosen to use on their device, both screen readers will still announce the app as "VA". + +## iOS + +### How to enable screen sharing in Zoom when using VoiceOver + +1. In Zoom, navigate/swipe to the "share" (button) and double-tap +2. Double-tap on the first option: "screen" (button) +3. Double-tap on "start broadcast" (button) +4. The participant will then hear a countdown (3, 2, 1) and the screen will begin broadcasting. + +### Screen Curtain + +When VoiceOver is active and the participant has begun sharing their screen with you, you may notice that the screen is black and that you cannot see anything that is being shared. This is called [screen curtain](https://support.apple.com/en-us/111797). If a participant has screen curtain enabled, you will not be able to see anything on the participants' screen unless they disable it. + +[Read instructions on how to disable screen curtain](https://support.apple.com/guide/iphone/keep-the-screen-off-iph756788a12/ios) + +:::note +If you instruct a user to turn off screen curtain during a session, offer to help them re-enable it before ending the session. Provide these instructions (the same instructions used to disable it) while they are still sharing their screen so that you can verify that screen curtain is re-enabled. +::: + +## Android + +### How to enable screen sharing in Zoom when using TalkBack + +1. By default, Zoom will typically default to "active speaker" mode. To display the toolbar, the participant can double-tap the screen with one finger. +2. Once the toolbar is displayed in Zoom, the particpant should navigate/swipe to the "share" (button) and double-tap ("share" should be option 6 of 11 in the Zoom toolbar). +3. The participant should navigate through the list until they get to "screen" (this is usually the 6th option in the list), then double-tap. +4. The participant will then read out the disclaimer for sharing their screen. Instruct the participant to navigate to "start now" (button) and double-tap. +5. After sharing the screen, Zoom will usually take the user out of Zoom and back to their home screen (or the last screen they were on prior to joining the meeting). Instruct them to return to the Zoom app to be able to access the chat area and access the link to the build. + +:::note +After the participant opens the Zoom app again, it will typically default them back to the share option, although it will now announce as "stop share" (Chat should be option 4 of 11 in the Zoom toolbar). +::: + +### Screen Curtain / Screen Shade + +Some Android devices do have the ability to enable screen curtain on their device. If a participant has shared their screen with you during a session and their screen is black, dimmed, or is not changing, it is possible that their device support screen curtain and that it is enabled. Unfortunately, there is not a simple gesture available to disable screen curtain. + +To disable screen curtain on an Android device, the participant should: + +1. Triple-tap the screen once or navigate to TalkBack settings within device settings (Settings > Accessibility > TalkBack) to open the TalkBack menu. +2. Instruct the participant to locate an option for "screen curtain", "screen shade", or "show screen". Depending on the participant's Android device manufacturer (Google, Samsung, etc.), this feature may have a different name or a different way of announcing / listing the feature. The participant should listen for an option that might impact the visibility of the device screen.Alternatively, they might also be ale to activate their voice assistant (Google Assistant, Bixby, etc.) and instruct the voice assistant to show their screen, disable screen shade/curtain, etc. +3. (This step will depend on the participant's Android device) After they have located the option for the screen curtain / shade, they should follow any necessary steps to show their screen and disable screen curtain. This could be a simple double-tap gesture to show the screen or could take them to their TalkBack settings where they may need to toggle off the screen curtain. + +:::note +It is recommended that you ask the participant to talk you through the steps that they are taking to disable the screen shade / curtain and that you (or an observer) make a quick note so that you can help them reactivate it at the end of the session. +::: + +## Additional Resources + +- [VoiceOver Gestures (iOS)](https://support.apple.com/guide/iphone/use-voiceover-gestures-iph3e2e2281/ios) +- [TalkBack Gestures (Android)](https://support.google.com/accessibility/android/answer/6151827?hl=en) +- [Inclusive Research for Screen Reader Users by Angela Fowler and Jamie Klenetsky Fay (Google Doc)](https://docs.google.com/document/d/1KvXZqzTm_Go1ZjzCmo8lNqe6Y2QOB9-9bt-RWsojGI0/edit?usp=sharing) + - **Note:** This guide is based on running inclusive research sessions with a computer and not on mobile devices. Not all of the information in the guide will apply to mobile-based research sessions. diff --git a/VAMobile/e2e/tests/AppointmentsExpanded.e2e.ts b/VAMobile/e2e/tests/AppointmentsExpanded.e2e.ts index 630d17cf08e..46d00f5bd0f 100644 --- a/VAMobile/e2e/tests/AppointmentsExpanded.e2e.ts +++ b/VAMobile/e2e/tests/AppointmentsExpanded.e2e.ts @@ -22,14 +22,25 @@ const checkMedicationWording = async ({ appointmentType === 'VA' || appointmentType === 'ATLAS' || appointmentType === 'GFE' || - appointmentType === 'Home' + appointmentType === 'Home' || + appointmentType === 'Claim' ) { if ( appointmentStatus === 'Canceled' || (!pastAppointment && (appointmentStatus === 'Upcoming' || appointmentStatus === 'Confirmed')) ) { await expect(element(by.text('Prepare for your appointment'))).toExist() - await expect(element(by.text('Find a full list of things to bring to your appointment'))).toExist() + if ( + appointmentType === 'Phone' || + appointmentType === 'CC' || + appointmentType === 'Onsite' || + appointmentType === 'VA' || + appointmentType === 'ATLAS' || + appointmentType === 'GFE' || + appointmentType === 'Home' + ) { + await expect(element(by.text('Find a full list of things to bring to your appointment'))).toExist() + } if (appointmentType === 'ATLAS' || appointmentType === 'Home' || appointmentType === 'GFE') { await expect(element(by.text('Get your device ready to join.'))).toExist() @@ -41,7 +52,26 @@ const checkMedicationWording = async ({ await element(by.id('prepareForVideoVisitTestID')).tap() await expect(element(by.text('Appointments help'))).toExist() await element(by.text('Close')).tap() + } else if (appointmentType === 'Claim') { + await expect(element(by.text('You don’t need to bring anything to your exam.'))).toExist() + await expect( + element( + by.text( + 'If you have any new non-VA medication records (like records from a recent surgery or illness), be sure to submit them before your appointment.', + ), + ), + ).toExist() + await expect(element(by.text('Learn more about claim exam appointments'))).toExist() } else { + await expect(element(by.text('You don’t need to bring anything to your exam.'))).not.toExist() + await expect( + element( + by.text( + 'If you have any new non-VA medication records (like records from a recent surgery or illness), be sure to submit them before your appointment.', + ), + ), + ).not.toExist() + await expect(element(by.text('Learn more about claim exam appointments'))).not.toExist() await expect(element(by.text('Get your device ready to join.'))).not.toExist() await expect(element(by.id('prepareForVideoVisitTestID'))).not.toExist() } diff --git a/VAMobile/e2e/tests/AvailabilityFramework.e2e.ts b/VAMobile/e2e/tests/AvailabilityFramework.e2e.ts index 064495a41be..c838ec428fa 100644 --- a/VAMobile/e2e/tests/AvailabilityFramework.e2e.ts +++ b/VAMobile/e2e/tests/AvailabilityFramework.e2e.ts @@ -131,13 +131,13 @@ const AFNavigationForIndividual = [ ['BenefitLetters.e2e', 'WG_ClaimLettersScreen', 'Benefits', 'Claims', 'Claim letters'], ['Claims.e2e', 'WG_ClaimDetailsScreen', 'Benefits', 'Claims', 'Claims history', 'Received December 05, 2021'], // [ - // 'Claims.e2e', - // 'WG_SubmitEvidence', - // 'Benefits', - // 'Claims', - // 'Claims history', - // 'Received December 05, 2021', - // 'Submit evidence', + // 'Claims.e2e', + // 'WG_SubmitEvidence', + // 'Benefits', + // 'Claims', + // 'Claims history', + // 'Received December 05, 2021', + // 'Submit evidence', // ], ['Appeals.e2e', 'WG_AppealDetailsScreen', 'Benefits', 'Claims', 'Claims history', 'Received July 17, 2008'], [ diff --git a/VAMobile/e2e/tests/Onboarding.e2e.ts b/VAMobile/e2e/tests/Onboarding.e2e.ts index e0bada940ec..930fb292252 100644 --- a/VAMobile/e2e/tests/Onboarding.e2e.ts +++ b/VAMobile/e2e/tests/Onboarding.e2e.ts @@ -5,7 +5,6 @@ import { CommonE2eIdConstants, checkImages, loginToDemoMode } from './utils' export const OnboardingE2eIdConstants = { VA_ICON_ID: 'VAIconOnboardingLogo', DONE_NEXT_BUTTON_ID: 'onboardingDoneNextButtonID', - SKIP_BACK_BUTTON_ID: 'onboardingSkipBackButtonID', } beforeAll(async () => { @@ -26,7 +25,7 @@ describe('Onboarding Screen', () => { ), ), ).toExist() - await expect(element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() + await expect(element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() await expect(element(by.id(OnboardingE2eIdConstants.DONE_NEXT_BUTTON_ID))).toExist() }) @@ -37,7 +36,7 @@ describe('Onboarding Screen', () => { await expect(element(by.text('Refill your prescriptions'))).toExist() await expect(element(by.text('Communicate with your health care team'))).toExist() await expect(element(by.text('Review your appointments'))).toExist() - await expect(element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() + await expect(element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() await expect(element(by.id(OnboardingE2eIdConstants.DONE_NEXT_BUTTON_ID))).toExist() }) @@ -48,7 +47,7 @@ describe('Onboarding Screen', () => { await expect(element(by.text('Review your disability rating'))).toExist() await expect(element(by.text('Check the status of your claims and appeals'))).toExist() await expect(element(by.label('Download common V-A letters'))).toExist() - await expect(element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() + await expect(element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() await expect(element(by.id(OnboardingE2eIdConstants.DONE_NEXT_BUTTON_ID))).toExist() }) @@ -58,16 +57,16 @@ describe('Onboarding Screen', () => { await expect(element(by.text('Use our payments tools to manage tasks like these:'))).toExist() await expect(element(by.text('Update your direct deposit information'))).toExist() await expect(element(by.text('Review the history of payments we’ve sent to you'))).toExist() - await expect(element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() + await expect(element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID))).toExist() await expect(element(by.id(OnboardingE2eIdConstants.DONE_NEXT_BUTTON_ID))).toExist() }) it('should tap back and verify the previous page is displayed', async () => { - await element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() + await element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() await expect(element(by.text('Manage your benefits'))).toExist() - await element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() + await element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() await expect(element(by.text('Manage your health care'))).toExist() - await element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() + await element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() await expect(element(by.text('Welcome, Kimberly'))).toExist() }) @@ -84,7 +83,7 @@ describe('Onboarding Screen', () => { await device.installApp() await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } }) await loginToDemoMode(false) - await element(by.id(OnboardingE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() + await element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() await expect(element(by.text(CommonE2eIdConstants.HOME_ACTIVITY_HEADER_TEXT))).toExist() }) }) diff --git a/VAMobile/e2e/tests/utils.ts b/VAMobile/e2e/tests/utils.ts index 2b0baf4b7d5..a54dbc36ad6 100644 --- a/VAMobile/e2e/tests/utils.ts +++ b/VAMobile/e2e/tests/utils.ts @@ -27,7 +27,7 @@ export const CommonE2eIdConstants = { DEMO_MODE_INPUT_ID: 'demo-mode-password', DEMO_BTN_ID: 'demo-btn', SIGN_IN_BTN_ID: 'Sign in', - SKIP_BTN_TEXT: 'Skip', + TURN_ON_NOTIFICATIONS_TEXT: 'Turn on notifications', VETERAN_CRISIS_LINE_BTN_TEXT: 'Talk to the Veterans Crisis Line now', VETERAN_CRISIS_LINE_BTN_ID: 'veteransCrisisLineID', VETERAN_CRISIS_LINE_BACK_ID: 'veteranCrisisLineBackID', @@ -87,6 +87,13 @@ export const CommonE2eIdConstants = { CLAIMS_DETAILS_BACK_ID: 'claimsDetailsBackTestID', CLAIMS_HISTORY_BACK_ID: 'claimsHistoryBackTestID', CLAIMS_HISTORY_CLOSED_TAB_ID: 'claimsHistoryClosedID', + SKIP_BACK_BUTTON_ID: 'onboardingSkipBackButtonID', + HEALTH_TAB_BUTTON_ID: 'Health', + PAYMENTS_TAB_BUTTON_ID: 'Payments', + BENEFITS_TAB_BUTTON_ID: 'Benefits', + HOME_TAB_BUTTON_ID: 'Home', + AF_APP_UPDATE_BUTTON_TOGGLE_ID: 'remoteConfigAppUpdateTestID', + AF_ENABLE_TOGGLE_ID: 'remoteConfigEnableTestID', } /** Log the automation into demo mode @@ -122,15 +129,22 @@ export async function loginToDemoMode(skipOnboarding = true, pushNotifications?: await element(by.id(CommonE2eIdConstants.DEMO_MODE_INPUT_ID)).tapReturnKey() await element(by.id(CommonE2eIdConstants.DEMO_BTN_ID)).multiTap(2) - await element(by.text(CommonE2eIdConstants.SIGN_IN_BTN_ID)).tap() + await element(by.id(CommonE2eIdConstants.SIGN_IN_BTN_ID)).tap() if (skipOnboarding === true) { - const ifCarouselSkipBtnExist = await checkIfElementIsPresent(CommonE2eIdConstants.SKIP_BTN_TEXT, true) + const ifCarouselSkipBtnExist = await checkIfElementIsPresent(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID) if (ifCarouselSkipBtnExist) { - await element(by.text(CommonE2eIdConstants.SKIP_BTN_TEXT)).tap() + await element(by.id(CommonE2eIdConstants.SKIP_BACK_BUTTON_ID)).tap() } } + const turnOnNotificationsBtnExist = await checkIfElementIsPresent( + CommonE2eIdConstants.TURN_ON_NOTIFICATIONS_TEXT, + true, + ) + if (turnOnNotificationsBtnExist) { + await element(by.text(CommonE2eIdConstants.TURN_ON_NOTIFICATIONS_TEXT)).tap() + } } /** this function is to see if a element is present that could sometime not be like the carousel for example @@ -316,7 +330,7 @@ export async function openMilitaryInformation() { } export async function openHealth() { - await element(by.text(CommonE2eIdConstants.HEALTH_TAB_BUTTON_TEXT)).tap() + await element(by.id(CommonE2eIdConstants.HEALTH_TAB_BUTTON_ID)).tap() } export async function openAppointments() { @@ -324,7 +338,7 @@ export async function openAppointments() { } export async function openPayments() { - await element(by.text(CommonE2eIdConstants.PAYMENTS_TAB_BUTTON_TEXT)).tap() + await element(by.id(CommonE2eIdConstants.PAYMENTS_TAB_BUTTON_ID)).tap() } export async function openDirectDeposit() { @@ -344,7 +358,7 @@ export async function openVAPaymentHistory() { } export async function openBenefits() { - await element(by.text(CommonE2eIdConstants.BENEFITS_TAB_BUTTON_TEXT)).tap() + await element(by.id(CommonE2eIdConstants.BENEFITS_TAB_BUTTON_ID)).tap() } export async function openLetters() { @@ -392,11 +406,11 @@ export async function enableAF(AFFeature, AFUseCase, AFAppUpdate = false) { await openProfile() await openSettings() await openDeveloperScreen() - await waitFor(element(by.text('Remote Config'))) + await waitFor(element(by.text(CommonE2eIdConstants.REMOTE_CONFIG_BUTTON_TEXT))) .toBeVisible() .whileElement(by.id('developerScreenTestID')) .scroll(200, 'down') - await element(by.text('Remote Config')).tap() + await element(by.text(CommonE2eIdConstants.REMOTE_CONFIG_BUTTON_TEXT)).tap() if (AFUseCase === 'DenyAccess') { await waitFor(element(by.text(CommonE2eIdConstants.IN_APP_REVIEW_TOGGLE_TEXT))) .toBeVisible() @@ -412,30 +426,30 @@ export async function enableAF(AFFeature, AFUseCase, AFAppUpdate = false) { if (AFAppUpdate) { try { - await expect(element(by.id('remoteConfigAppUpdateTestID'))).toHaveToggleValue(true) + await expect(element(by.id(CommonE2eIdConstants.AF_APP_UPDATE_BUTTON_TOGGLE_ID))).toHaveToggleValue(true) } catch (ex) { - await element(by.text('appUpdateButton')).tap() + await element(by.id(CommonE2eIdConstants.AF_APP_UPDATE_BUTTON_TOGGLE_ID)).tap() } } else if (AFFeature === 'WG_Health') { try { - await expect(element(by.id('remoteConfigEnableTestID'))).toHaveToggleValue(false) + await expect(element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID))).toHaveToggleValue(false) } catch (ex) { - await element(by.text('Enabled')).tap() + await element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID)).tap() } } if (!AFAppUpdate) { if (AFUseCase === 'AllowFunction') { try { - await expect(element(by.id('remoteConfigEnableTestID'))).toHaveToggleValue(false) + await expect(element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID))).toHaveToggleValue(false) } catch (ex) { - await element(by.text('Enabled')).tap() + await element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID)).tap() } } else if (AFUseCase === 'DenyAccess') { try { - await expect(element(by.id('remoteConfigEnableTestID'))).toHaveToggleValue(false) + await expect(element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID))).toHaveToggleValue(false) } catch (ex) { - await element(by.text('Enabled')).tap() + await element(by.id(CommonE2eIdConstants.AF_ENABLE_TOGGLE_ID)).tap() } } } @@ -457,7 +471,7 @@ export async function enableAF(AFFeature, AFUseCase, AFAppUpdate = false) { await loginToDemoMode() } } else { - await element(by.text('Home')).tap() + await element(by.id(CommonE2eIdConstants.HOME_TAB_BUTTON_ID)).tap() } } @@ -471,11 +485,11 @@ export async function disableAF(featureNavigationArray, AFFeature, AFFeatureName await openProfile() await openSettings() await openDeveloperScreen() - await waitFor(element(by.text('Remote Config'))) + await waitFor(element(by.text(CommonE2eIdConstants.REMOTE_CONFIG_BUTTON_TEXT))) .toBeVisible() .whileElement(by.id('developerScreenTestID')) .scroll(200, 'down') - await element(by.text('Remote Config')).tap() + await element(by.text(CommonE2eIdConstants.REMOTE_CONFIG_BUTTON_TEXT)).tap() await waitFor(element(by.text(AFFeature))) .toBeVisible() .whileElement(by.id(CommonE2eIdConstants.REMOTE_CONFIG_TEST_ID)) @@ -484,7 +498,7 @@ export async function disableAF(featureNavigationArray, AFFeature, AFFeatureName await element(by.text('Enabled')).tap() await element(by.text('Save')).tap() - await element(by.text('Home')).tap() + await element(by.id(CommonE2eIdConstants.HOME_TAB_BUTTON_ID)).tap() if (featureNavigationArray !== undefined) { await navigateToFeature(featureNavigationArray) @@ -584,17 +598,19 @@ export async function verifyAF(featureNavigationArray, AFUseCase, AFUseCaseUpgra if (device.getPlatform() === 'android') { await device.disableSynchronization() try { - await element(by.text('800-698-2411')).atIndex(0).tap() + await element(by.id(CommonE2eIdConstants.CALL_VA_PHONE_NUMBER_ID)).atIndex(0).tap() } catch (ex) { - await element(by.text('800-698-2411').withAncestor(by.id('AFUseCase2TestID'))).tap() + await element(by.id(CommonE2eIdConstants.CALL_VA_PHONE_NUMBER_ID).withAncestor(by.id('AFUseCase2TestID'))).tap() } await setTimeout(5000) await device.takeScreenshot(featureName + 'AFUseCase2PhoneNumber') await device.launchApp({ newInstance: false }) try { - await element(by.text('TTY: 711')).atIndex(0).tap() + await element(by.id(CommonE2eIdConstants.CALL_VA_TTY_PHONE_NUMBER_ID)).atIndex(0).tap() } catch (ex) { - await element(by.text('TTY: 711').withAncestor(by.id('AFUseCase2TestID'))).tap() + await element( + by.id(CommonE2eIdConstants.CALL_VA_TTY_PHONE_NUMBER_ID).withAncestor(by.id('AFUseCase2TestID')), + ).tap() } await setTimeout(5000) await device.takeScreenshot(featureName + 'AFUseCase2TTY') diff --git a/VAMobile/env/constant.env b/VAMobile/env/constant.env index d912e23ce81..1c69bafef5e 100644 --- a/VAMobile/env/constant.env +++ b/VAMobile/env/constant.env @@ -1,3 +1,4 @@ +WEBVIEW_URL_APPOINTMENTS_CLAIM_EXAM_LEARN_MORE=https://www.va.gov/disability/va-claim-exam/ WEBVIEW_URL_CHANGE_LEGAL_NAME=https://www.va.gov/resources/how-to-change-your-legal-name-on-file-with-va/ WEBVIEW_URL_CORONA_FAQ=https://www.va.gov/coronavirus-veteran-frequently-asked-questions WEBVIEW_URL_FACILITY_LOCATOR=https://www.va.gov/find-locations/ diff --git a/VAMobile/ios/Gemfile.lock b/VAMobile/ios/Gemfile.lock index e60cfae3a4c..4ce56818fa0 100644 --- a/VAMobile/ios/Gemfile.lock +++ b/VAMobile/ios/Gemfile.lock @@ -5,8 +5,9 @@ GEM base64 nkf rexml - activesupport (7.2.1.2) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) @@ -24,7 +25,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.998.0) + aws-partitions (1.1001.0) aws-sdk-core (3.211.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -41,12 +42,13 @@ GEM aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) + benchmark (0.3.0) bigdecimal (3.1.8) claide (1.1.0) - cocoapods (1.16.1) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.16.1) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -60,8 +62,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.26.0, < 2.0) - cocoapods-core (1.16.1) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -219,7 +221,7 @@ GEM i18n (1.14.6) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.4) + json (2.7.6) jwt (2.9.3) base64 logger (1.6.1) @@ -278,7 +280,7 @@ GEM xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.26.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/VAMobile/src/App.tsx b/VAMobile/src/App.tsx index b5f96b12b04..f80d9b8980f 100644 --- a/VAMobile/src/App.tsx +++ b/VAMobile/src/App.tsx @@ -45,9 +45,10 @@ import { } from 'screens' import FileRequestSubtask from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/FileRequestSubtask' import SubmitEvidenceSubtask from 'screens/BenefitsScreen/ClaimsScreen/ClaimDetailsScreen/ClaimStatus/ClaimFileUpload/SubmitEvidenceSubtask' -import BiometricsPreferenceScreen from 'screens/BiometricsPreferenceScreen' import { profileAddressType } from 'screens/HomeScreen/ProfileScreen/ContactInformationScreen/AddressSummary' import EditAddressScreen from 'screens/HomeScreen/ProfileScreen/ContactInformationScreen/EditAddressScreen' +import BiometricsPreferenceScreen from 'screens/auth/BiometricsPreferenceScreen' +import RequestNotificationsScreen from 'screens/auth/RequestNotifications/RequestNotificationsScreen' import store, { RootState } from 'store' import { injectStore } from 'store/api/api' import { AnalyticsState, AuthState, handleTokenCallbackUrl, initializeAuth } from 'store/slices' @@ -113,6 +114,7 @@ export type RootNavStackParamList = WebviewStackParams & { type StackNavParamList = WebviewStackParams & { Splash: undefined BiometricsPreference: undefined + RequestNotifications: undefined Sync: undefined Login: undefined LoaGate: undefined @@ -197,8 +199,15 @@ function MainApp() { export function AuthGuard() { const dispatch = useAppDispatch() - const { initializing, loggedIn, syncing, firstTimeLogin, canStoreWithBiometric, displayBiometricsPreferenceScreen } = - useSelector((state) => state.auth) + const { + initializing, + loggedIn, + syncing, + firstTimeLogin, + canStoreWithBiometric, + displayBiometricsPreferenceScreen, + requestNotificationsPreferenceScreen, + } = useSelector((state) => state.auth) const { tappedForegroundNotification, setTappedForegroundNotification } = useNotificationContext() const { loadingRemoteConfig, remoteConfigActivated } = useSelector( (state) => state.settings, @@ -358,6 +367,16 @@ export function AuthGuard() { ) } else if (firstTimeLogin && loggedIn) { content = + } else if (!firstTimeLogin && loggedIn && requestNotificationsPreferenceScreen) { + content = ( + + + + ) } else if (loggedIn) { content = ( <> diff --git a/VAMobile/src/api/claimsAndAppeals/downloadEFolderDocument.tsx b/VAMobile/src/api/claimsAndAppeals/downloadEFolderDocument.tsx new file mode 100644 index 00000000000..13a87ff846c --- /dev/null +++ b/VAMobile/src/api/claimsAndAppeals/downloadEFolderDocument.tsx @@ -0,0 +1,42 @@ +import FileViewer from 'react-native-file-viewer' + +import { useQuery } from '@tanstack/react-query' + +import store from 'store' +import { DEMO_MODE_LETTER_ENDPOINT } from 'store/api/demo/letters' +import getEnv from 'utils/env' +import { downloadDemoFile, downloadFile } from 'utils/filesystem' +import { registerReviewEvent } from 'utils/inAppReviews' + +import { claimsAndAppealsKeys } from './queryKeys' + +const { API_ROOT } = getEnv() + +/** + * Fetch user E Folder Document + */ +const downloadEFolderDocument = async (id: string, fileName: string): Promise => { + const eFolderDocumentAPI = `${API_ROOT}/v0/efolder/documents/${id}/download?file_name=${fileName}}` + + const filePath = store.getState().demo.demoMode + ? await downloadDemoFile(DEMO_MODE_LETTER_ENDPOINT, fileName) + : await downloadFile('POST', eFolderDocumentAPI, fileName, undefined, 1) + if (filePath) { + await FileViewer.open(filePath, { onDismiss: () => registerReviewEvent() }) + return true + } +} + +/** + * Returns a query for a user E Folder Document + */ +export const useDownloadEFolderDocument = (id: string, fileName: string) => { + return useQuery({ + enabled: false, + queryKey: [claimsAndAppealsKeys.eFolderDownloadDoc, id, fileName], + queryFn: () => downloadEFolderDocument(id, fileName), + meta: { + errorName: 'downloadEFolderDocument: Service error', + }, + }) +} diff --git a/VAMobile/src/api/claimsAndAppeals/getEFolderDocuments.tsx b/VAMobile/src/api/claimsAndAppeals/getEFolderDocuments.tsx new file mode 100644 index 00000000000..892c9ee1b10 --- /dev/null +++ b/VAMobile/src/api/claimsAndAppeals/getEFolderDocuments.tsx @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query' + +import { ClaimEFolderData, ClaimEFolderDocuments } from 'api/types' +import { get } from 'store/api' + +import { claimsAndAppealsKeys } from './queryKeys' + +/** + * Fetch user E Folder Documents + */ +const getEfolderDocuments = async (): Promise | undefined> => { + const response = await get(`/v0/efolder/documents`, {}) + return response?.data +} + +/** + * Returns a query for user E Folder Documents + */ +export const useEFolderDocuments = (options?: { enabled?: boolean }) => { + return useQuery({ + ...options, + queryKey: [claimsAndAppealsKeys.eFolderDocs], + queryFn: () => getEfolderDocuments(), + meta: { + errorName: 'get E Folder Documents: Service error', + }, + }) +} diff --git a/VAMobile/src/api/claimsAndAppeals/index.ts b/VAMobile/src/api/claimsAndAppeals/index.ts index e1b10870ffc..82c26abab14 100644 --- a/VAMobile/src/api/claimsAndAppeals/index.ts +++ b/VAMobile/src/api/claimsAndAppeals/index.ts @@ -1,6 +1,8 @@ +export * from './downloadEFolderDocument' export * from './getAppeal' export * from './getClaim' export * from './getClaimsAndAppeals' +export * from './getEFolderDocuments' export * from './queryKeys' export * from './submitClaimDecision' export * from './uploadFileToClaim' diff --git a/VAMobile/src/api/claimsAndAppeals/queryKeys.ts b/VAMobile/src/api/claimsAndAppeals/queryKeys.ts index ef0537ab4e5..9a8ca14816d 100644 --- a/VAMobile/src/api/claimsAndAppeals/queryKeys.ts +++ b/VAMobile/src/api/claimsAndAppeals/queryKeys.ts @@ -2,4 +2,6 @@ export const claimsAndAppealsKeys = { appeal: ['appeal'] as const, claim: ['claim'] as const, claimsAndAppeals: ['claimsAndAppeals'] as const, + eFolderDocs: ['EFolderDocs'] as const, + eFolderDownloadDoc: ['EFolderDownloadDoc'] as const, } diff --git a/VAMobile/src/api/types/ClaimsAndAppealsData.ts b/VAMobile/src/api/types/ClaimsAndAppealsData.ts index 8f1f9c90fea..47b082e3a4d 100644 --- a/VAMobile/src/api/types/ClaimsAndAppealsData.ts +++ b/VAMobile/src/api/types/ClaimsAndAppealsData.ts @@ -459,6 +459,21 @@ export type ClaimEventData = { suspenseDate?: string | null documents?: Array phase?: number + documentId?: string +} + +export type ClaimEFolderData = { + data: Array +} + +export type ClaimEFolderDocuments = { + id: string + type: string + attributes: { + doc_type: string + type_description: string + received_at: string + } } export type ClaimAttributesData = { @@ -535,6 +550,7 @@ export type ClaimEventDocumentData = { documentType?: string filename?: string uploadDate: string + documentId?: string } export type ClaimPhaseData = { diff --git a/VAMobile/src/components/LabelTag.tsx b/VAMobile/src/components/LabelTag.tsx index fca4f8cc893..a1a1fdbb2ae 100644 --- a/VAMobile/src/components/LabelTag.tsx +++ b/VAMobile/src/components/LabelTag.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react' -import { Pressable, PressableProps, useWindowDimensions } from 'react-native' +import { AccessibilityRole, Pressable, PressableProps, useWindowDimensions } from 'react-native' import { useTheme } from 'utils/hooks' @@ -36,10 +36,13 @@ export type LabelTagProps = { /** Optional accessibility hint if there is an on press */ a11yHint?: string + + /** Optional role to override the default role of button */ + a11yRole?: AccessibilityRole } /**Common component to show a text inside a tag*/ -const LabelTag: FC = ({ text, labelType, onPress, a11yHint, a11yLabel }) => { +const LabelTag: FC = ({ text, labelType, onPress, a11yHint, a11yLabel, a11yRole }) => { const theme = useTheme() const fontScale = useWindowDimensions().fontScale const adjustSize = fontScale >= 2 @@ -84,7 +87,7 @@ const LabelTag: FC = ({ text, labelType, onPress, a11yHint, a11yL let pressableProps: PressableProps = { onPress: onPress, accessible: true, - accessibilityRole: 'button', + accessibilityRole: a11yRole || 'button', } if (a11yHint) { diff --git a/VAMobile/src/components/Menu/MenuView.tsx b/VAMobile/src/components/Menu/MenuView.tsx index 6a81f08b4a9..7748b7d5285 100644 --- a/VAMobile/src/components/Menu/MenuView.tsx +++ b/VAMobile/src/components/Menu/MenuView.tsx @@ -136,7 +136,7 @@ const MenuView: FC = ({ actions }) => { return ( <> - + diff --git a/VAMobile/src/components/MultiTouchCard.test.tsx b/VAMobile/src/components/MultiTouchCard.test.tsx index 5d94120601e..aae71520e0b 100644 --- a/VAMobile/src/components/MultiTouchCard.test.tsx +++ b/VAMobile/src/components/MultiTouchCard.test.tsx @@ -61,7 +61,7 @@ context('MultiTouchCard', () => { }) it('calls onPress function on bottomContent click', () => { - fireEvent.press(screen.getByRole('button', { name: 'bottom line 1' })) + fireEvent.press(screen.getByRole('link', { name: 'bottom line 1' })) expect(onPressSpy).toBeCalled() }) }) diff --git a/VAMobile/src/components/MultiTouchCard.tsx b/VAMobile/src/components/MultiTouchCard.tsx index df5e1a4bf9a..a22e85fe752 100644 --- a/VAMobile/src/components/MultiTouchCard.tsx +++ b/VAMobile/src/components/MultiTouchCard.tsx @@ -58,7 +58,7 @@ const MultiTouchCard: FC = ({ let bottomPressableProps: PressableProps = { onPress: bottomOnPress, accessible: true, - accessibilityRole: 'button', + accessibilityRole: 'link', accessibilityHint: bottomA11yHint, } diff --git a/VAMobile/src/components/NotificationManager/NotificationManager.tsx b/VAMobile/src/components/NotificationManager/NotificationManager.tsx index 487303b7b47..ad9d1f5121d 100644 --- a/VAMobile/src/components/NotificationManager/NotificationManager.tsx +++ b/VAMobile/src/components/NotificationManager/NotificationManager.tsx @@ -30,7 +30,7 @@ const NotificationContext = createContext({ * notification manager component to handle all push logic */ const NotificationManager: FC = ({ children }) => { - const { loggedIn } = useSelector((state) => state.auth) + const { loggedIn, firstTimeLogin, requestNotifications } = useSelector((state) => state.auth) const { data: personalInformation } = usePersonalInformation({ enabled: loggedIn }) const { mutate: registerDevice } = useRegisterDevice() const [tappedForegroundNotification, setTappedForegroundNotification] = useState(false) @@ -57,13 +57,15 @@ const NotificationManager: FC = ({ children }) => { registeredNotifications.remove() failedNotifications.remove() }) - Notifications.registerRemoteNotifications() + if (firstTimeLogin === false && requestNotifications === true) { + Notifications.registerRemoteNotifications() + } } if (loggedIn && personalInformation?.id) { register() } - }, [loggedIn, personalInformation?.id, registerDevice]) + }, [loggedIn, firstTimeLogin, requestNotifications, personalInformation?.id, registerDevice]) const registerNotificationEvents = () => { // Register callbacks for notifications that happen when the app is in the foreground diff --git a/VAMobile/src/screens/BiometricsPreferenceScreen/BiometricsPreferenceScreen.test.tsx b/VAMobile/src/screens/BiometricsPreferenceScreen/BiometricsPreferenceScreen.test.tsx deleted file mode 100644 index a49b172f2b2..00000000000 --- a/VAMobile/src/screens/BiometricsPreferenceScreen/BiometricsPreferenceScreen.test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react' -import { BIOMETRY_TYPE } from 'react-native-keychain' - -import { fireEvent, screen } from '@testing-library/react-native' - -import { InitialState, setBiometricsPreference, setDisplayBiometricsPreferenceScreen } from 'store/slices' -import { context, render } from 'testUtils' - -import BiometricsPreferenceScreen from './BiometricsPreferenceScreen' - -jest.mock('store/slices', () => { - const actual = jest.requireActual('store/slices') - return { - ...actual, - setBiometricsPreference: jest.fn(() => { - return { - type: '', - payload: '', - } - }), - setDisplayBiometricsPreferenceScreen: jest.fn(() => { - return { - type: '', - payload: '', - } - }), - } -}) - -context('BiometricsPreferenceScreen', () => { - const initializeTestInstance = (biometric = BIOMETRY_TYPE.TOUCH_ID) => { - render(, { - preloadedState: { - auth: { - ...InitialState.auth, - supportedBiometric: biometric, - }, - }, - }) - } - - beforeEach(() => { - initializeTestInstance() - }) - - it('initializes correctly', () => { - expect(screen.getByRole('header', { name: 'Do you want to allow us to use Touch ID for sign in?' })).toBeTruthy() - expect( - screen.getByText( - 'Touch ID lets you use your fingerprint to sign in to this app.\nYou can always change this later in your app settings.', - ), - ).toBeTruthy() - initializeTestInstance(BIOMETRY_TYPE.FACE_ID) - expect(screen.getByRole('header', { name: 'Do you want to allow us to use Face ID for sign in?' })).toBeTruthy() - expect( - screen.getByText( - 'Face ID lets us recognize an image of your face to sign you in to this app.\nYou can always change this later in your app settings.', - ), - ).toBeTruthy() - initializeTestInstance(BIOMETRY_TYPE.FACE) - expect( - screen.getByRole('header', { name: 'Do you want to allow us to use Face Recognition for sign in?' }), - ).toBeTruthy() - expect( - screen.getByText( - 'Face recognition lets you use facial recognition to sign into this app.\nYou can always change this later in your app settings.', - ), - ).toBeTruthy() - initializeTestInstance(BIOMETRY_TYPE.FINGERPRINT) - expect(screen.getByRole('header', { name: 'Do you want to allow us to use Fingerprint for sign in?' })).toBeTruthy() - expect( - screen.getByText( - 'Fingerprint lets you use your fingerprint to sign into this app.\nYou can always change this later in your app settings.', - ), - ).toBeTruthy() - initializeTestInstance(BIOMETRY_TYPE.IRIS) - expect(screen.getByRole('header', { name: 'Do you want to allow us to use Iris for sign in?' })).toBeTruthy() - expect( - screen.getByText( - 'Iris lets us recognize a video image of your eyes to sign you in to this app.\nYou can always change this later in your app settings.', - ), - ).toBeTruthy() - }) - - describe('on click of the use biometric button', () => { - it('should call setBiometricsPreference and setDisplayBiometricsPreferenceScreen', () => { - fireEvent.press(screen.getByRole('button', { name: 'Turn on Touch ID' })) - expect(setBiometricsPreference).toHaveBeenCalledWith(true) - expect(setDisplayBiometricsPreferenceScreen).toHaveBeenCalledWith(false) - }) - }) - - describe('on click of the skip button button', () => { - it('should call setDisplayBiometricsPreferenceScreen', () => { - fireEvent.press(screen.getByRole('button', { name: 'Skip' })) - expect(setDisplayBiometricsPreferenceScreen).toHaveBeenCalledWith(false) - }) - }) -}) diff --git a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.test.tsx b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.test.tsx index a9c6960fd7d..2b8926588bd 100644 --- a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.test.tsx +++ b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.test.tsx @@ -1,6 +1,7 @@ import React from 'react' import { screen } from '@testing-library/react-native' +import { t } from 'i18next' import { AppointmentAttributes, @@ -107,6 +108,11 @@ context('ClaimExamAppointment', () => { expect(screen.getByText('Clinic: Johnson Clinic suite 100')).toBeTruthy() expect(screen.getByText('Location: 123 San Jacinto Ave, San Jacinto, CA 92583')).toBeTruthy() + expect(screen.getByRole('header', { name: t('appointmentsTab.medicationWording.title') })).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet1'))).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet2'))).toBeTruthy() + expect(screen.getByRole('link', { name: t('appointmentsTab.medicationWording.claimExam.webLink') })).toBeTruthy() + expect(screen.getByRole('header', { name: 'Need to reschedule or cancel?' })).toBeTruthy() expect( screen.getByText('Call the compensation and pension office at VA Long Beach Healthcare System.'), @@ -162,6 +168,11 @@ context('ClaimExamAppointment', () => { expect(screen.getByText('Clinic: Not available')).toBeTruthy() expect(screen.getByText('Location: Not available')).toBeTruthy() + expect(screen.getByRole('header', { name: t('appointmentsTab.medicationWording.title') })).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet1'))).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet2'))).toBeTruthy() + expect(screen.getByRole('link', { name: t('appointmentsTab.medicationWording.claimExam.webLink') })).toBeTruthy() + expect(screen.getByRole('header', { name: 'Need to reschedule or cancel?' })).toBeTruthy() expect(screen.getByText('Call the compensation and pension office at VA facility.')).toBeTruthy() }) @@ -268,6 +279,11 @@ context('ClaimExamAppointment', () => { expect(screen.getByText('Clinic: Johnson Clinic suite 100')).toBeTruthy() expect(screen.getByText('Location: 123 San Jacinto Ave, San Jacinto, CA 92583')).toBeTruthy() + expect(screen.getByRole('header', { name: t('appointmentsTab.medicationWording.title') })).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet1'))).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet2'))).toBeTruthy() + expect(screen.getByRole('link', { name: t('appointmentsTab.medicationWording.claimExam.webLink') })).toBeTruthy() + expect(screen.getByRole('header', { name: 'Need to reschedule?' })).toBeTruthy() expect( screen.getByLabelText('Call the compensation and pension office at V-A Long Beach Healthcare System.'), @@ -318,6 +334,11 @@ context('ClaimExamAppointment', () => { expect(screen.getByText('Clinic: Not available')).toBeTruthy() expect(screen.getByText('Location: Not available')).toBeTruthy() + expect(screen.getByRole('header', { name: t('appointmentsTab.medicationWording.title') })).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet1'))).toBeTruthy() + expect(screen.getByText(t('appointmentsTab.medicationWording.claimExam.bullet2'))).toBeTruthy() + expect(screen.getByRole('link', { name: t('appointmentsTab.medicationWording.claimExam.webLink') })).toBeTruthy() + expect(screen.getByRole('header', { name: 'Need to reschedule?' })).toBeTruthy() expect(screen.getByText('Call the compensation and pension office at VA facility.')).toBeTruthy() }) diff --git a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.tsx b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.tsx index 8cb9103cd63..372534221d2 100644 --- a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.tsx +++ b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/ClaimExamAppointment.tsx @@ -13,6 +13,7 @@ import { AppointmentDateAndTime, AppointmentDetailsModality, AppointmentLocation, + AppointmentMedicationWording, AppointmentPersonalContactInfo, AppointmentPreferredModality, AppointmentProvider, @@ -62,6 +63,7 @@ function ClaimExamAppointment({ + , + t: TFunction, +) => { + let text = t('appointmentsTab.medicationWording.whatToBringLink') + let url = WEBVIEW_URL_WHAT_TO_BRING_TO_APPOINTMENTS + + if (type === AppointmentDetailsTypeConstants.ClaimExam) { + text = t('appointmentsTab.medicationWording.claimExam.webLink') + url = WEBVIEW_URL_APPOINTMENTS_CLAIM_EXAM_LEARN_MORE + } -const getWebViewLink = (onPress: () => void, t: TFunction) => ( - -) + return ( + { + navigateTo('Webview', { + url, + displayTitle: t('webview.vagov'), + loadingMessage: t('loading.vaWebsite'), + }) + }} + text={text} + /> + ) +} type AppointmentMedicationWordingProps = { subType: AppointmentDetailsSubType type: AppointmentDetailsScreenType } -const { WEBVIEW_URL_WHAT_TO_BRING_TO_APPOINTMENTS } = getEnv() - function AppointmentMedicationWording({ subType, type }: AppointmentMedicationWordingProps) { const { t } = useTranslation(NAMESPACE.COMMON) const navigateTo = useRouteNavigation() const body = t('appointmentsTab.medicationWording.default.body') const theme = useTheme() - const openWebviewLink = () => { - navigateTo('Webview', { - url: WEBVIEW_URL_WHAT_TO_BRING_TO_APPOINTMENTS, - displayTitle: t('webview.vagov'), - loadingMessage: t('loading.vaWebsite'), - }) - } - - const webViewLink = getWebViewLink(openWebviewLink, t) + const webViewLink = getWebViewLink(type, navigateTo, t) const getContent = () => { switch (type) { @@ -76,6 +91,21 @@ function AppointmentMedicationWording({ subType, type }: AppointmentMedicationWo /> ) + case AppointmentDetailsTypeConstants.ClaimExam: + return ( + <> + + {webViewLink} + + ) default: return null } diff --git a/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionCommon/RefillTag.tsx b/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionCommon/RefillTag.tsx index bfb654b4bdd..1c946697735 100644 --- a/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionCommon/RefillTag.tsx +++ b/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionCommon/RefillTag.tsx @@ -28,6 +28,7 @@ function RefillTag({ status }: RefillTagProps) { labelType: getTagTypeForStatus(status), onPress: () => navigateTo('StatusDefinition', { display: statusText, value: status }), a11yHint: t('prescription.history.a11yHint.status'), + a11yRole: 'link', } return ( diff --git a/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionHistory/PrescriptionHistory.tsx b/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionHistory/PrescriptionHistory.tsx index 56e50ce8038..e08faf29405 100644 --- a/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionHistory/PrescriptionHistory.tsx +++ b/VAMobile/src/screens/HealthScreen/Pharmacy/PrescriptionHistory/PrescriptionHistory.tsx @@ -216,7 +216,7 @@ function PrescriptionHistory({ navigation, route }: PrescriptionHistoryProps) { const detailsPressableProps: PressableProps = { onPress: () => prescriptionDetailsClicked(prescription), accessible: true, - accessibilityRole: 'button', + accessibilityRole: 'link', accessibilityLabel: t('prescription.history.getDetails'), } diff --git a/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.test.tsx b/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.test.tsx index 1b796a4178b..946a6c7097c 100644 --- a/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.test.tsx +++ b/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.test.tsx @@ -4,10 +4,13 @@ import { screen } from '@testing-library/react-native' import { CategoryTypeFields, + SecureMessagingFolderMessagesGetData, SecureMessagingFoldersGetData, SecureMessagingMessageGetData, + SecureMessagingSystemFolderIdConstants, SecureMessagingThreadGetData, } from 'api/types' +import { LARGE_PAGE_SIZE } from 'constants/common' import * as api from 'store/api' import { context, mockNavProps, render, waitFor, when } from 'testUtils' @@ -195,6 +198,47 @@ context('ViewMessageScreen', () => { inboxUnreadCount: 0, } + const messages: SecureMessagingFolderMessagesGetData = { + data: [ + { + type: 'test', + id: 1, + attributes: { + messageId: 1, + category: CategoryTypeFields.other, + subject: 'test', + body: 'test', + hasAttachments: false, + attachment: false, + sentDate: '1-1-21', + senderId: 2, + senderName: 'mock sender', + recipientId: 3, + recipientName: 'mock recipient name', + readReceipt: 'mock read receipt', + }, + }, + ], + links: { + self: '', + first: '', + prev: '', + next: '', + last: '', + }, + meta: { + sort: { + sentDate: 'DESC', + }, + pagination: { + currentPage: 1, + perPage: 1, + totalPages: 3, + totalEntries: 5, + }, + }, + } + const initializeTestInstance = (messageID: number = 3) => { render( { .mockResolvedValue(oldMessage) .calledWith('/v0/messaging/health/folders') .mockResolvedValue(listOfFolders) + .calledWith(`/v0/messaging/health/folders/${SecureMessagingSystemFolderIdConstants.INBOX}/messages`, { + page: '1', + per_page: LARGE_PAGE_SIZE.toString(), + useCache: 'false', + } as api.Params) + .mockResolvedValue(messages) initializeTestInstance(45) await waitFor(() => expect(screen.getByText('mock sender 45')).toBeTruthy()) await waitFor(() => expect(screen.getByText('Start new message')).toBeTruthy()) @@ -240,6 +290,12 @@ context('ViewMessageScreen', () => { .mockResolvedValue(message) .calledWith('/v0/messaging/health/folders') .mockResolvedValue(listOfFolders) + .calledWith(`/v0/messaging/health/folders/${SecureMessagingSystemFolderIdConstants.INBOX}/messages`, { + page: '1', + per_page: LARGE_PAGE_SIZE.toString(), + useCache: 'false', + } as api.Params) + .mockResolvedValue(messages) initializeTestInstance() expect(screen.getByText('Loading your message...')).toBeTruthy() await waitFor(() => expect(screen.queryByRole('link', { name: '1-800-698-2411.Thank' })).toBeFalsy()) diff --git a/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.tsx b/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.tsx index bcb6ec070e1..f4b215db10a 100644 --- a/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.tsx +++ b/VAMobile/src/screens/HealthScreen/SecureMessaging/ViewMessage/ViewMessageScreen.tsx @@ -9,7 +9,14 @@ import { useQueryClient } from '@tanstack/react-query' import { DateTime } from 'luxon' import _ from 'underscore' -import { secureMessagingKeys, useFolders, useMessage, useMoveMessage, useThread } from 'api/secureMessaging' +import { + secureMessagingKeys, + useFolderMessages, + useFolders, + useMessage, + useMoveMessage, + useThread, +} from 'api/secureMessaging' import { MoveMessageParameters, SecureMessagingAttachment, @@ -134,6 +141,15 @@ function ViewMessageScreen({ route, navigation }: ViewMessageScreenProps) { enabled: isScreenContentAllowed && smNotInDowntime, }) + const { + data: inboxMessagesData, + isFetching: loadingFolderMessages, + error: folderMessagesError, + refetch: refetchFolderMessages, + } = useFolderMessages(currentFolderIdParam, { + enabled: isScreenContentAllowed && smNotInDowntime, + }) + const folders = foldersData?.data || ([] as SecureMessagingFolderList) const message = messageData?.data.attributes || ({} as SecureMessagingMessageAttributes) const includedAttachments = messageData?.included?.filter((included) => included.type === 'attachments') @@ -159,11 +175,7 @@ function ViewMessageScreen({ route, navigation }: ViewMessageScreenProps) { useEffect(() => { if (messageFetched && currentFolderIdParam === SecureMessagingSystemFolderIdConstants.INBOX && currentPage) { let updateQueries = false - const inboxMessagesData = queryClient.getQueryData([ - secureMessagingKeys.folderMessages, - currentFolderIdParam, - ]) as SecureMessagingFolderMessagesGetData - const newInboxMessages = inboxMessagesData.data.map((m) => { + const newInboxMessages = inboxMessagesData?.data.map((m) => { if (m.attributes.messageId === message.messageId && m.attributes.readReceipt !== READ) { updateQueries = true m.attributes.readReceipt = READ @@ -209,6 +221,7 @@ function ViewMessageScreen({ route, navigation }: ViewMessageScreenProps) { messageData?.included, foldersData, messageData, + inboxMessagesData, ]) const getFolders = (): PickerItem[] => { @@ -354,8 +367,8 @@ function ViewMessageScreen({ route, navigation }: ViewMessageScreenProps) { // If error is caused by an individual message, we want the error alert to be // contained to that message, not to take over the entire screen - const hasError = foldersError || messageError || threadError || !smNotInDowntime - const isLoading = loadingFolder || loadingThread || loadingMessage || loadingMoveMessage + const hasError = folderMessagesError || foldersError || messageError || threadError || !smNotInDowntime + const isLoading = loadingFolder || loadingThread || loadingMessage || loadingMoveMessage || loadingFolderMessages const isEmpty = !message || !thread const loadingText = loadingMoveMessage ? t('secureMessaging.movingMessage') : t('secureMessaging.viewMessage.loading') @@ -385,9 +398,17 @@ function ViewMessageScreen({ route, navigation }: ViewMessageScreenProps) { ) : hasError ? ( ) : isEmpty ? ( diff --git a/VAMobile/src/screens/HomeScreen/HomeScreen.test.tsx b/VAMobile/src/screens/HomeScreen/HomeScreen.test.tsx index 6d1d16f03b8..b7971babf71 100644 --- a/VAMobile/src/screens/HomeScreen/HomeScreen.test.tsx +++ b/VAMobile/src/screens/HomeScreen/HomeScreen.test.tsx @@ -2,6 +2,7 @@ import React from 'react' import { Linking } from 'react-native' import { fireEvent, screen, waitFor } from '@testing-library/react-native' +import { t } from 'i18next' import { DateTime } from 'luxon' import { @@ -15,6 +16,7 @@ import { DEFAULT_UPCOMING_DAYS_LIMIT } from 'constants/appointments' import { get } from 'store/api' import { ErrorsState } from 'store/slices' import { RenderParams, context, mockNavProps, render, when } from 'testUtils' +import { roundToHundredthsPlace } from 'utils/formattingUtils' import { getAppointmentsPayload, getClaimsAndAppealsPayload, @@ -107,9 +109,7 @@ context('HomeScreen', () => { .mockRejectedValue('failure') initializeTestInstance() - await waitFor(() => - expect(screen.queryByText('We can’t show all activity right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('activity.error.cantShowAllActivity'))).toBeTruthy()) }) it('displays error message when one of the features are in downtime', async () => { @@ -135,10 +135,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => - expect(screen.getByText('We can’t show all activity right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.getByText(t('activity.error.cantShowAllActivity'))).toBeTruthy()) }) it('displays error message when all the features are in downtime', async () => { @@ -169,10 +167,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => - expect(screen.getByText('We can’t show all activity right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.getByText(t('activity.error.cantShowAllActivity'))).toBeTruthy()) }) it('does not display an error message when all API calls succeed', async () => { @@ -187,10 +183,8 @@ context('HomeScreen', () => { .mockResolvedValue(getPrescriptionsPayload(3)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all activity right now. Check back later.')).toBeFalsy(), - ) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.error.cantShowAllActivity'))).toBeFalsy()) }) it('displays cerner related message if veteran has a cerner facility', async () => { @@ -198,7 +192,7 @@ context('HomeScreen', () => { .calledWith('/v0/facilities-info') .mockResolvedValue(getFacilitiesPayload(true)) initializeTestInstance() - await waitFor(() => expect(screen.getByText('Information from My VA Health portal not included.')).toBeTruthy()) + await waitFor(() => expect(screen.getByText(t('activity.informationNotIncluded'))).toBeTruthy()) }) it('does not display cerner related message if veteran does not have a cerner facility', async () => { @@ -207,7 +201,7 @@ context('HomeScreen', () => { .mockResolvedValue(getFacilitiesPayload(false)) initializeTestInstance() await waitFor(() => expect(get).toBeCalledWith('/v0/facilities-info')) - await waitFor(() => expect(screen.queryByText('Information from My VA Health portal not included.')).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.informationNotIncluded'))).toBeFalsy()) }) }) @@ -218,11 +212,14 @@ context('HomeScreen', () => { .calledWith('/v0/appointments', expect.anything()) .mockResolvedValue(getAppointmentsPayload(upcomingAppointmentsCount)) initializeTestInstance() - await waitFor(() => expect(screen.getByRole('link', { name: 'Appointments' })).toBeTruthy()) + await waitFor(() => expect(screen.getByRole('link', { name: t('appointments') })).toBeTruthy()) await waitFor(() => expect( screen.getByRole('link', { - name: `${upcomingAppointmentsCount} in the next ${DEFAULT_UPCOMING_DAYS_LIMIT} days`, + name: t('appointments.activityButton.subText', { + count: upcomingAppointmentsCount, + dayCount: DEFAULT_UPCOMING_DAYS_LIMIT, + }), }), ).toBeTruthy(), ) @@ -233,7 +230,7 @@ context('HomeScreen', () => { .calledWith('/v0/appointments', expect.anything()) .mockResolvedValue(getAppointmentsPayload(3)) initializeTestInstance() - await waitFor(() => fireEvent.press(screen.getByRole('link', { name: 'Appointments' }))) + await waitFor(() => fireEvent.press(screen.getByRole('link', { name: t('appointments') }))) await waitFor(() => expect(Linking.openURL).toBeCalledWith('vamobile://appointments')) }) @@ -242,8 +239,8 @@ context('HomeScreen', () => { .calledWith('/v0/appointments', expect.anything()) .mockResolvedValue(getAppointmentsPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Appointments' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('appointments') })).toBeFalsy()) }) it('is not displayed when the API call throws an error', async () => { @@ -251,8 +248,8 @@ context('HomeScreen', () => { .calledWith('/v0/appointments', expect.anything()) .mockRejectedValue('fail') initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Appointments' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('appointments') })).toBeFalsy()) }) it('is not displayed when appointments is in downtime', async () => { @@ -271,8 +268,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Appointments' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('appointments') })).toBeFalsy()) }) }) @@ -283,8 +280,16 @@ context('HomeScreen', () => { .calledWith('/v0/claims-and-appeals-overview', expect.anything()) .mockResolvedValue(getClaimsAndAppealsPayload(activeClaimsCount)) initializeTestInstance() - await waitFor(() => expect(screen.getByRole('link', { name: 'Claims' })).toBeTruthy()) - await waitFor(() => expect(screen.getByRole('link', { name: `${activeClaimsCount} active` })).toBeTruthy()) + await waitFor(() => expect(screen.getByRole('link', { name: t('claims.title') })).toBeTruthy()) + await waitFor(() => + expect( + screen.getByRole('link', { + name: t('claims.activityButton.subText', { + count: activeClaimsCount, + }), + }), + ).toBeTruthy(), + ) }) it('navigates to Claims history screen when pressed', async () => { @@ -292,7 +297,7 @@ context('HomeScreen', () => { .calledWith('/v0/claims-and-appeals-overview', expect.anything()) .mockResolvedValue(getClaimsAndAppealsPayload(2)) initializeTestInstance() - await waitFor(() => fireEvent.press(screen.getByRole('link', { name: 'Claims' }))) + await waitFor(() => fireEvent.press(screen.getByRole('link', { name: t('claims.title') }))) await waitFor(() => expect(Linking.openURL).toBeCalledWith('vamobile://claims')) }) @@ -301,8 +306,8 @@ context('HomeScreen', () => { .calledWith('/v0/claims-and-appeals-overview', expect.anything()) .mockResolvedValue(getClaimsAndAppealsPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Claims' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('claims.title') })).toBeFalsy()) }) it('is not displayed when the API call throws an error', async () => { @@ -310,8 +315,8 @@ context('HomeScreen', () => { .calledWith('/v0/claims-and-appeals-overview', expect.anything()) .mockRejectedValue('fail') initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Claims' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('claims.title') })).toBeFalsy()) }) it('is not displayed when there is a service error', async () => { @@ -326,8 +331,8 @@ context('HomeScreen', () => { .calledWith('/v0/claims-and-appeals-overview', expect.anything()) .mockResolvedValue(getClaimsAndAppealsPayload(2, serviceErrors)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Claims' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('claims.title') })).toBeFalsy()) }) it('is not displayed when claims is in downtime', async () => { @@ -349,8 +354,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Claims' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('claims.title') })).toBeFalsy()) }) }) @@ -361,8 +366,14 @@ context('HomeScreen', () => { .calledWith('/v0/messaging/health/folders') .mockResolvedValue(getFoldersPayload(unreadMessageCount)) initializeTestInstance() - await waitFor(() => expect(screen.getByRole('link', { name: 'Messages' })).toBeTruthy()) - await waitFor(() => expect(screen.getByRole('link', { name: `${unreadMessageCount} unread` })).toBeTruthy()) + await waitFor(() => expect(screen.getByRole('link', { name: t('messages') })).toBeTruthy()) + await waitFor(() => + expect( + screen.getByRole('link', { + name: t('secureMessaging.activityButton.subText', { count: unreadMessageCount }), + }), + ).toBeTruthy(), + ) }) it('navigates to Messages screen when pressed', async () => { @@ -370,7 +381,7 @@ context('HomeScreen', () => { .calledWith('/v0/messaging/health/folders') .mockResolvedValue(getFoldersPayload(3)) initializeTestInstance() - await waitFor(() => fireEvent.press(screen.getByRole('link', { name: 'Messages' }))) + await waitFor(() => fireEvent.press(screen.getByRole('link', { name: t('messages') }))) await waitFor(() => expect(Linking.openURL).toBeCalledWith('vamobile://messages')) }) @@ -379,8 +390,8 @@ context('HomeScreen', () => { .calledWith('/v0/messaging/health/folders') .mockResolvedValue(getFoldersPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Messages' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('messages') })).toBeFalsy()) }) it('is not displayed when the API call throws an error', async () => { @@ -388,8 +399,8 @@ context('HomeScreen', () => { .calledWith('/v0/messaging/health/folders') .mockRejectedValue('fail') initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Messages' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('messages') })).toBeFalsy()) }) it('is not displayed when secure messaging is in downtime', async () => { @@ -408,8 +419,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Messages' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('messages') })).toBeFalsy()) }) }) @@ -420,9 +431,15 @@ context('HomeScreen', () => { .calledWith('/v0/health/rx/prescriptions', expect.anything()) .mockResolvedValue(getPrescriptionsPayload(refillablePrescriptionsCount)) initializeTestInstance() - await waitFor(() => expect(screen.getByRole('link', { name: 'Prescriptions' })).toBeTruthy()) + await waitFor(() => expect(screen.getByRole('link', { name: t('prescription.title') })).toBeTruthy()) await waitFor(() => - expect(screen.getByRole('link', { name: `${refillablePrescriptionsCount} ready to refill` })).toBeTruthy(), + expect( + screen.getByRole('link', { + name: t('prescriptions.activityButton.subText', { + count: refillablePrescriptionsCount, + }), + }), + ).toBeTruthy(), ) }) @@ -431,7 +448,7 @@ context('HomeScreen', () => { .calledWith('/v0/health/rx/prescriptions', expect.anything()) .mockResolvedValue(getPrescriptionsPayload(3)) initializeTestInstance() - await waitFor(() => fireEvent.press(screen.getByRole('link', { name: 'Prescriptions' }))) + await waitFor(() => fireEvent.press(screen.getByRole('link', { name: t('prescription.title') }))) await waitFor(() => expect(Linking.openURL).toBeCalledWith('vamobile://prescriptions')) }) @@ -440,8 +457,8 @@ context('HomeScreen', () => { .calledWith('/v0/health/rx/prescriptions', expect.anything()) .mockResolvedValue(getPrescriptionsPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Prescriptions' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('prescription.title') })).toBeFalsy()) }) it('is not displayed when the API call throws an error', async () => { @@ -449,8 +466,8 @@ context('HomeScreen', () => { .calledWith('/v0/health/rx/prescriptions', expect.anything()) .mockRejectedValue('fail') initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Prescriptions' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('prescription.title') })).toBeFalsy()) }) it('is not displayed when prescriptions is in downtime', async () => { @@ -469,18 +486,25 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading mobile app activity...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByRole('link', { name: 'Prescriptions' })).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('activity.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByRole('link', { name: t('prescription.title') })).toBeFalsy()) }) }) describe('About you section', () => { it('displays disability rating percentage when veteran has disability rating', async () => { + const disabilityRating = 100 when(get as jest.Mock) .calledWith('/v0/disability-rating') - .mockResolvedValue(getDisabilityRatingPayload(100)) + .mockResolvedValue(getDisabilityRatingPayload(disabilityRating)) initializeTestInstance() - await waitFor(() => expect(screen.getByLabelText('Disability rating 100% service connected')).toBeTruthy()) + await waitFor(() => + expect( + screen.getByLabelText( + `${t('disabilityRating.title')} ${t('disabilityRatingDetails.percentage', { rate: disabilityRating })} ${t('disabilityRating.serviceConnected')}`, + ), + ).toBeTruthy(), + ) }) it('does not display disability rating percentage when veteran does not have disability rating', async () => { @@ -488,8 +512,8 @@ context('HomeScreen', () => { .calledWith('/v0/disability-rating') .mockResolvedValue(getDisabilityRatingPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading your information...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByText('Disability rating')).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('disabilityRating.title'))).toBeFalsy()) }) it('does not display disability rating percentage and show error message when disability ratings API call fails', async () => { @@ -501,19 +525,22 @@ context('HomeScreen', () => { .calledWith('/v0/military-service-history') .mockResolvedValue(getMilitaryServiceHistoryPayload({} as ServiceHistoryAttributes)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading your information...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByText('Disability rating')).toBeFalsy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all your information right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('aboutYou.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('disabilityRating.title'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.error.cantShowAllInfo'))).toBeTruthy()) }) it('displays monthly payment amount when veteran has monthly compensation payment', async () => { + const monthlyAwardAmount = 3084.75 when(get as jest.Mock) .calledWith('/v0/letters/beneficiary') - .mockResolvedValue(getLetterBeneficiaryPayload(3084.75)) + .mockResolvedValue(getLetterBeneficiaryPayload(monthlyAwardAmount)) initializeTestInstance() - await waitFor(() => expect(screen.getByLabelText('Monthly compensation payment $3,084.75')).toBeTruthy()) + await waitFor(() => + expect( + screen.getByLabelText(`${t('monthlyCompensationPayment')} $${roundToHundredthsPlace(monthlyAwardAmount)}`), + ).toBeTruthy(), + ) }) it('does not display monthly payment amount when veteran does not have monthly compensation payment', async () => { @@ -521,8 +548,8 @@ context('HomeScreen', () => { .calledWith('/v0/letters/beneficiary') .mockResolvedValue(getLetterBeneficiaryPayload(0)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading your information...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByText('Monthly compensation payment')).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('monthlyCompensationPayment'))).toBeFalsy()) }) it('does not display monthly payment and show error message when the beneficiary API call fails', async () => { @@ -534,11 +561,9 @@ context('HomeScreen', () => { .calledWith('/v0/military-service-history') .mockResolvedValue(getMilitaryServiceHistoryPayload({} as ServiceHistoryAttributes)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('Loading your information...')).toBeFalsy()) - await waitFor(() => expect(screen.queryByText('Monthly compensation payment')).toBeFalsy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all your information right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('aboutYou.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('monthlyCompensationPayment'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.error.cantShowAllInfo'))).toBeTruthy()) }) it("displays message when no 'About you' info exists", async () => { @@ -551,10 +576,8 @@ context('HomeScreen', () => { .mockResolvedValue(getMilitaryServiceHistoryPayload({} as ServiceHistoryAttributes)) initializeTestInstance() - await waitFor(() => expect(screen.queryByText('We can’t show your information right now.')).toBeTruthy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all your information right now. Check back later.')).toBeFalsy(), - ) + await waitFor(() => expect(screen.queryByText(t('aboutYou.noInformation'))).toBeTruthy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.error.cantShowAllInfo'))).toBeFalsy()) }) it('displays error message when one of the features are in downtime', async () => { @@ -577,10 +600,8 @@ context('HomeScreen', () => { } as ErrorsState, }, }) - await waitFor(() => expect(screen.queryByText('Loading your information...')).toBeFalsy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all your information right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('aboutYou.loading'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.error.cantShowAllInfo'))).toBeTruthy()) }) it("displays error message when some 'About you' info doesn't exist and rest of info has errors", async () => { @@ -593,27 +614,25 @@ context('HomeScreen', () => { .mockRejectedValue('fail') initializeTestInstance() - await waitFor(() => expect(screen.queryByText('We can’t show your information right now.')).toBeFalsy()) - await waitFor(() => - expect(screen.queryByText('We can’t show all your information right now. Check back later.')).toBeTruthy(), - ) + await waitFor(() => expect(screen.queryByText(t('aboutYou.noInformation'))).toBeFalsy()) + await waitFor(() => expect(screen.queryByText(t('aboutYou.error.cantShowAllInfo'))).toBeTruthy()) }) }) describe('VA resources section', () => { it('navigates to the "Contact VA" screen when the "Contact us" link is pressed', () => { initializeTestInstance() - fireEvent.press(screen.getByRole('link', { name: 'Contact us' })) + fireEvent.press(screen.getByRole('link', { name: t('contactUs') })) expect(mockNavigationSpy).toBeCalledWith('ContactVA') }) it('launches WebView when the "Find a VA location" link is pressed', () => { initializeTestInstance() - fireEvent.press(screen.getByRole('link', { name: 'Find a VA location' })) + fireEvent.press(screen.getByRole('link', { name: t('findLocation.title') })) expect(mockNavigationSpy).toBeCalledWith('Webview', { - displayTitle: 'va.gov', + displayTitle: t('webview.vagov'), url: 'https://www.va.gov/find-locations/', - loadingMessage: 'Loading VA location finder...', + loadingMessage: t('webview.valocation.loading'), }) }) }) diff --git a/VAMobile/src/screens/HomeScreen/HomeScreen.tsx b/VAMobile/src/screens/HomeScreen/HomeScreen.tsx index cab27ac6447..75f7819ae02 100644 --- a/VAMobile/src/screens/HomeScreen/HomeScreen.tsx +++ b/VAMobile/src/screens/HomeScreen/HomeScreen.tsx @@ -65,6 +65,7 @@ import ProfileScreen from './ProfileScreen/ProfileScreen' import SettingsScreen from './ProfileScreen/SettingsScreen' import AccountSecurity from './ProfileScreen/SettingsScreen/AccountSecurity/AccountSecurity' import DeveloperScreen from './ProfileScreen/SettingsScreen/DeveloperScreen' +import OverrideAPIScreen from './ProfileScreen/SettingsScreen/DeveloperScreen/OverrideApiScreen' import RemoteConfigScreen from './ProfileScreen/SettingsScreen/DeveloperScreen/RemoteConfigScreen' import NotificationsSettingsScreen from './ProfileScreen/SettingsScreen/NotificationsSettingsScreen/NotificationsSettingsScreen' @@ -583,6 +584,11 @@ function HomeStackScreen({}: HomeStackScreenProps) { options={FEATURE_LANDING_TEMPLATE_OPTIONS} /> + + + + + ) + }) + + return ( + + +